diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ea19771
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,25 @@
+# Files and directories used during the build process:
+/build/firefox-unpacked
+/build/firefox-addon-sdk-url.txt
+/build/firefox-addon-sdk
+/build/xar-url.txt
+/build/xar/
+/lib/BabelExtResources.js
+
+# Files and directories containing unpacked extensions:
+/build/Chrome
+/build/Firefox/*.png
+/build/Firefox/data
+/build/Firefox/icons
+/build/Safari.safariextension
+
+# output directory:
+/out
+
+# Files containing sensitive information that must never go in version control:
+/build/safari-certs
+/build/Chrome.pem
+/conf/local_settings.json
+
+# miscellaneous:
+*~
diff --git a/Chrome/.gitignore b/Chrome/.gitignore
deleted file mode 100644
index 603b5e1..0000000
--- a/Chrome/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-/extension.js
-/BabelExt.js
diff --git a/Chrome/background.js b/Chrome/background.js
deleted file mode 100644
index d7e9671..0000000
--- a/Chrome/background.js
+++ /dev/null
@@ -1,89 +0,0 @@
-var contextMenuClick = function(info, tab, callbackID) {
- chrome.tabs.sendMessage(tab.id, {
- requestType: "contextMenu.click",
- callbackID: callbackID
- });
-};
-
-chrome.runtime.onMessage.addListener(
- function(request, sender, sendResponse) {
- // all requests expect a JSON object with requestType and then the relevant
- // companion information...
- switch(request.requestType) {
- case 'xmlhttpRequest':
- var xhr = new XMLHttpRequest();
- xhr.open(request.method, request.url, true);
- if (request.method === "POST") {
- xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
- // xhr.setRequestHeader("Content-length", request.data.length);
- // xhr.setRequestHeader("Connection", "close");
- }
- xhr.onreadystatechange = function() {
- if (xhr.readyState === 4) {
- // Only store 'status' and 'responseText' fields and send them back.
- var response = {status: xhr.status, responseText: xhr.responseText};
- sendResponse(response);
- }
- };
- xhr.send(request.data);
- return true; // true must be returned here to indicate successful XHR
- break;
- case 'createTab':
- var newIndex,
- focus = (request.background !== true);
-
- if (typeof(request.index) !== 'undefined') {
- newIndex = request.index;
- } else {
- // If index wasn't specified, get the selected tab so we can get the index of it.
- // This allows us to open our new tab as the "next" tab in order rather than at the end.
- newIndex = sender.tab.index+1;
- }
- chrome.tabs.create({url: request.url, selected: focus, index: newIndex});
- sendResponse({status: "success"});
- break;
- case 'createNotification':
- if (!request.icon) {
- // if no icon specified, make a single pixel empty gif so we don't get a broken image link.
- request.icon = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==';
- }
- var notification = window.webkitNotifications.createNotification(
- request.icon, // icon url - can be relative
- request.title, // notification title
- request.text // notification body text
- );
- notification.show();
- break;
- case 'localStorage':
- switch (request.operation) {
- case 'getItem':
- sendResponse({status: true, key: request.itemName, value: localStorage.getItem(request.itemName)});
- break;
- case 'removeItem':
- localStorage.removeItem(request.itemName);
- sendResponse({status: true, key: request.itemName, value: null});
- break;
- case 'setItem':
- localStorage.setItem(request.itemName, request.itemValue);
- sendResponse({status: true, key: request.itemName, value: request.itemValue});
- break;
- }
- break;
- case 'addURLToHistory':
- chrome.history.addUrl({url: request.url});
- break;
- case 'contextMenus.create':
- if (typeof request.obj.onclick === 'number') {
- var callbackID = request.obj.onclick;
- request.obj.onclick = function(info, tab) {
- contextMenuClick(info, tab, callbackID);
- };
- }
- chrome.contextMenus.create(request.obj);
- break;
- default:
- sendResponse({status: "unrecognized request type"});
- break;
- }
- }
-);
diff --git a/Chrome/manifest.json b/Chrome/manifest.json
deleted file mode 100644
index d35165f..0000000
--- a/Chrome/manifest.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
- "name": "BabelExt Extension",
- "version": "0.95",
- "manifest_version": 2,
- "description": "An extension created with BabelExt - www.babelext.com",
- "update_url": "http://babelext.com/update-chrome.php",
- "background": {
- "scripts": ["background.js"]
- },
- "content_scripts": [
- {
- "matches": [
- "http://babelext.com/*"
- ],
- "js": ["BabelExt.js", "extension.js"] // add others here if you like, i.e. jquery...
- }
- ],
- "icons": {
- // "48": "icon48.png",
- // "128": "icon128.png"
- },
- "permissions": [
- "contextMenus",
- "tabs",
- "history",
- "notifications"
- ]
-}
\ No newline at end of file
diff --git a/Firefox/README.md b/Firefox/README.md
deleted file mode 100644
index 49a92fc..0000000
--- a/Firefox/README.md
+++ /dev/null
@@ -1 +0,0 @@
-The Firefox Addon SDK requires you have a README.md present.
\ No newline at end of file
diff --git a/Firefox/data/.gitignore b/Firefox/data/.gitignore
deleted file mode 100644
index 603b5e1..0000000
--- a/Firefox/data/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-/extension.js
-/BabelExt.js
diff --git a/Firefox/lib/main.js b/Firefox/lib/main.js
deleted file mode 100644
index 6d09acc..0000000
--- a/Firefox/lib/main.js
+++ /dev/null
@@ -1,184 +0,0 @@
-// Import the APIs we need.
-
-var pageMod = require("page-mod");
-var Request = require("request").Request;
-var notifications = require("notifications");
-var self = require("self");
-var tabs = require("tabs");
-var ss = require("simple-storage");
-var workers = [];
-var contextMenu = require("context-menu");
-var priv = require("private-browsing");
-var windows = require("sdk/windows").browserWindows;
-
-// require chrome allows us to use XPCOM objects...
-const {Cc, Ci, Cu, Cr} = require("chrome");
-
-var historyService = Cc["@mozilla.org/browser/history;1"].getService(Ci.mozIAsyncHistory);
-
-// this function takes in a string (and optional charset, paseURI) and creates an nsURI object, which is required by historyService.addURI...
-function makeURI(aURL, aOriginCharset, aBaseURI) {
- var ioService = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
- return ioService.newURI(aURL, aOriginCharset, aBaseURI);
-}
-
-
-function detachWorker(worker, workerArray) {
- var index = workerArray.indexOf(worker);
- if(index != -1) {
- workerArray.splice(index, 1);
- }
-}
-var localStorage = ss.storage;
-
-// these aliases are just for simplicity, so that the code here looks just like background code
-// for all of the other browsers...
-localStorage.getItem = function(key) {
- return ss.storage[key];
-};
-localStorage.setItem = function(key, value) {
- ss.storage[key] = value;
-};
-localStorage.removeItem = function(key) {
- delete ss.storage[key];
-};
-
-pageMod.PageMod({
- include: ["*.babelext.com"],
- contentScriptWhen: 'ready',
- contentScriptFile: [self.data.url('BabelExt.js'), self.data.url('extension.js')],
- onAttach: function(worker) {
- tabs.on('activate', function(tab) {
- // run some code when a tab is activated...
- });
-
- workers.push(worker);
- worker.on('detach', function () {
- detachWorker(this, workers);
- // console.log('worker detached, total now: ' + workers.length);
- });
-
- worker.on('message', function(data) {
- var request = data;
- switch(request.requestType) {
- case 'xmlhttpRequest':
- var responseObj = {
- callbackID: request.callbackID,
- name: request.requestType
- };
- if (request.method == 'POST') {
- Request({
- url: request.url,
- onComplete: function(response) {
- responseObj.response = {
- responseText: response.text,
- status: response.status
- };
- worker.postMessage(responseObj);
- },
- headers: request.headers,
- content: request.data
- }).post();
- } else {
- Request({
- url: request.url,
- onComplete: function(response) {
- responseObj.response = {
- responseText: response.text,
- status: response.status
- };
- worker.postMessage(responseObj);
- },
- headers: request.headers,
- content: request.data
- }).get();
- }
- break;
- case 'createTab':
- var focus = (request.background !== true);
- tabs.open({url: request.url, inBackground: !focus });
- worker.postMessage({status: "success"});
- break;
- case 'createNotification':
- if (!request.icon) {
- // if no icon specified, make a single pixel empty gif so we don't get a broken image link.
- request.icon = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==';
- }
- notifications.notify({
- title: request.title,
- text: request.text,
- iconURL: request.icon
- });
- break;
- case 'localStorage':
- switch (request.operation) {
- case 'getItem':
- worker.postMessage({
- name: 'localStorage',
- callbackID: request.callbackID,
- status: true,
- key: request.itemName,
- value: localStorage.getItem(request.itemName)
- });
- break;
- case 'removeItem':
- localStorage.removeItem(request.itemName);
- worker.postMessage({
- name: 'localStorage',
- callbackID: request.callbackID,
- status: true,
- value: null
- });
- break;
- case 'setItem':
- localStorage.setItem(request.itemName, request.itemValue);
- worker.postMessage({
- name: 'localStorage',
- callbackID: request.callbackID,
- status: true,
- key: request.itemName,
- value: request.itemValue
- });
- break;
- }
- break;
- case 'addURLToHistory':
- var isPrivate = priv.isPrivate(windows.activeWindow);
- if (isPrivate) {
- // do not add to history if in private browsing mode!
- return false;
- }
- var uri = makeURI(request.url);
- historyService.updatePlaces({
- uri: uri,
- visits: [{
- transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
- visitDate: Date.now() * 1000
- }]
- });
- break;
- case 'contextMenus.create':
- contextMenu.Item({
- label: request.obj.title,
- context: contextMenu.PageContext(),
- data: request.obj.onclick,
- contentScript: 'self.on("click", function (node, data) {' +
- 'self.postMessage(data);' +
- '});',
- onMessage: function(onclick) {
- worker.postMessage({
- name: 'contextMenus.click',
- callbackID: onclick
- });
- }
-
- });
- break;
- default:
- worker.postMessage({status: "unrecognized request type"});
- break;
- }
-
- });
- }
-});
diff --git a/Firefox/package.json b/Firefox/package.json
deleted file mode 100644
index 3379d5d..0000000
--- a/Firefox/package.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "description": "An extension created with BabelExt - www.babelext.com",
- "license": "GPL",
- "author": "honestbleeps",
- "version": "0.95",
- "fullName": "BabelExt",
- "id": "jid1-h7LuK9FSeAYouw",
- "name": "babelext_your_name_here"
-}
diff --git a/Opera/config.xml b/Opera/config.xml
deleted file mode 100644
index 90a9838..0000000
--- a/Opera/config.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
- BabelExt Extension
- An extension created with BabelExt - www.babelext.com
-
- Steve Sobel
-
-
-
diff --git a/Opera/includes/.gitignore b/Opera/includes/.gitignore
deleted file mode 100644
index 8e35b06..0000000
--- a/Opera/includes/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-/extension.user.js
-/BabelExt.js
\ No newline at end of file
diff --git a/Opera/index.html b/Opera/index.html
deleted file mode 100644
index 6d22bc6..0000000
--- a/Opera/index.html
+++ /dev/null
@@ -1,133 +0,0 @@
-
-
\ No newline at end of file
diff --git a/Opera/notification.html b/Opera/notification.html
deleted file mode 100644
index 2379514..0000000
--- a/Opera/notification.html
+++ /dev/null
@@ -1,68 +0,0 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/README.md b/README.md
index 0ae8ba7..6b170fb 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,11 @@
MIT (X11) license. See LICENSE.txt
+### Building ###
+
+To build run ./script/build.sh build chrome|safari|amo
+
+
### What is BabelExt? ###
BabelExt is a library (or perhaps more of a boilerplate) meant to simplify the
@@ -34,6 +39,8 @@ has its own function calls and way of working, including, but not limited to:
- Accessing and controlling tabs (i.e. opening a link in a new one and choosing if it's focused)
- Cross domain http requests (extensions require)
- Storing data (using HTML5 localStorage or similar/equivalent engines)
+- Managing resources (like large HTML snippets that are hard to read in raw JavaScript)
+- Managing add-on preferences (which some browsers call options or settings)
- Triggering notifications (desktop or browser, depending on the browser's particular level of support)
- Adding URLs to history (to mark links as visited)
- Note: this is a bit of a hack in all non-Chrome browsers...
@@ -48,10 +55,11 @@ websites or functionality on the web. For this reason, functionality that is no
by one or more of the 4 BabelExt browsers (Chrome, Firefox, Opera, Safari) may not be added
to BabelExt.
-BabelExt also isn't meant to handle building each browser's native settings consoles/panels, etc.
-They're just too different from each other to try and abstract into a nice little package,
-and with the 4 supported browsers all handling modern HTML/CSS/Javascript so well - it makes
-sense (to me, anyhow) to build settings consoles and the like using those technologies.
+Because each browser implements preferences in a slightly different way, BabelExt only supports
+the baseline functionality that can be supported across all browsers. That might be enough if
+you only need a few buttons and options, but with the 4 supported browsers all handling modern
+HTML/CSS/Javascript so well - it makes sense (to me, anyhow) to build preference pages into the
+site your extension is for.
That's what I did with Reddit Enhancement Suite, and it has worked rather well. I am considering
adding the automatic form rendering code from RES into BabelExt, but I will need to devote some
@@ -61,36 +69,36 @@ thought to how to make it more universally useful.
First, download all of the source from Github and put it together within a folder.
-In Windows, run `makelinks.bat` to create symlinks to extension.js - these links are not
-handled by github, which is why you unfortuntately have to make them yourself.
-**NOTE:** You may need to open a command prompt as Administrator for this batch file to
-work.
+Then, download [PhantomJS](http://phantomjs.org), which is used to build and deploy extensions.
-In UNIX-based OSes, you can run `makelinks.sh`. Note that this will make hardlinks.
+Next, rename `conf/local_settings.json.example` to `conf/local_settings.json`. You will need
+to edit this when you release your extension, but the defaults should be fine for now.
-**IMPORTANT OPERA NOTE:** Note that the Opera js file has .user.js in it - that's because without this,
-@include and @exclude directives will be ignored and your script will run on every page on
-the internet!
+In UNIX-based OSes, run `./script/build.sh build ` to build packages for each browser,
+and `./script/build.sh release ` to release them to the various extension sites.
-**IMPORTANT SAFARI NOTE:** Safari has a "security feature" that is not documented, gives no user
-feedback at all, and can be a HUGE time sink if you don't know about it! If you have any
-files in your extension folder that are symlinks, Safari will **silently** ignore them.
-With Safari, a hard link will work, but a symbolic link will not. If you made the links
-yourself instead of using the batch file, and your extension is doing nothing at all in
-Safari, double check that!
+The build system hasn't been tested under Windows yet - your best bet is probably to look at
+the scripts and write a Windows equivalent. If it's any good, please send in a patch!
-One last Safari quirk: if the directory does not end in ".safariextension", it will not be
-recognized by Safari. Don't remove that from the name!
+The build system maintains browser-specific `build` directories based on `conf/settings.json`.
+It uses symbolic links where possible, but falls back to hard links for Chrome and Safari
+(which silently ignore symlinks).
+
+It is recommended run `./script/build.sh maintain &` in the background.
+This automatically fixes broken hard links and updates `BabelExt.resources` every few seconds.
## Instructions for loading/testing an extension in each browser ##
-### Chrome ###
+- You need to build the package before you start - the initial build
+ process configures some files that aren't stored in git
+
+### Chrome / Opera ###
-- Click the wrench icon and choose Tools -> Extensions
+- Go to about://extensions
-- Check the "Developer Mode" checkbox
+- Check "Developer Mode"
-- Click "load unpacked extension" and choose the Chrome directory
+- Click "load unpacked extension" and choose the build/Chrome directory
- You're good to go! If you just want to try out the BabelExt kitchen sink demo, navigate to [http://babelext.com/demo/](http://babelext.com/demo/)
@@ -98,26 +106,19 @@ recognized by Safari. Don't remove that from the name!
### Firefox ###
-- Download the Firefox Addon SDK from [https://addons.mozilla.org/en-US/developers/builder](https://addons.mozilla.org/en-US/developers/builder)
-
-- Follow the installation instructions there, and run the appropriate activation script (i.e. bin\activate.bat in windows)
+- Go to about:addons, click the "Tools" icon in the top-right and install the add-on from file
-- Navigate to the Firefox directory under BabelExt, and type: cfx run
+- Go to about:support and click the "Open Directory" to go to your profile directory
-- You're good to go! If you just want to try out the BabelExt kitchen sink demo, navigate to [http://babelext.com/demo/](http://babelext.com/demo/)
-
-- Further Firefox development information can be found at [https://addons.mozilla.org/en-US/developers/docs/sdk/latest/](https://addons.mozilla.org/en-US/developers/docs/sdk/latest/)
-
-### Opera ###
+- Open the "extensions" subdirectory and look for a subdirectory matching the "id" in your settings.json file
-- Click Tools -> Extensions -> Manage Extensions
+- Delete the file and replace it with a link to your extension's "build/firefox-unpacked" directory
-- Find the config.xml file in the Opera directory of BabelExt, and drag it to the Extensions window
+- Restart Firefox
- You're good to go! If you just want to try out the BabelExt kitchen sink demo, navigate to [http://babelext.com/demo/](http://babelext.com/demo/)
-- Further Opera development information can be found at [http://dev.opera.com/addons/extensions/](http://dev.opera.com/addons/extensions/)
-
+- Further Firefox development information: [Add-on SDK](https://addons.mozilla.org/en-US/developers/docs/sdk/latest/) and [setting up an extension development environment](https://developer.mozilla.org/en-US/Add-ons/Setting_up_extension_development_environment)
### Safari ###
@@ -129,8 +130,49 @@ recognized by Safari. Don't remove that from the name!
- Click the + button at the bottom left, and choose "Add Extension"
-- Choose the Safari.safariextension folder from BabelExt
+- Choose the build/Safari.safariextension folder from BabelExt
- You're good to go! If you just want to try out the BabelExt kitchen sink demo, navigate to [http://babelext.com/demo/](http://babelext.com/demo/)
- Further Safari development information can be found at [https://developer.apple.com/library/safari/#documentation/Tools/Conceptual/SafariExtensionGuide/Introduction/Introduction.html](https://developer.apple.com/library/safari/#documentation/Tools/Conceptual/SafariExtensionGuide/Introduction/Introduction.html)
+
+#### Certificates ####
+
+Safari requires all packages to be signed with a private key that's been registered with Apple.
+You can develop unpacked extensions without a license, but you will need a (free) Apple Developer
+account to build a package. You will also need to create a private key, which you can do with:
+
+ openssl req -new -nodes -newkey rsa:2048 -keyout build/safari-info/id.rsa -out apple-cert.csr
+
+Apple seems to prefer you have a single private key per Apple Developer account.
+If you maintain several projects with one account, consider linking build/safari-certs to a central location.
+
+BabelExt will automatically register your key and download extra certificates if you pass in your
+username and password. Here are the steps if you prefer to do it by hand:
+
+- Go through [Apple's Certificate Request process](https://developer.apple.com/account/safari/certificate/certificateRequest.action) and save your certificate as `build/safari-certs/local.cer`
+- Download [Apple's Worldwide Developer Relations Certificate](https://developer.apple.com/certificationauthority/AppleWWDRCA.) to `build/safari-certs/AppleIncRootCertificate.cer`
+- Download [Apple's Root Certificate](https://www.apple.com/appleca/AppleIncRootCertificate.cer) to `build/safari-certs/AppleWWDRCA.cer`
+- Download and compile [a modified version of the "xar" tool](http://mackyle.github.io/xar/) as `build/xar`
+
+Note: some online documentation refers to these keys as `cert00`, `cert01` and `cert02`
+(these are the names `xar` uses when extracting them from a package)
+
+## Resetting extension data ##
+
+If your extension uses storage or preferences, you will need to test the extension data with
+different stored values. Apart from Safari, all the browsers let you create multiple
+profiles ("users" in Chrome), so you might want to create throwaway profiles for use during
+testing.
+
+Private browsing isn't much help here, as some private browsing data will be initialised from
+your public data. If you find profiles too much effort, Chrome/Opera also let you clear
+extension data by deleting all files matching /Local*/**
+
+## Releasing packages ##
+
+You need to release the first version of your extension by hand, because each site has slightly
+different requirements for their extensions.
+
+After the initial release, fill in `local_settings.json` and run `script/build.sh release `
+to release and update metadata.
diff --git a/Safari.safariextension/.gitignore b/Safari.safariextension/.gitignore
deleted file mode 100644
index ab2f236..0000000
--- a/Safari.safariextension/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-/extension.js
-/BabelExt.js
\ No newline at end of file
diff --git a/Safari.safariextension/notification.png b/Safari.safariextension/notification.png
deleted file mode 100644
index 4abc65b..0000000
Binary files a/Safari.safariextension/notification.png and /dev/null differ
diff --git a/build/Chrome/.gitignore b/build/Chrome/.gitignore
new file mode 100644
index 0000000..dcaffc0
--- /dev/null
+++ b/build/Chrome/.gitignore
@@ -0,0 +1 @@
+/*.js
diff --git a/build/Chrome/background.js b/build/Chrome/background.js
new file mode 100644
index 0000000..4cf2fc7
--- /dev/null
+++ b/build/Chrome/background.js
@@ -0,0 +1,194 @@
+var contextMenuClick = function(info, tab, callbackID) {
+ chrome.tabs.sendMessage(tab.id, {
+ requestType: "contextMenu.click",
+ callbackID: callbackID
+ });
+};
+
+var memoryStorage = { storage: {} };
+memoryStorage. getItem = function(key ) { return memoryStorage.storage[key] };
+memoryStorage. setItem = function(key, value) { memoryStorage.storage[key] = value };
+memoryStorage.removeItem = function(key ) { delete memoryStorage.storage[key] };
+
+chrome.runtime.onMessage.addListener(
+ function(request, sender, sendResponse) {
+ // all requests expect a JSON object with requestType and then the relevant
+ // companion information...
+ switch(request.requestType) {
+ case 'xmlhttpRequest':
+ var xhr = new XMLHttpRequest();
+ xhr.open(request.method, request.url, true, request.user, request.password);
+ if (request.method === "POST") {
+ xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
+ // xhr.setRequestHeader("Content-length", request.data.length);
+ // xhr.setRequestHeader("Connection", "close");
+ }
+ Object.keys(request.headers).forEach(function(header) { xhr.setRequestHeader(header, request.headers[header]) });
+ if ( typeof(request.overrideMimeType) != 'undefined' ) xhr.overrideMimeType = request.overrideMimeType;
+ xhr.onload = function() {
+ var response = {status: xhr.status, statusText: xhr.statusText, responseText: xhr.responseText, _response_headers: xhr.getAllResponseHeaders()};
+ sendResponse(response);
+ }
+ xhr.onerror = function() {
+ var response = {status: xhr.status, statusText: xhr.statusText, responseText: xhr.responseText, _response_headers: xhr.getAllResponseHeaders(), error: true};
+ sendResponse(response);
+ }
+ xhr.send(request.data);
+ return true; // true must be returned here to indicate successful XHR
+ break;
+ case 'createTab':
+ var newIndex,
+ focus = (request.background !== true);
+
+ if (typeof(request.index) !== 'undefined') {
+ newIndex = request.index;
+ } else {
+ // If index wasn't specified, get the selected tab so we can get the index of it.
+ // This allows us to open our new tab as the "next" tab in order rather than at the end.
+ newIndex = sender.tab.index+1;
+ }
+ chrome.tabs.create({url: request.url, selected: focus, index: newIndex});
+ sendResponse({status: "success"});
+ break;
+ case 'createNotification':
+ if (!request.icon) {
+ // if no icon specified, make a single pixel empty gif so we don't get a broken image link.
+ request.icon = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==';
+ }
+ var notification = window.webkitNotifications.createNotification(
+ request.icon, // icon url - can be relative
+ request.title, // notification title
+ request.text // notification body text
+ );
+ notification.show();
+ break;
+ case 'localStorage':
+ switch (request.operation) {
+ case 'getItem':
+ sendResponse({status: true, key: request.itemName, value: localStorage.getItem(request.itemName)});
+ break;
+ case 'removeItem':
+ localStorage.removeItem(request.itemName);
+ sendResponse({status: true, key: request.itemName, value: null});
+ break;
+ case 'setItem':
+ localStorage.setItem(request.itemName, request.itemValue);
+ sendResponse({status: true, key: request.itemName, value: request.itemValue});
+ break;
+ }
+ break;
+ case 'memoryStorage':
+ switch (request.operation) {
+ case 'getItem':
+ sendResponse({status: true, key: request.itemName, value: memoryStorage.getItem(request.itemName)});
+ break;
+ case 'removeItem':
+ memoryStorage.removeItem(request.itemName);
+ sendResponse({status: true, key: request.itemName, value: null});
+ break;
+ case 'setItem':
+ memoryStorage.setItem(request.itemName, request.itemValue);
+ sendResponse({status: true, key: request.itemName, value: request.itemValue});
+ break;
+ }
+ break;
+ case 'addURLToHistory':
+ chrome.history.addUrl({url: request.url});
+ break;
+ case 'contextMenus.create':
+ if (typeof request.obj.onclick === 'number') {
+ var callbackID = request.obj.onclick;
+ request.obj.onclick = function(info, tab) {
+ contextMenuClick(info, tab, callbackID);
+ };
+ }
+ // id not available on firefox but title is, use it as common id
+ request.obj.id = request.obj.title;
+ chrome.contextMenus.create(request.obj);
+ break;
+ case 'contextMenus.remove':
+ chrome.contextMenus.remove(request.obj.title);
+ break;
+ default:
+ sendResponse({status: "unrecognized request type"});
+ break;
+ }
+ }
+);
+
+// chrome.storage.local should return almost instantly, but has been seen in the wild timing out.
+// We do a test request first, and use a fallback implementation if that takes too long.
+// The fallback implementation always returns default values.
+var storage_local_works = true, storage_start_time = new Date().getTime();
+try {
+ chrome.storage.local.get('', function() {
+ storage_local_works = new Date().getTime() - storage_start_time < 1000;
+ if (!storage_local_works) {
+ console.log( 'chrome.storage.local took too long to respond - disabing.', chrome.runtime.lastError );
+ }
+ });
+} catch (e) {
+ storage_local_works = false;
+ console.log('chrome.storage.local disabled: ', e);
+ console.log('This extension will still work, but will act as if all options have the default value.');
+}
+
+// the simple "onMessage" interface only works when the response is sent sychronously.
+// Because preferences need to respond after a delay, we have to use the full interface:
+chrome.runtime.onConnect.addListener(function(port) {
+ console.assert(port.name == "delayedMessage");
+ if (storage_local_works) { // default behaviour
+ port.onMessage.addListener(function(request) {
+ function sendResponse(response) { port.postMessage({ request: request, response: response }) }
+ // all requests expect a JSON object with requestType and then the relevant
+ // companion information...
+ switch(request.requestType) {
+ case 'preferences':
+ switch (request.operation) {
+ case 'getItem':
+ chrome.storage.local.get(request.itemName, function(items) {
+ sendResponse({status: true, key: request.itemName, value: (items||{}).hasOwnProperty(request.itemName) ? items[request.itemName] : default_preferences[request.itemName]});
+ });
+ break;
+ case 'setItem':
+ var toSet = {}; toSet[request.itemName] = request.itemValue;
+ chrome.storage.local.set(toSet, function() {
+ sendResponse({status: true, key: request.itemName, value: request.itemValue});
+ });
+ break;
+ }
+ }
+ });
+ } else { // fallback behaviour - return default values without waiting for the storage system
+ port.onMessage.addListener(function(request) {
+ function sendResponse(response) { port.postMessage({ request: request, response: response }) }
+ switch(request.requestType) {
+ case 'preferences':
+ switch (request.operation) {
+ case 'getItem':
+ sendResponse({status: true, key: request.itemName, value: default_preferences[request.itemName]});
+ break;
+ case 'setItem':
+ sendResponse({status: false, key: request.itemName, value: request.itemValue});
+ break;
+ }
+ }
+ });
+ }
+});
+
+if ( auto_reload ) {
+ // Chrome defines a "fast reload" as a reload within 10 seconds of the previous one.
+ // Five consecutive fast reloads and the extension is disabled for a little while.
+ // Ideally we'd allow a small burst, but that would require us to store the burst count across reloads
+ var reload_timeout = new Date().getTime() + 10000;
+ chrome.webNavigation.onBeforeNavigate.addListener(function(data) {
+ var time = reload_timeout - new Date().getTime()
+ if ( time < 0 ) {
+ chrome.runtime.reload();
+ reload_timeout = new Date().getTime() + 10000;
+ } else {
+ console.log( 'Fast reload detected - must wait ' + (time/1000) + ' seconds before reloading the extension again' );
+ }
+ });
+}
diff --git a/build/Chrome/chrome-bootstrap.css b/build/Chrome/chrome-bootstrap.css
new file mode 100644
index 0000000..c481188
--- /dev/null
+++ b/build/Chrome/chrome-bootstrap.css
@@ -0,0 +1,719 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2012 Chrome Bootstrap authors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+.chrome-bootstrap {
+ font-family: 'Segoe UI', 'Chrome Droid Sans', 'Droid Sans Fallback', 'Lucida Grande', 'Tahoma', sans-serif;
+ font-size: 12px;
+ color: #303942;
+ cursor: default;
+ margin: 0;
+ /* Headings
+ ============================================== */
+ /* Layout
+ ============================================== */
+ /* Header
+ ============================================== */
+ /* View sections
+ ============================================== */
+ /* Control bar
+ ============================================== */
+ /* Pagination
+ ============================================== */
+ /* Alert
+ ============================================== */
+ /* Tags
+ ============================================== */
+ /* Main menu
+ ============================================== */
+ /* Icons
+ ============================================== */
+ /* Highlightable list
+ ============================================== */
+ /* Input styling
+ ============================================== */
+ /* Focused --------------------------------- */
+ /* Disabled --------------------------------- */
+ /* Hovering --------------------------------- */
+ /* Active --------------------------------- */
+ /* Modal
+ ============================================== */
+}
+.chrome-bootstrap a {
+ border: none;
+ color: #15C;
+ cursor: pointer;
+ text-decoration: underline;
+ font-weight: normal;
+}
+.chrome-bootstrap a:hover,
+.chrome-bootstrap a:focus {
+ outline: none;
+}
+.chrome-bootstrap ul,
+.chrome-bootstrap ol {
+ padding: 0;
+}
+.chrome-bootstrap li {
+ list-style-type: none;
+}
+.chrome-bootstrap dl,
+.chrome-bootstrap dt,
+.chrome-bootstrap dd {
+ margin: 0;
+}
+.chrome-bootstrap button {
+ cursor: pointer;
+}
+.chrome-bootstrap h1,
+.chrome-bootstrap h2,
+.chrome-bootstrap h3,
+.chrome-bootstrap h4 {
+ -webkit-user-select: none;
+ font-weight: normal;
+ line-height: 1;
+}
+.chrome-bootstrap h1 small,
+.chrome-bootstrap h2 small,
+.chrome-bootstrap h3 small,
+.chrome-bootstrap h4 small {
+ font-size: 15px;
+ margin: 0 10px;
+ color: #53637D;
+}
+.chrome-bootstrap h1 {
+ -webkit-margin-after: 1em;
+ -webkit-margin-before: 21px;
+ -webkit-margin-start: 23px;
+ height: 18px;
+ font-size: 18px;
+}
+.chrome-bootstrap h1 a {
+ color: #5C6166;
+ text-decoration: none;
+}
+.chrome-bootstrap h3 {
+ color: black;
+ font-size: 1.2em;
+ margin-bottom: 0.8em;
+}
+.chrome-bootstrap h4 {
+ font-size: 1em;
+ margin-bottom: 5px;
+}
+.chrome-bootstrap .frame .navigation {
+ height: 100%;
+ -webkit-margin-start: 0;
+ position: fixed;
+ -webkit-margin-end: 15px;
+ width: 155px;
+ z-index: 3;
+}
+.chrome-bootstrap .frame .view,
+.chrome-bootstrap .frame .content {
+ width: 738px;
+ overflow-x: hidden;
+}
+.chrome-bootstrap .frame .content {
+ padding-top: 55px;
+}
+.chrome-bootstrap .frame .content p {
+ text-align: justify;
+}
+.chrome-bootstrap .frame .with_controls .content {
+ padding-top: 104px;
+}
+.chrome-bootstrap .frame .view {
+ -webkit-margin-start: 155px;
+}
+.chrome-bootstrap .frame .view a {
+ font: inherit;
+}
+.chrome-bootstrap .frame .mainview > * {
+ -webkit-margin-start: -20px;
+ -webkit-transition: margin 100ms, opacity 100ms;
+ opacity: 0;
+ z-index: 0;
+ position: absolute;
+ top: 0;
+ display: block;
+}
+.chrome-bootstrap .frame .mainview > .selected {
+ -webkit-margin-start: 0;
+ -webkit-transition: margin 200ms, opacity 200ms;
+ -webkit-transition-delay: 100ms;
+ z-index: 1;
+ opacity: 1;
+}
+.chrome-bootstrap header {
+ position: fixed;
+ background-image: -webkit-linear-gradient(#ffffff, #ffffff 40%, rgba(255, 255, 255, 0.92));
+ width: 738px;
+ z-index: 2;
+}
+.chrome-bootstrap header h1 {
+ padding: 21px 0 13px;
+ margin: 0;
+ border-bottom: 1px solid #EEE;
+}
+.chrome-bootstrap header .corner {
+ position: absolute;
+ right: 0px;
+ top: 21px;
+}
+.chrome-bootstrap header .corner input[type="text"] {
+ width: 210px;
+}
+.chrome-bootstrap header .corner.cancelable .delete {
+ opacity: 1;
+ top: 4px;
+ right: 5px;
+}
+.chrome-bootstrap section {
+ -webkit-padding-start: 18px;
+ margin-bottom: 24px;
+ margin-top: 8px;
+ max-width: 600px;
+}
+.chrome-bootstrap section h3 {
+ -webkit-margin-start: -18px;
+}
+.chrome-bootstrap section .row {
+ display: block;
+ margin: 0.65em 0;
+}
+.chrome-bootstrap .controls {
+ -webkit-padding-end: 3px;
+ -webkit-padding-start: 4px;
+ -webkit-transition: padding 100ms, height 100ms, opacity 100ms;
+ border-bottom: 1px solid #EEE;
+ display: -webkit-box;
+ overflow: hidden;
+ padding: 13px 0;
+ position: relative;
+}
+.chrome-bootstrap .controls .text {
+ display: inline-block;
+ margin-top: 4px;
+}
+.chrome-bootstrap .controls .spacer {
+ -webkit-box-flex: 1;
+}
+.chrome-bootstrap ol.pagination li {
+ margin: 0 2px;
+ display: inline-block;
+ line-height: 25px;
+}
+.chrome-bootstrap ol.pagination a {
+ width: 25px;
+ height: 24px;
+ text-align: center;
+ display: block;
+ background: #F0F6FE;
+ text-decoration: none;
+}
+.chrome-bootstrap ol.pagination a:hover,
+.chrome-bootstrap ol.pagination a.selected {
+ background: #8AAAED;
+ color: #FFF;
+}
+.chrome-bootstrap .alert {
+ border-radius: 3px;
+ background: rgba(147, 184, 252, 0.2);
+ display: block;
+ position: relative;
+ padding: 10px 30px 10px 10px;
+ line-height: 17px;
+}
+.chrome-bootstrap .alert .delete {
+ top: 5px;
+ right: 6px;
+ opacity: 1;
+}
+.chrome-bootstrap ul.tags li {
+ background: #8AAAED;
+ color: #FFF;
+ border-radius: 3px;
+ position: relative;
+ display: inline-block;
+ padding: 2px 5px;
+}
+.chrome-bootstrap ul.tags li a {
+ color: #FFF;
+ text-decoration: none;
+}
+.chrome-bootstrap ul.tags li a:hover {
+ text-decoration: underline;
+}
+.chrome-bootstrap ul.tags li .delete {
+ opacity: 1;
+ position: relative;
+ display: inline-block;
+ width: 13px;
+ height: 12px;
+ top: 1px;
+ background-position-y: -1px;
+}
+.chrome-bootstrap ul.menu {
+ -webkit-margin-before: 1em;
+ -webkit-margin-after: 2em;
+ -webkit-margin-start: 0px;
+ -webkit-margin-end: 0px;
+ -webkit-padding-start: 40px;
+ list-style-type: none;
+ padding: 0;
+}
+.chrome-bootstrap ul.menu li {
+ -webkit-border-start: 6px solid transparent;
+ -webkit-padding-start: 18px;
+ -webkit-user-select: none;
+ display: list-item;
+ text-align: -webkit-match-parent;
+}
+.chrome-bootstrap ul.menu li.selected {
+ -webkit-border-start-color: #4e5764;
+}
+.chrome-bootstrap ul.menu li.selected a {
+ color: #464E5A;
+}
+.chrome-bootstrap ul.menu li a {
+ border: 0;
+ color: #999;
+ cursor: pointer;
+ font: inherit;
+ line-height: 29px;
+ margin: 0;
+ padding: 0;
+ text-decoration: none;
+ display: block;
+}
+.chrome-bootstrap .arrow_collapse {
+ border-top: 5px solid transparent;
+ border-bottom: 5px solid transparent;
+ border-left: 6px solid #999;
+ -webkit-margin-end: 4px;
+ top: 1px;
+}
+.chrome-bootstrap .arrow_expand {
+ border-left: 5px solid transparent;
+ border-right: 5px solid transparent;
+ border-top: 7px solid #999;
+ -webkit-margin-end: 4px;
+}
+.chrome-bootstrap .arrow {
+ width: 0;
+ height: 0;
+ position: relative;
+ display: inline-block;
+}
+.chrome-bootstrap .delete {
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAiElEQVR42r2RsQrDMAxEBRdl8SDcX8lQPGg1GBI6lvz/h7QyRRXV0qUULwfvwZ1tenw5PxToRPWMC52eA9+WDnlh3HFQ/xBQl86NFYJqeGflkiogrOvVlIFhqURFVho3x1moGAa3deMs+LS30CAhBN5nNxeT5hbJ1zwmji2k+aF6NENIPf/hs54f0sZFUVAMigAAAABJRU5ErkJggg==");
+ background-repeat: no-repeat;
+ display: block;
+ opacity: 0;
+ height: 14px;
+ width: 14px;
+ -webkit-transition: 150ms opacity;
+ background-color: transparent;
+ text-indent: -5000px;
+ position: absolute;
+}
+.chrome-bootstrap .delete:hover {
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAqklEQVR4XqWRMQ6DMAxF/1Fyilyj2SmIBUG5QcTCyJA5Z8jGhlBPgRi4TmoDraVmKFJlWYrlp/g5QfwRlwEVNWVa4WzfH9jK6kCkEkBjwxOhLghheMWMELUAqqwQ4OCbnE4LJnhr5IYdqQt4DJQjhe9u4vBBmnxHHNzRFkDGjHDo0VuTAqy2vAG4NkvXXDHxbGsIGlj3e835VFNtdugma/Jk0eXq0lP//5svi4PtO01oFfYAAAAASUVORK5CYII=");
+}
+.chrome-bootstrap .highlightable li {
+ position: relative;
+ padding: 2px 0;
+}
+.chrome-bootstrap .highlightable li:hover > a:not(.action),
+.chrome-bootstrap .highlightable li a:not(.action):focus {
+ background-color: #F0F6FE;
+ color: #555;
+}
+.chrome-bootstrap .highlightable li:hover > .action {
+ opacity: 0.7;
+}
+.chrome-bootstrap .highlightable li a {
+ padding: 5px;
+ display: block;
+ position: relative;
+ z-index: 0;
+ text-decoration: none;
+}
+.chrome-bootstrap .highlightable li dt {
+ font-size: 105%;
+ margin-bottom: 3px;
+}
+.chrome-bootstrap .highlightable li dd {
+ color: #999;
+ overflow: hidden;
+ white-space: nowrap;
+ font-size: 10px;
+ margin-top: 5px;
+}
+.chrome-bootstrap .highlightable li .tags {
+ float: left;
+ margin-top: -1px;
+ font-size: 12px;
+}
+.chrome-bootstrap .highlightable li .tags li:last-child {
+ margin-right: 5px;
+}
+.chrome-bootstrap .highlightable li .tags li:hover > a:not(.action) {
+ background: #8AAAED;
+ color: #FFF;
+}
+.chrome-bootstrap .highlightable li .tags li a {
+ padding: 0;
+}
+.chrome-bootstrap .highlightable li .action {
+ -webkit-appearance: none;
+ -webkit-transition: opacity 150ms;
+ background: #8AAAED;
+ border: none;
+ border-radius: 2px;
+ color: white;
+ opacity: 0;
+ margin-top: 0;
+ font-size: 10px;
+ padding: 1px 6px;
+ position: absolute;
+ top: 8px;
+ right: 32px;
+ -webkit-transition: 150ms opacity;
+ cursor: pointer;
+}
+.chrome-bootstrap .highlightable li .action:hover {
+ opacity: 1;
+}
+.chrome-bootstrap .highlightable li .highlightable {
+ -webkit-margin-start: 30px;
+}
+.chrome-bootstrap .highlightable.editable .delete {
+ position: absolute;
+ top: 7px;
+ right: 5px;
+}
+.chrome-bootstrap .highlightable.editable li:hover > .delete {
+ opacity: 1;
+}
+.chrome-bootstrap .highlightable.draggable .handle {
+ width: 8px;
+ height: 41px;
+ background-image: linear-gradient(to bottom, #c1c1c1 50%, rgba(255, 255, 255, 0) 0%);
+ background-position: center;
+ background-size: 100% 17%;
+ background-repeat: repeat-y;
+ visibility: hidden;
+ position: absolute;
+ top: 4px;
+ left: 2px;
+}
+.chrome-bootstrap .highlightable.draggable .handle:hover {
+ cursor: move;
+ cursor: -webkit-grab;
+ display: block;
+}
+.chrome-bootstrap .highlightable.draggable .handle:after {
+ margin-left: 3px;
+ width: 2px;
+ height: 41px;
+ background: #F0F6FE;
+ content: "";
+ display: block;
+}
+.chrome-bootstrap .highlightable.draggable li:hover .handle {
+ visibility: visible;
+ z-index: 1;
+}
+.chrome-bootstrap .highlightable.draggable li > .item {
+ padding-left: 20px;
+}
+.chrome-bootstrap .match {
+ background: #f2f37b;
+ display: inline-block;
+ margin: 0 1px;
+}
+.chrome-bootstrap select,
+.chrome-bootstrap input[type='checkbox'],
+.chrome-bootstrap input[type='radio'],
+.chrome-bootstrap input[type='button'],
+.chrome-bootstrap button {
+ -webkit-appearance: none;
+ -webkit-user-select: none;
+ background-image: -webkit-linear-gradient(#ededed, #ededed 38%, #dedede);
+ border: 1px solid rgba(0, 0, 0, 0.25);
+ border-radius: 2px;
+ box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08), inset 0 1px 2px rgba(255, 255, 255, 0.75);
+ color: #444;
+ font: inherit;
+ margin: 0 1px 0 0;
+ text-shadow: 0 1px 0 #F0F0F0;
+}
+.chrome-bootstrap button.small {
+ padding: 1px 5px 2px;
+ min-height: 1em;
+}
+.chrome-bootstrap input[type='checkbox']:checked::before {
+ -webkit-user-select: none;
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAYAAACprHcmAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9wDBhYcG79aGIsAAACbSURBVBjTjdFBCkFhFAXgj4fp24PBy0SZ2ICRXRgYGb2xlKzBSEo2YgsiKWVoZgFKMjD5X/2Ux6lb99bpnNO5lKMR5i8MsEQHkhJiEzlS9HCqfiFWMUIt3AfsC3KKLCL30Qr7HfM4Ro4h6rhiEqmusIMKuphGqo+ogSPGcbYLzh91vdkXSHDDBk+0gxussS3rNcMCs+D6E18/9gLPPhbDshfzLgAAAABJRU5ErkJggg==");
+ background-size: 100% 100%;
+ content: '';
+ display: block;
+ height: 100%;
+ width: 100%;
+}
+.chrome-bootstrap html[dir='rtl'] input[type='checkbox']:checked::before {
+ -webkit-transform: scaleX(-1);
+}
+.chrome-bootstrap input[type='radio']:checked::before {
+ background-color: #666;
+ border-radius: 100%;
+ bottom: 3px;
+ content: '';
+ display: block;
+ left: 3px;
+ position: absolute;
+ right: 3px;
+ top: 3px;
+}
+.chrome-bootstrap select {
+ -webkit-appearance: none;
+ -webkit-padding-end: 20px;
+ -webkit-padding-start: 6px;
+ /* OVERRIDE */
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAWklEQVQokWNgoAOIAuI0PDiKaJMSgYCZmfkbkPkfHYPEQfJEG/b//3+FBQsWLGRjY/uJbBCIDxIHyRNtGDYDyTYI3UA+Pr4vFBmEbODbt2+bKDYIyUBWYtQBAIRzRP/XKJ//AAAAAElFTkSuQmCC), -webkit-linear-gradient(#ededed, #ededed 38%, #dedede);
+ background-position: right center;
+ background-repeat: no-repeat;
+}
+.chrome-bootstrap select {
+ min-height: 2em;
+ min-width: 4em;
+}
+.chrome-bootstrap html[dir='rtl'] select {
+ background-position: center left;
+}
+.chrome-bootstrap input[type='checkbox'] {
+ bottom: 2px;
+ height: 13px;
+ position: relative;
+ vertical-align: middle;
+ width: 13px;
+}
+.chrome-bootstrap input[type='radio'] {
+ /* OVERRIDE */
+ border-radius: 100%;
+ bottom: 3px;
+ height: 15px;
+ position: relative;
+ vertical-align: middle;
+ width: 15px;
+}
+.chrome-bootstrap button {
+ -webkit-padding-end: 10px;
+ -webkit-padding-start: 10px;
+ min-height: 2em;
+ min-width: 4em;
+}
+.chrome-bootstrap input[type='text'],
+.chrome-bootstrap input[type='number'],
+.chrome-bootstrap input[type='search'] {
+ border: 1px solid #BFBFBF;
+ border-radius: 2px;
+ box-sizing: border-box;
+ color: #444;
+ font: inherit;
+ margin: 0;
+ min-height: 2em;
+ padding: 3px;
+ padding-bottom: 4px;
+}
+.chrome-bootstrap .radio,
+.chrome-bootstrap .checkbox {
+ margin: 0.65em 0;
+}
+.chrome-bootstrap select:focus,
+.chrome-bootstrap input[type='checkbox']:focus,
+.chrome-bootstrap input[type='password']:focus,
+.chrome-bootstrap input[type='radio']:focus,
+.chrome-bootstrap input[type='search']:focus,
+.chrome-bootstrap input[type='text']:focus,
+.chrome-bootstrap input[type='number']:focus,
+.chrome-bootstrap button:focus {
+ /* OVERRIDE */
+ -webkit-transition: border-color 200ms;
+ /* We use border color because it follows the border radius (unlike outline).
+ * This is particularly noticeable on mac. */
+ border-color: #4d90fe;
+ outline: none;
+}
+.chrome-bootstrap button:disabled,
+.chrome-bootstrap select:disabled {
+ background-image: -webkit-linear-gradient(#f1f1f1, #f1f1f1 38%, #e6e6e6);
+ border-color: rgba(80, 80, 80, 0.2);
+ box-shadow: 0 1px 0 rgba(80, 80, 80, 0.08), inset 0 1px 2px rgba(255, 255, 255, 0.75);
+ color: #aaa;
+ cursor: default;
+}
+.chrome-bootstrap select:disabled {
+ /* OVERRIDE */
+ background-image: -webkit-image-set(url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAAXNSR0IArs4c6QAAAAd0SU1FB9sLAxYEBKriBmwAAAAGYktHRAD/AP8A/6C9p5MAAAAJcEhZcwAACxMAAAsTAQCanBgAAABLSURBVCiRY2CgA4gC4jQ8OIpokxKBoKGh4T8uDJIn2rD///8rLFiwYCE2g0DiIHkSfIndQLIMwmYgRQYhG/j27dsmig1CMpCVGHUAo8FcsHfxfXQAAAAASUVORK5CYII=") 1x), -webkit-linear-gradient(#f1f1f1, #f1f1f1 38%, #e6e6e6);
+}
+.chrome-bootstrap input[type='checkbox']:disabled,
+.chrome-bootstrap input[type='radio']:disabled {
+ opacity: .75;
+}
+.chrome-bootstrap input[type='search']:disabled,
+.chrome-bootstrap input[type='number']:disabled,
+.chrome-bootstrap input[type='text']:disabled {
+ color: #999;
+}
+.chrome-bootstrap select:hover:enabled,
+.chrome-bootstrap input[type='checkbox']:hover:enabled,
+.chrome-bootstrap input[type='radio']:hover:enabled,
+.chrome-bootstrap button:hover:enabled {
+ background-image: -webkit-linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0);
+ border-color: rgba(0, 0, 0, 0.3);
+ box-shadow: 0 1px 0 rgba(0, 0, 0, 0.12), inset 0 1px 2px rgba(255, 255, 255, 0.95);
+ color: black;
+}
+.chrome-bootstrap select:hover:enabled {
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAWklEQVQokWNgoAOIAuI0PDiKaJMSgYCZmfkbkPkfHYPEQfJEG/b//3+FBQsWLGRjY/uJbBCIDxIHyRNtGDYDyTYI3UA+Pr4vFBmEbODbt2+bKDYIyUBWYtQBAIRzRP/XKJ//AAAAAElFTkSuQmCC"), -webkit-linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0);
+}
+.chrome-bootstrap select:active:enabled,
+.chrome-bootstrap input[type='checkbox']:active:enabled,
+.chrome-bootstrap input[type='radio']:active:enabled,
+.chrome-bootstrap button:active:enabled {
+ background-image: -webkit-linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7);
+ box-shadow: none;
+ text-shadow: none;
+}
+.chrome-bootstrap select:active:enabled {
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAWklEQVQokWNgoAOIAuI0PDiKaJMSgYCZmfkbkPkfHYPEQfJEG/b//3+FBQsWLGRjY/uJbBCIDxIHyRNtGDYDyTYI3UA+Pr4vFBmEbODbt2+bKDYIyUBWYtQBAIRzRP/XKJ//AAAAAElFTkSuQmCC"), -webkit-linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7);
+}
+.chrome-bootstrap .overlay {
+ -webkit-box-align: center;
+ -webkit-box-orient: vertical;
+ -webkit-box-pack: center;
+ -webkit-transition: opacity .2s;
+ background-color: rgba(255, 255, 255, 0.75);
+ bottom: 0;
+ display: -webkit-box;
+ left: 0;
+ overflow: auto;
+ padding: 20px;
+ position: fixed;
+ right: 0;
+ top: 0;
+ z-index: 5;
+ opacity: 1;
+}
+.chrome-bootstrap .overlay.transparent {
+ opacity: 0;
+}
+.chrome-bootstrap .overlay.transparent .page {
+ -webkit-transform: scale(0.99) translateY(-20px);
+}
+.chrome-bootstrap .overlay .page {
+ -webkit-border-radius: 3px;
+ -webkit-box-orient: vertical;
+ -webkit-transition: 200ms -webkit-transform;
+ background: white;
+ box-shadow: 0 4px 23px 5px rgba(0, 0, 0, 0.2), 0 2px 6px rgba(0, 0, 0, 0.15);
+ color: #333;
+ display: -webkit-box;
+ min-width: 400px;
+ padding: 0;
+ position: relative;
+ overflow: hidden;
+}
+@-webkit-keyframes pulse {
+ 0% {
+ -webkit-transform: scale(1);
+ }
+ 40% {
+ -webkit-transform: scale(1.02);
+ }
+ 60% {
+ -webkit-transform: scale(1.02);
+ }
+ 100% {
+ -webkit-transform: scale(1);
+ }
+}
+.chrome-bootstrap .overlay .page.pulse {
+ -webkit-animation-duration: 180ms;
+ -webkit-animation-iteration-count: 1;
+ -webkit-animation-name: pulse;
+ -webkit-animation-timing-function: ease-in-out;
+}
+.chrome-bootstrap .overlay .page h1 {
+ -webkit-padding-end: 24px;
+ -webkit-user-select: none;
+ color: #333;
+ font-size: 120%;
+ margin: 0;
+ padding: 14px 17px 14px;
+ text-shadow: white 0 1px 2px;
+}
+.chrome-bootstrap .overlay .page ul li {
+ padding: 5px 0;
+}
+.chrome-bootstrap .overlay .page ul.tags li {
+ padding: 2px 5px;
+}
+.chrome-bootstrap .overlay .page .content-area {
+ -webkit-box-flex: 1;
+ overflow: auto;
+ padding: 6px 17px 6px;
+}
+.chrome-bootstrap .overlay .page .close-button {
+ background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAQAAAC1QeVaAAAAUklEQVR4XqXPYQrAIAhAYW/gXd8NJxTopVqsGEhtf+L9/ERU2k/HSMFQpKcYJeNFI9Be0LCMij8cYyjj5EHIivGBkwLfrbX3IF8PqumVmnDpEG+eDsKibPG2JwAAAABJRU5ErkJggg==');
+ background-position: center;
+ background-repeat: no-repeat;
+ height: 14px;
+ position: absolute;
+ right: 7px;
+ top: 7px;
+ width: 14px;
+}
+.chrome-bootstrap .overlay .page .close-button:hover {
+ background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAQAAAC1QeVaAAAAnUlEQVR4XoWQQQ6CQAxFewjkJkMCyXgJPMk7AiYczyBeZEAX6AKctGIaN+bt+trk9wtGQc/IkhnoKGxqqiWxOSZalapWFZ6VrIUDExsN0a5JRBq9LoVOR0eEQMoEhKizXhhsn0p1sCWVo7CwOf1RytPL8CPvwuBUoHL6ugeK30CVD1TqK7V/hdpe+VNChhOzV8xWny/+xosHF8578W/Hmc1OOC3wmwAAAABJRU5ErkJggg==');
+}
+.chrome-bootstrap .overlay .page .action-area {
+ -webkit-box-align: center;
+ -webkit-box-orient: horizontal;
+ -webkit-box-pack: end;
+ display: -webkit-box;
+ padding: 14px 17px;
+}
+.chrome-bootstrap .overlay .page .action-area-right {
+ display: -webkit-box;
+}
+.chrome-bootstrap .overlay .page .button-strip {
+ -webkit-box-orient: horizontal;
+ display: -webkit-box;
+}
+.chrome-bootstrap .overlay .page .button-strip button {
+ -webkit-margin-start: 10px;
+ display: block;
+}
diff --git a/build/Chrome/manifest.json b/build/Chrome/manifest.json
new file mode 100644
index 0000000..0bba4cd
--- /dev/null
+++ b/build/Chrome/manifest.json
@@ -0,0 +1,38 @@
+{
+ "name": "BabelExt",
+ "author": "honestbleeps",
+ "version": "0.95",
+ "manifest_version": 2,
+ "description": "An extension created with BabelExt - www.babelext.com",
+ "background": {
+ "scripts": [
+ "preferences.js",
+ "background.js"
+ ]
+ },
+ "content_scripts": [
+ {
+ "matches": [
+ "*://babelext.com/*"
+ ],
+ "js": [
+ "lib/BabelExt.js",
+ "src/extension.js"
+ ],
+ "run_at": "document_end",
+ "css": [
+ "src/extension.css"
+ ]
+ }
+ ],
+ "icons": {},
+ "permissions": [
+ "*://babelext.com/*",
+ "contextMenus",
+ "tabs",
+ "history",
+ "notifications",
+ "storage"
+ ],
+ "options_page": "options.html"
+}
diff --git a/build/Chrome/options.js b/build/Chrome/options.js
new file mode 100644
index 0000000..0d4e53a
--- /dev/null
+++ b/build/Chrome/options.js
@@ -0,0 +1,51 @@
+// based on https://developer.chrome.com/extensions/options
+
+function get_preferences() {
+ var preferences = {};
+ [].slice.call(document.querySelectorAll('.pref')).forEach(function(element) {
+ switch ( element.nodeName ) {
+ case 'INPUT':
+ switch ( element.type ) {
+ case 'checkbox':
+ if ( element.checked ) {
+ preferences[element.id] =
+ element.hasAttribute('data-on') ? parseInt(element.getAttribute('data-on'),10) : true;
+ } else {
+ preferences[element.id] =
+ element.hasAttribute('data-off') ? parseInt(element.getAttribute('data-off'),10) : false;
+ }
+ break;
+ case 'radio' : if ( element.checked ) preferences[element.name] = element.value; break;
+ case 'number': preferences[element.id] = parseInt(element.value,10); break;
+ case 'text' : preferences[element.id] = element.value; break;
+ }
+ break;
+ case 'SELECT': preferences[element.id] = element.value; break;
+ }
+ });
+ return preferences;
+}
+
+document.addEventListener('DOMContentLoaded', function() {
+ chrome.storage.local.get(get_preferences(), function(preferences) {
+ [].slice.call(document.querySelectorAll('.pref')).forEach(function(element) {
+ switch ( element.nodeName ) {
+ case 'INPUT':
+ switch ( element.type ) {
+ case 'checkbox':
+ element.checked =
+ preferences[element.id] == ( element.hasAttribute('data-on') ? element.getAttribute('data-on') : true );
+ break;
+ case 'radio' : element.checked = preferences[element.name] == element.value; break;
+ case 'number': element.value = preferences[element.id]; break;
+ case 'text' : element.value = preferences[element.id]; break;
+ }
+ break;
+ case 'SELECT': element.value = preferences[element.id]; break;
+ }
+ });
+ });
+});
+
+document.addEventListener('click', function() { chrome.storage.local.set(get_preferences(), function() {}); });
+document.addEventListener('input', function() { chrome.storage.local.set(get_preferences(), function() {}); });
diff --git a/build/Firefox/lib/main.js b/build/Firefox/lib/main.js
new file mode 100644
index 0000000..5311b31
--- /dev/null
+++ b/build/Firefox/lib/main.js
@@ -0,0 +1,243 @@
+// Import the APIs we need.
+
+var pageMod = require("sdk/page-mod");
+var XMLHttpRequest = require("sdk/net/xhr").XMLHttpRequest;
+var notifications = require("sdk/notifications");
+var self = require("sdk/self");
+var tabs = require("sdk/tabs");
+var ss = require("sdk/simple-storage");
+var workers = [];
+var contextMenu = require("sdk/context-menu");
+var priv = require("sdk/private-browsing");
+var windows = require("sdk/windows").browserWindows;
+var prefs = require("sdk/simple-prefs").prefs;
+
+// require chrome allows us to use XPCOM objects...
+const {Cc, Ci, Cu, Cr} = require("chrome");
+
+var historyService = Cc["@mozilla.org/browser/history;1"].getService(Ci.mozIAsyncHistory);
+
+// this function takes in a string (and optional charset, paseURI) and creates an nsURI object, which is required by historyService.addURI...
+function makeURI(aURL, aOriginCharset, aBaseURI) {
+ var ioService = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+ return ioService.newURI(aURL, aOriginCharset, aBaseURI);
+}
+
+
+function detachWorker(worker, workerArray) {
+ var index = workerArray.indexOf(worker);
+ if(index != -1) {
+ workerArray.splice(index, 1);
+ }
+}
+var localStorage = ss.storage;
+
+// these aliases are just for simplicity, so that the code here looks just like background code
+// for all of the other browsers...
+localStorage. getItem = function(key ) { return ss.storage[key] };
+localStorage. setItem = function(key, value) { ss.storage[key] = value };
+localStorage.removeItem = function(key ) { delete ss.storage[key] };
+
+var memoryStorage = { storage: {} };
+memoryStorage. getItem = function(key ) { return memoryStorage.storage[key] };
+memoryStorage. setItem = function(key, value) { memoryStorage.storage[key] = value };
+memoryStorage.removeItem = function(key ) { delete memoryStorage.storage[key] };
+
+var settings = require("./settings.js");
+
+pageMod.PageMod({
+ include: settings.include,
+ contentScriptWhen: settings.contentScriptWhen,
+ contentScriptFile: settings.contentScriptFile.map(function(file) { return self.data.url(file) }),
+ contentStyleFile: settings.contentStyleFile.map(function(file) { return self.data.url(file) }),
+ onAttach: function(worker) {
+ tabs.on('activate', function(tab) {
+ // run some code when a tab is activated...
+ });
+
+ workers.push(worker);
+ worker.on('detach', function () {
+ detachWorker(this, workers);
+ // console.log('worker detached, total now: ' + workers.length);
+ });
+
+ worker.on('message', function(data) {
+ var request = data;
+ switch(request.requestType) {
+ case 'xmlhttpRequest':
+ var responseObj = {
+ callbackID: request.callbackID,
+ name: request.requestType
+ };
+ var xhr = new XMLHttpRequest();
+ xhr.open(request.method, request.url, true, request.user, request.password);
+ if (request.method === "POST") {
+ xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
+ }
+ Object.keys(request.headers).forEach(function(header) { xhr.setRequestHeader(header, request.headers[header]) });
+ if ( typeof(request.overrideMimeType) != 'undefined' ) xhr.overrideMimeType = request.overrideMimeType;
+ xhr.onload = function() {
+ responseObj.response = {status: xhr.status, statusText: xhr.statusText, responseText: xhr.responseText, _response_headers: xhr.getAllResponseHeaders()};
+ worker.postMessage(responseObj);
+ }
+ xhr.onerror = function() {
+ responseObj.response = {status: xhr.status, statusText: xhr.statusText, responseText: xhr.responseText, _response_headers: xhr.getAllResponseHeaders(), error: true};
+ worker.postMessage(responseObj);
+ }
+ xhr.send(request.data);
+ break;
+ case 'createTab':
+ var focus = (request.background !== true);
+ tabs.open({url: request.url, inBackground: !focus });
+ worker.postMessage({status: "success"});
+ break;
+ case 'createNotification':
+ if (!request.icon) {
+ // if no icon specified, make a single pixel empty gif so we don't get a broken image link.
+ request.icon = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==';
+ }
+ notifications.notify({
+ title: request.title,
+ text: request.text,
+ iconURL: request.icon
+ });
+ break;
+ case 'localStorage':
+ switch (request.operation) {
+ case 'getItem':
+ worker.postMessage({
+ name: 'localStorage',
+ callbackID: request.callbackID,
+ status: true,
+ key: request.itemName,
+ value: localStorage.getItem(request.itemName)
+ });
+ break;
+ case 'removeItem':
+ localStorage.removeItem(request.itemName);
+ worker.postMessage({
+ name: 'localStorage',
+ callbackID: request.callbackID,
+ status: true,
+ value: null
+ });
+ break;
+ case 'setItem':
+ localStorage.setItem(request.itemName, request.itemValue);
+ worker.postMessage({
+ name: 'localStorage',
+ callbackID: request.callbackID,
+ status: true,
+ key: request.itemName,
+ value: request.itemValue
+ });
+ break;
+ }
+ break;
+ case 'memoryStorage':
+ switch (request.operation) {
+ case 'getItem':
+ worker.postMessage({
+ name: 'memoryStorage',
+ callbackID: request.callbackID,
+ status: true,
+ key: request.itemName,
+ value: memoryStorage.getItem(request.itemName)
+ });
+ break;
+ case 'removeItem':
+ memoryStorage.removeItem(request.itemName);
+ worker.postMessage({
+ name: 'memoryStorage',
+ callbackID: request.callbackID,
+ status: true,
+ value: null
+ });
+ break;
+ case 'setItem':
+ memoryStorage.setItem(request.itemName, request.itemValue);
+ worker.postMessage({
+ name: 'memoryStorage',
+ callbackID: request.callbackID,
+ status: true,
+ key: request.itemName,
+ value: request.itemValue
+ });
+ break;
+ }
+ break;
+ case 'preferences':
+ switch (request.operation) {
+ case 'getItem':
+ worker.postMessage({
+ name: 'preferences',
+ callbackID: request.callbackID,
+ status: true,
+ key: request.itemName,
+ value: prefs[request.itemName]
+ });
+ break;
+ case 'setItem':
+ prefs[request.itemName] = request.itemValue;
+ worker.postMessage({
+ name: 'preferences',
+ callbackID: request.callbackID,
+ status: true,
+ key: request.itemName,
+ value: request.itemValue
+ });
+ break;
+ }
+ break;
+ case 'addURLToHistory':
+ var isPrivate = priv.isPrivate(windows.activeWindow);
+ if (isPrivate) {
+ // do not add to history if in private browsing mode!
+ return false;
+ }
+ var uri = makeURI(request.url);
+ historyService.updatePlaces({
+ uri: uri,
+ visits: [{
+ transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
+ visitDate: Date.now() * 1000
+ }]
+ });
+ break;
+ case 'contextMenus.create':
+ contextMenu.Item({
+ label: request.obj.title,
+ context: contextMenu.PageContext(),
+ data: request.obj.onclick,
+ contentScript: 'self.on("click", function (node, data) {' +
+ 'self.postMessage(data);' +
+ '});',
+ onMessage: function(onclick) {
+ worker.postMessage({
+ name: 'contextMenus.click',
+ callbackID: onclick
+ });
+ }
+
+ });
+ break;
+ case 'contextMenus.remove':
+ // Run through the current context items and destroy the one with a matching name
+ contextItems = contextMenu.contentContextMenu.items;
+ var len = contextItems.length;
+ for(var i =0; i < len; ++i){
+ if(request.obj.title == contextItems[i].label){
+ contextMenu.contentContextMenu.destroy(contextItems[i]);
+ break;
+ }
+ }
+
+ break;
+ default:
+ worker.postMessage({status: "unrecognized request type"});
+ break;
+ }
+
+ });
+ }
+});
diff --git a/build/Firefox/lib/settings.js b/build/Firefox/lib/settings.js
new file mode 100644
index 0000000..ab3075f
--- /dev/null
+++ b/build/Firefox/lib/settings.js
@@ -0,0 +1,4 @@
+exports.include = ["http://babelext.com/*","https://babelext.com/*"];
+exports.contentScriptWhen = "ready";
+exports.contentScriptFile = ["lib/BabelExt.js","src/extension.js"];
+exports.contentStyleFile = ["src/extension.css"];
diff --git a/build/Firefox/package.json b/build/Firefox/package.json
new file mode 100644
index 0000000..94eee61
--- /dev/null
+++ b/build/Firefox/package.json
@@ -0,0 +1,81 @@
+{
+ "description": "An extension created with BabelExt - www.babelext.com",
+ "license": "GPL",
+ "author": "honestbleeps",
+ "version": "0.95",
+ "title": "BabelExt",
+ "id": "abcdef01-2345-6789-9876-543210fedcba",
+ "name": "babelext_your_name_here",
+ "preferences": [
+ {
+ "name": "myBool",
+ "type": "bool",
+ "title": "myBool preference title",
+ "description": "myBool short description for the preference",
+ "value": false
+ },
+ {
+ "name": "myBoolint",
+ "type": "boolint",
+ "title": "myBoolint preference title",
+ "description": "myBoolint short description for the preference",
+ "off": "1",
+ "on": "2",
+ "value": 1
+ },
+ {
+ "name": "myInteger",
+ "type": "integer",
+ "title": "myInteger preference title",
+ "description": "myInteger short description for the preference",
+ "value": 2
+ },
+ {
+ "name": "myString",
+ "type": "string",
+ "title": "myString preference title",
+ "description": "myString short description for the preference",
+ "value": "this is the default string value"
+ },
+ {
+ "name": "myMenulist",
+ "type": "menulist",
+ "title": "myMenulist preference title",
+ "value": 0,
+ "options": [
+ {
+ "value": "0",
+ "label": "first label"
+ },
+ {
+ "value": "1",
+ "label": "second label"
+ },
+ {
+ "value": "2",
+ "label": "third label"
+ }
+ ]
+ },
+ {
+ "name": "myRadio",
+ "type": "radio",
+ "title": "myRadioTitle",
+ "value": "a",
+ "options": [
+ {
+ "value": "a",
+ "label": "first label"
+ },
+ {
+ "value": "b",
+ "label": "second label"
+ },
+ {
+ "value": "c",
+ "label": "third label"
+ }
+ ]
+ }
+ ]
+}
diff --git a/build/Firefox/test/test-main.js b/build/Firefox/test/test-main.js
new file mode 100644
index 0000000..147f98a
--- /dev/null
+++ b/build/Firefox/test/test-main.js
@@ -0,0 +1,12 @@
+var main = require("./main");
+
+exports["test main"] = function(assert) {
+ assert.pass("Unit test running!");
+};
+
+exports["test main async"] = function(assert, done) {
+ assert.pass("async Unit test running!");
+ done();
+};
+
+require("sdk/test").run(exports);
diff --git a/build/README.txt b/build/README.txt
new file mode 100644
index 0000000..f8a2c04
--- /dev/null
+++ b/build/README.txt
@@ -0,0 +1,3 @@
+Each web browser wants you to build your extension in a slightly different way.
+
+The build script will automatically build browser-specific extensions from the values you provide.
diff --git a/Safari.safariextension/Info.plist b/build/Safari.safariextension/Info.plist
similarity index 82%
rename from Safari.safariextension/Info.plist
rename to build/Safari.safariextension/Info.plist
index 38cc1c4..594e6b9 100644
--- a/Safari.safariextension/Info.plist
+++ b/build/Safari.safariextension/Info.plist
@@ -3,13 +3,13 @@
Author
- Steve Sobel
+ honestbleeps
Builder Version
- 8536.30.1
+ 8537.85.12.18
CFBundleDisplayName
- BabelExt Extension
+ BabelExt
CFBundleIdentifier
- com.honestbleeps.babelext-extension
+ com.honestbleeps.abcdefghijklmnopqabcdefghijklmnopqabcdefghij
CFBundleInfoDictionaryVersion
6.0
CFBundleShortVersionString
@@ -35,7 +35,7 @@
Command
-
+
Identifier
notificationButton
Image
@@ -57,16 +57,22 @@
End
- extension.js
+ src/extension.js
Start
- BabelExt.js
+ lib/BabelExt.js
+ Stylesheets
+
+ src/extension.css
+
Description
An extension created with BabelExt - www.babelext.com
+ DeveloperIdentifier
+ ABCDEFGHIJ
ExtensionInfoDictionaryVersion
1.0
Permissions
diff --git a/Safari.safariextension/background.html b/build/Safari.safariextension/background.html
similarity index 63%
rename from Safari.safariextension/background.html
rename to build/Safari.safariextension/background.html
index b31ccf8..e29ae03 100644
--- a/Safari.safariextension/background.html
+++ b/build/Safari.safariextension/background.html
@@ -1,6 +1,11 @@
-
-
\ No newline at end of file
+