Răsfoiți Sursa

feat: modularization

Gerald 9 ani în urmă
părinte
comite
e0884d06d5
45 a modificat fișierele cu 2750 adăugiri și 2569 ștergeri
  1. 13 4
      .eslintrc.yml
  2. 12 19
      gulpfile.js
  3. 1 1
      package.json
  4. 1 1
      scripts/i18n.js
  5. 12 5
      scripts/templateCache.js
  6. 247 235
      src/background/app.js
  7. 571 564
      src/background/db.js
  8. 28 24
      src/background/events.js
  9. 1 0
      src/background/index.html
  10. 17 13
      src/background/requests.js
  11. 7 4
      src/background/sync/dropbox.js
  12. 16 12
      src/background/sync/index.js
  13. 7 4
      src/background/sync/onedrive.js
  14. 0 241
      src/background/utils.js
  15. 26 0
      src/background/utils/cache.js
  16. 97 0
      src/background/utils/script.js
  17. 18 0
      src/background/utils/search.js
  18. 32 0
      src/background/utils/tabs.js
  19. 68 0
      src/background/utils/tester.js
  20. 64 63
      src/cache.js
  21. 1 1
      src/injected.js
  22. 55 55
      src/options/app.js
  23. 3 3
      src/options/editor.js
  24. 1 0
      src/options/index.html
  25. 51 47
      src/options/model.js
  26. 19 16
      src/options/views/confirm-options.js
  27. 191 186
      src/options/views/confirm.js
  28. 27 24
      src/options/views/edit-meta.js
  29. 114 106
      src/options/views/edit.js
  30. 23 17
      src/options/views/main.js
  31. 37 34
      src/options/views/message.js
  32. 257 253
      src/options/views/script.js
  33. 25 22
      src/options/views/sync-service.js
  34. 12 9
      src/options/views/tab-about.js
  35. 77 70
      src/options/views/tab-installed.js
  36. 256 251
      src/options/views/tab-settings.js
  37. 115 101
      src/popup/app.js
  38. 1 0
      src/popup/index.html
  39. 5 7
      src/popup/model.js
  40. 28 22
      src/popup/views/base.js
  41. 34 28
      src/popup/views/command.js
  42. 33 27
      src/popup/views/domain.js
  43. 35 30
      src/popup/views/item.js
  44. 75 70
      src/popup/views/menu.js
  45. 37 0
      src/public/lib/require-lite.js

+ 13 - 4
.eslintrc.yml

@@ -1,8 +1,4 @@
 rules:
-  no-undef:
-    - 0
-  no-unused-vars:
-    - 0
   comma-dangle:
     - 0
   no-console:
@@ -12,9 +8,22 @@ rules:
       - error
   no-empty:
     - 0
+  no-unused-vars:
+    - 2
+    - args: all
+      argsIgnorePattern: ^_
 
 env:
   browser: true
   es6: true
 
+globals:
+  define: true
+  _: true
+  $: true
+  chrome: true
+  zip: true
+  Backbone: true
+  CodeMirror: true
+
 extends: 'eslint:recommended'

+ 12 - 19
gulpfile.js

@@ -1,10 +1,10 @@
 const gulp = require('gulp');
 const concat = require('gulp-concat');
 const replace = require('gulp-replace');
+const footer = require('gulp-footer');
 const merge2 = require('merge2');
 const cssnano = require('gulp-cssnano');
 const gulpFilter = require('gulp-filter');
-const order = require('gulp-order');
 const eslint = require('gulp-eslint');
 const uglify = require('gulp-uglify');
 const svgSprite = require('gulp-svg-sprite');
@@ -55,8 +55,8 @@ gulp.task('eslint', () => (
 
 gulp.task('templates', () => {
   var stream = merge2([
-    gulp.src(paths.templates).pipe(templateCache()),
     gulp.src(paths.cache),
+    gulp.src(paths.templates).pipe(templateCache()),
   ])
   .pipe(concat('cache.js'));
   if (isProd) stream = stream.pipe(uglify());
@@ -65,33 +65,24 @@ gulp.task('templates', () => {
 
 gulp.task('js-bg', () => {
   var stream = gulp.src(paths.jsBg)
-  .pipe(order([
-    '**/utils.js',
-    '!**/app.js',
-  ]))
-  .pipe(concat('background/app.js'));
+  .pipe(concat('background/app.js'))
+  .pipe(footer(';define.use("app");'));
   if (isProd) stream = stream.pipe(uglify());
   return stream.pipe(gulp.dest('dist'));
 });
 
 gulp.task('js-options', () => {
   var stream = gulp.src(paths.jsOptions)
-  .pipe(order([
-    '**/tab-*.js',
-    '!**/app.js',
-  ]))
-  .pipe(concat('options/app.js'));
+  .pipe(concat('options/app.js'))
+  .pipe(footer(';define.use("app");'));
   if (isProd) stream = stream.pipe(uglify());
   return stream.pipe(gulp.dest('dist'));
 });
 
 gulp.task('js-popup', () => {
   var stream = gulp.src(paths.jsPopup)
-  .pipe(order([
-    '**/base.js',
-    '!**/app.js',
-  ]))
-  .pipe(concat('popup/app.js'));
+  .pipe(concat('popup/app.js'))
+  .pipe(footer(';define.use("app");'));
   if (isProd) stream = stream.pipe(uglify());
   return stream.pipe(gulp.dest('dist'))
 });
@@ -105,10 +96,12 @@ gulp.task('manifest', () => (
 gulp.task('copy-files', () => {
   const jsFilter = gulpFilter(['**/*.js'], {restore: true});
   const cssFilter = gulpFilter(['**/*.css'], {restore: true});
-  return gulp.src(paths.copy)
+  var stream = gulp.src(paths.copy);
+  if (isProd) stream = stream
   .pipe(jsFilter)
   .pipe(uglify())
-  .pipe(jsFilter.restore)
+  .pipe(jsFilter.restore);
+  stream = stream
   .pipe(cssFilter)
   .pipe(cssnano({
     zindex: false,

+ 1 - 1
package.json

@@ -19,7 +19,7 @@
     "gulp-cssnano": "^2.1.2",
     "gulp-eslint": "^2.0.0",
     "gulp-filter": "^4.0.0",
-    "gulp-order": "^1.1.1",
+    "gulp-footer": "^1.0.5",
     "gulp-replace": "^0.5.4",
     "gulp-svg-sprite": "^1.2.19",
     "gulp-uglify": "^1.5.3",

+ 1 - 1
scripts/i18n.js

@@ -140,7 +140,7 @@ Locales.prototype.touch = function (key) {
 function extract(options) {
   const keys = new Set();
   const patterns = {
-    js: ['_.i18n\\(([\'"])(\\w+)\\1', 2],
+    js: ['_\\.i18n\\(([\'"])(\\w+)\\1', 2],
     json: ['__MSG_(\\w+)__', 1],
     html: ['data-i18n=([\'"]?)(\\w+)\\1', 2],
   };

+ 12 - 5
scripts/templateCache.js

@@ -11,18 +11,25 @@ const minified = require('./minifyHtml');
 }*/
 
 module.exports = function templateCache() {
-  const contentTpl = '_.cache.put(<%= name %>, <%= content %>);\n';
-  let content = '/* Below are templates cached from `_.template` with love :) */\n\n';
+  const contentTpl = 'cache.put(<%= name %>, <%= content %>);\n';
+  const header = `/* Templates cached from \`_.template\` with love :) */
+define('templates', function (require, exports, module) {
+  var cache = require('cache');
+`;
+  const footer = `
+});
+`;
+  const contents = [];
 
   function bufferContents(file, enc, cb) {
     if (file.isNull()) return cb();
     if (file.isStream())
       return this.emit('error', new gutil.PluginError('VM-cache', 'Stream is not supported.'));
-    content += gutil.template(contentTpl, {
+    contents.push(gutil.template(contentTpl, {
       name: JSON.stringify(('/' + file.relative).replace(/\\/g, '/')),
       content: _.template(minified(file.contents), {variable: 'it'}).source,
       file: '',
-    });
+    }));
     cb();
   }
 
@@ -30,7 +37,7 @@ module.exports = function templateCache() {
     this.push(new gutil.File({
       base: '',
       path: 'template.js',
-      contents: new Buffer(content),
+      contents: new Buffer(header + contents.join('') + footer),
     }));
     cb();
   }

+ 247 - 235
src/background/app.js

@@ -1,256 +1,268 @@
-var vmdb = new VMDB;
-var VM_VER = chrome.app.getDetails().version;
-var commands = {
-  NewScript: function (data, src) {
-    return scriptUtils.newScript();
-  },
-  RemoveScript: function (id, src) {
-    return vmdb.removeScript(id)
-    .then(function () {
-      sync.sync();
-      _.messenger.post({
-        cmd: 'del',
-        data: id,
+define('app', function (require, exports, _module) {
+  var VMDB = require('vmdb');
+  var sync = require('sync');
+  var requests = require('requests');
+  var cache = require('utils/cache');
+  var tabsUtils = require('utils/tabs');
+  var scriptUtils = require('utils/script');
+
+  var vmdb = exports.vmdb = new VMDB;
+  var VM_VER = chrome.app.getDetails().version;
+
+  var autoUpdate = function () {
+    function check() {
+      checking = true;
+      return new Promise(function (resolve, reject) {
+        if (!_.options.get('autoUpdate')) return reject();
+        if (Date.now() - _.options.get('lastUpdate') >= 864e5)
+          resolve(commands.CheckUpdateAll());
+      }).then(function () {
+        setTimeout(check, 36e5);
+      }, function () {
+        checking = false;
       });
-    });
-  },
-  GetData: function (data, src) {
-    return vmdb.getData().then(function (data) {
-      data.sync = sync.states();
-      data.version = VM_VER;
-      return data;
-    });
-  },
-  GetInjected: function (url, src) {
-    var data = {
-      isApplied: _.options.get('isApplied'),
-      injectMode: _.options.get('injectMode'),
-      version: VM_VER,
+    }
+    var checking;
+    return function () {
+      checking || check();
     };
-    if (src.url == src.tab.url)
-      chrome.tabs.sendMessage(src.tab.id, {cmd: 'GetBadge'});
-    return data.isApplied
-    ? vmdb.getScriptsByURL(url).then(function (res) {
-      return _.assign(data, res);
-    }) : data;
-  },
-  UpdateScriptInfo: function (data, src) {
-    return vmdb.updateScriptInfo(data.id, data, {
-      modified: Date.now(),
-    })
-    .then(function (script) {
-      sync.sync();
-      _.messenger.post({
-        cmd: 'update',
-        data: script,
-      });
-    });
-  },
-  SetValue: function (data, src) {
-    return vmdb.setValue(data.uri, data.values)
-    .then(function () {
-      _.tabs.broadcast({
-        cmd: 'UpdateValues',
-        data: {
-          uri: data.uri,
-          values: data.values,
-        },
-      });
-    });
-  },
-  ExportZip: function (data, src) {
-    return vmdb.getExportData(data.ids, data.values);
-  },
-  GetScript: function (id, src) {
-    return vmdb.getScriptData(id);
-  },
-  GetMetas: function (ids, src) {
-    return vmdb.getScriptInfos(ids);
-  },
-  Move: function (data, src) {
-    return vmdb.moveScript(data.id, data.offset);
-  },
-  Vacuum: function (data, src) {
-    return vmdb.vacuum();
-  },
-  ParseScript: function (data, src) {
-    return vmdb.parseScript(data).then(function (res) {
-      var meta = res.data.meta;
-      if (!meta.grant.length && !_.options.get('ignoreGrant'))
-        notify({
-          id: 'VM-NoGrantWarning',
-          title: _.i18n('Warning'),
-          body: _.i18n('msgWarnGrant', [meta.name||_.i18n('labelNoName')]),
-          isClickable: true,
+  }();
+  var commands = {
+    NewScript: function (_data, _src) {
+      return scriptUtils.newScript();
+    },
+    RemoveScript: function (id, _src) {
+      return vmdb.removeScript(id)
+        .then(function () {
+          sync.sync();
+          _.messenger.post({
+            cmd: 'del',
+            data: id,
+          });
         });
-      _.messenger.post(res);
-      sync.sync();
-      return res.data;
-    });
-  },
-  CheckUpdate: function (id, src) {
-    vmdb.getScript(id).then(vmdb.checkUpdate);
-    return false;
-  },
-  CheckUpdateAll: function (data, src) {
-    _.options.set('lastUpdate', Date.now());
-    vmdb.getScriptsByIndex('update', 1).then(function (scripts) {
-      return Promise.all(scripts.map(vmdb.checkUpdate));
-    });
-    return false;
-  },
-  ParseMeta: function (code, src) {
-    return scriptUtils.parseMeta(code);
-  },
-  AutoUpdate: autoUpdate,
-  GetRequestId: function (data, src) {
-    return requests.getRequestId();
-  },
-  HttpRequest: function (details, src) {
-    requests.httpRequest(details, function (res) {
-      _.messenger.send(src.tab.id, {
-        cmd: 'HttpRequested',
-        data: res,
+    },
+    GetData: function (_data, _src) {
+      return vmdb.getData().then(function (data) {
+        data.sync = sync.states();
+        data.version = VM_VER;
+        return data;
       });
-    });
-    return false;
-  },
-  AbortRequest: function (id, src) {
-    return requests.abortRequest(id);
-  },
-  SetBadge: function (num, src) {
-    setBadge(num, src);
-    return false;
-  },
-  Authenticate: function (data, src) {
-    var service = sync.service(data);
-    service && service.authenticate && service.authenticate();
-    return false;
-  },
-  SyncStart: function (data, src) {
-    sync.sync(data && sync.service(data));
-    return false;
-  },
-  GetFromCache: function (data, src) {
-    return _.cache.get(data) || null;
-  },
-};
-
-vmdb.initialized.then(function () {
-  chrome.runtime.onMessage.addListener(function (req, src, callback) {
-    var func = commands[req.cmd];
-    if (func) {
-      var res = func(req.data, src);
-      if (res === false) return;
-      var finish = function (data) {
-        try {
-          callback(data);
-        } catch (e) {
-          // callback fails if not given in content page
-        }
+    },
+    GetInjected: function (url, src) {
+      var data = {
+        isApplied: _.options.get('isApplied'),
+        injectMode: _.options.get('injectMode'),
+        version: VM_VER,
       };
-      Promise.resolve(res).then(function (data) {
-        finish({
-          data: data,
-          error: null,
+      if (src.url == src.tab.url)
+        chrome.tabs.sendMessage(src.tab.id, {cmd: 'GetBadge'});
+      return data.isApplied
+        ? vmdb.getScriptsByURL(url).then(function (res) {
+          return _.assign(data, res);
+        }) : data;
+    },
+    UpdateScriptInfo: function (data, _src) {
+      return vmdb.updateScriptInfo(data.id, data, {
+        modified: Date.now(),
+      })
+      .then(function (script) {
+        sync.sync();
+        _.messenger.post({
+          cmd: 'update',
+          data: script,
         });
-      }, function (data) {
-        finish({
-          error: data,
+      });
+    },
+    SetValue: function (data, _src) {
+      return vmdb.setValue(data.uri, data.values)
+        .then(function () {
+          tabsUtils.broadcast({
+            cmd: 'UpdateValues',
+            data: {
+              uri: data.uri,
+              values: data.values,
+            },
+          });
         });
+    },
+    ExportZip: function (data, _src) {
+      return vmdb.getExportData(data.ids, data.values);
+    },
+    GetScript: function (id, _src) {
+      return vmdb.getScriptData(id);
+    },
+    GetMetas: function (ids, _src) {
+      return vmdb.getScriptInfos(ids);
+    },
+    Move: function (data, _src) {
+      return vmdb.moveScript(data.id, data.offset);
+    },
+    Vacuum: function (_data, _src) {
+      return vmdb.vacuum();
+    },
+    ParseScript: function (data, _src) {
+      return vmdb.parseScript(data).then(function (res) {
+        var meta = res.data.meta;
+        if (!meta.grant.length && !_.options.get('ignoreGrant'))
+          notify({
+            id: 'VM-NoGrantWarning',
+            title: _.i18n('Warning'),
+            body: _.i18n('msgWarnGrant', [meta.name||_.i18n('labelNoName')]),
+            isClickable: true,
+          });
+          _.messenger.post(res);
+          sync.sync();
+          return res.data;
       });
-      return true;
-    }
-  });
-  setTimeout(autoUpdate, 2e4);
-  sync.init();
-});
-
-// Common functions
+    },
+    CheckUpdate: function (id, _src) {
+      vmdb.getScript(id).then(vmdb.checkUpdate);
+      return false;
+    },
+    CheckUpdateAll: function (_data, _src) {
+      _.options.set('lastUpdate', Date.now());
+      vmdb.getScriptsByIndex('update', 1).then(function (scripts) {
+        return Promise.all(scripts.map(vmdb.checkUpdate));
+      });
+      return false;
+    },
+    ParseMeta: function (code, _src) {
+      return scriptUtils.parseMeta(code);
+    },
+    AutoUpdate: autoUpdate,
+    GetRequestId: function (_data, _src) {
+      return requests.getRequestId();
+    },
+    HttpRequest: function (details, src) {
+      requests.httpRequest(details, function (res) {
+        _.messenger.send(src.tab.id, {
+          cmd: 'HttpRequested',
+          data: res,
+        });
+      });
+      return false;
+    },
+    AbortRequest: function (id, _src) {
+      return requests.abortRequest(id);
+    },
+    SetBadge: function (num, src) {
+      setBadge(num, src);
+      return false;
+    },
+    Authenticate: function (data, _src) {
+      var service = sync.service(data);
+      service && service.authenticate && service.authenticate();
+      return false;
+    },
+    SyncStart: function (data, _src) {
+      sync.sync(data && sync.service(data));
+      return false;
+    },
+    GetFromCache: function (data, _src) {
+      return cache.get(data) || null;
+    },
+  };
 
-function notify(options) {
-  chrome.notifications.create(options.id || 'ViolentMonkey', {
-    type: 'basic',
-    iconUrl: '/images/icon128.png',
-    title: options.title + ' - ' + _.i18n('extName'),
-    message: options.body,
-    isClickable: options.isClickable,
+  vmdb.initialized.then(function () {
+    chrome.runtime.onMessage.addListener(function (req, src, callback) {
+      var func = commands[req.cmd];
+      if (func) {
+        var res = func(req.data, src);
+        if (res === false) return;
+        var finish = function (data) {
+          try {
+            callback(data);
+          } catch (e) {
+            // callback fails if not given in content page
+          }
+        };
+        Promise.resolve(res).then(function (data) {
+          finish({
+            data: data,
+            error: null,
+          });
+        }, function (data) {
+          finish({
+            error: data,
+          });
+        });
+        return true;
+      }
+    });
+    setTimeout(autoUpdate, 2e4);
+    sync.init();
   });
-}
 
-var setBadge = function () {
-  var badges = {};
-  return function (num, src) {
-    var o = badges[src.id];
-    if (!o) o = badges[src.id] = {num: 0};
-    o.num += num;
-    chrome.browserAction.setBadgeBackgroundColor({
-      color: '#808',
-      tabId: src.tab.id,
-    });
-    chrome.browserAction.setBadgeText({
-      text: (o.num || '').toString(),
-      tabId: src.tab.id,
-    });
-    if (o.timer) clearTimeout(o.timer);
-    o.timer = setTimeout(function () {
-      delete badges[src.id];
-    }, 300);
-  };
-}();
+  // Common functions
 
-var autoUpdate = function () {
-  function check() {
-    checking = true;
-    return new Promise(function (resolve, reject) {
-      if (!_.options.get('autoUpdate')) return reject();
-      if (Date.now() - _.options.get('lastUpdate') >= 864e5)
-        resolve(commands.CheckUpdateAll());
-    }).then(function () {
-      setTimeout(check, 36e5);
-    }, function () {
-      checking = false;
+  function notify(options) {
+    chrome.notifications.create(options.id || 'ViolentMonkey', {
+      type: 'basic',
+      iconUrl: '/images/icon128.png',
+      title: options.title + ' - ' + _.i18n('extName'),
+      message: options.body,
+      isClickable: options.isClickable,
     });
   }
-  var checking;
-  return function () {
-    checking || check();
-  };
-}();
 
-_.messenger = function () {
-  var port;
-  chrome.runtime.onConnect.addListener(function (_port) {
-    port = _port;
-    _port.onDisconnect.addListener(function () {
-      if (port === _port) port = null;
+  var setBadge = function () {
+    var badges = {};
+    return function (num, src) {
+      var o = badges[src.id];
+      if (!o) o = badges[src.id] = {num: 0};
+      o.num += num;
+      chrome.browserAction.setBadgeBackgroundColor({
+        color: '#808',
+        tabId: src.tab.id,
+      });
+      chrome.browserAction.setBadgeText({
+        text: (o.num || '').toString(),
+        tabId: src.tab.id,
+      });
+      if (o.timer) clearTimeout(o.timer);
+      o.timer = setTimeout(function () {
+        delete badges[src.id];
+      }, 300);
+    };
+  }();
+
+  _.messenger = function () {
+    var port;
+    chrome.runtime.onConnect.addListener(function (_port) {
+      port = _port;
+      _port.onDisconnect.addListener(function () {
+        if (port === _port) port = null;
+      });
     });
-  });
 
-  return {
-    post: function (data) {
-      try {
-        port && port.postMessage(data);
-      } catch (e) {
-        port = null;
-      }
-    },
-    send: function (tabId, data) {
-      chrome.tabs.sendMessage(tabId, data);
-    },
-  };
-}();
+    return {
+      post: function (data) {
+        try {
+          port && port.postMessage(data);
+        } catch (e) {
+          port = null;
+        }
+      },
+      send: function (tabId, data) {
+        chrome.tabs.sendMessage(tabId, data);
+      },
+    };
+  }();
 
-!function (isApplied) {
-  chrome.browserAction.setIcon({
-    path: {
-      19: '/images/icon19' + (isApplied ? '' : 'w') + '.png',
-      38: '/images/icon38' + (isApplied ? '' : 'w') + '.png'
-    },
+  !function (isApplied) {
+    chrome.browserAction.setIcon({
+      path: {
+        19: '/images/icon19' + (isApplied ? '' : 'w') + '.png',
+        38: '/images/icon38' + (isApplied ? '' : 'w') + '.png'
+      },
+    });
+  }(_.options.get('isApplied'));
+
+  chrome.notifications.onClicked.addListener(function(id) {
+    if(id == 'VM-NoGrantWarning')
+      tabsUtils.create('http://wiki.greasespot.net/@grant');
   });
-}(_.options.get('isApplied'));
 
-chrome.notifications.onClicked.addListener(function(id) {
-  if(id == 'VM-NoGrantWarning')
-    _.tabs.create('http://wiki.greasespot.net/@grant');
+  require('sync_dropbox');
+  require('sync_onedrive');
 });

+ 571 - 564
src/background/db.js

@@ -1,639 +1,646 @@
-function VMDB() {
-  var _this = this;
-  _this.initialized = _this.openDB().then(_this.initPosition.bind(_this));
-  _this.checkUpdate = _this.checkUpdate.bind(_this);
-}
+define('vmdb', function (require, _exports, module) {
+  var scriptUtils = require('utils/script');
+  var tester = require('utils/tester');
 
-VMDB.prototype.openDB = function () {
-  var _this = this;
-  return new Promise(function (resolve, reject) {
-    var request = indexedDB.open('Violentmonkey', 1);
-    request.onsuccess = function (e) {
-      _this.db = request.result;
-      resolve();
-    };
-    request.onerror = function (e) {
-      var err = e.target.error;
-      console.error('IndexedDB error: ' + err.message);
-      reject(err);
-    };
-    request.onupgradeneeded = function (e) {
-      var r = e.currentTarget.result;
-      // scripts: id uri custom meta enabled update code position
-      var o = r.createObjectStore('scripts', {
-        keyPath: 'id',
-        autoIncrement: true,
-      });
-      o.createIndex('uri', 'uri', {unique: true});
-      o.createIndex('update', 'update', {unique: false});
-      // position should be unique at last
-      o.createIndex('position', 'position', {unique: false});
-      // require: uri code
-      o = r.createObjectStore('require', {keyPath: 'uri'});
-      // cache: uri data
-      o = r.createObjectStore('cache', {keyPath: 'uri'});
-      // values: uri values
-      o = r.createObjectStore('values', {keyPath: 'uri'});
-    };
-  });
-};
-
-VMDB.prototype.initPosition = function () {
-  var _this = this;
-  _this.position = 0;
-  var o = _this.db.transaction('scripts', 'readwrite').objectStore('scripts');
-  return new Promise(function (resolve, reject) {
-    o.index('position').openCursor(null, 'prev').onsuccess = function (e) {
-      var result = e.target.result;
-      if (result) _this.position = result.key;
-      resolve();
-    };
-  });
-};
-
-VMDB.prototype.getScript = function (id, tx) {
-  tx = tx || this.db.transaction('scripts');
-  var os = tx.objectStore('scripts');
-  return new Promise(function (resolve, reject) {
-    os.get(id).onsuccess = function (e) {
-      resolve(e.target.result);
-    };
-  });
-};
+  function VMDB() {
+    var _this = this;
+    _this.initialized = _this.openDB().then(_this.initPosition.bind(_this));
+    _this.checkUpdate = _this.checkUpdate.bind(_this);
+  }
 
-VMDB.prototype.queryScript = function (id, meta, tx) {
-  var _this = this;
-  return id
-    ? _this.getScript(id, tx)
-    : new Promise(function (resolve, reject) {
-      var uri = scriptUtils.getNameURI({meta: meta});
-      tx = tx || _this.db.transaction('scripts');
-      tx.objectStore('scripts')
-      .index('uri').get(uri).onsuccess = function (e) {
-        resolve(e.target.result);
+  VMDB.prototype.openDB = function () {
+    var _this = this;
+    return new Promise(function (resolve, reject) {
+      var request = indexedDB.open('Violentmonkey', 1);
+      request.onsuccess = function (_e) {
+        _this.db = request.result;
+        resolve();
+      };
+      request.onerror = function (e) {
+        var err = e.target.error;
+        console.error('IndexedDB error: ' + err.message);
+        reject(err);
+      };
+      request.onupgradeneeded = function (e) {
+        var r = e.currentTarget.result;
+        // scripts: id uri custom meta enabled update code position
+        var o = r.createObjectStore('scripts', {
+          keyPath: 'id',
+          autoIncrement: true,
+        });
+        o.createIndex('uri', 'uri', {unique: true});
+        o.createIndex('update', 'update', {unique: false});
+        // position should be unique at last
+        o.createIndex('position', 'position', {unique: false});
+        // require: uri code
+        o = r.createObjectStore('require', {keyPath: 'uri'});
+        // cache: uri data
+        o = r.createObjectStore('cache', {keyPath: 'uri'});
+        // values: uri values
+        o = r.createObjectStore('values', {keyPath: 'uri'});
       };
     });
-};
-
-VMDB.prototype.getScriptData = function (id) {
-  return this.getScript(id).then(function (script) {
-    if (!script) return Promise.reject();
-    var data = scriptUtils.getScriptInfo(script);
-    data.code = script.code;
-    return data;
-  });
-};
+  };
 
-VMDB.prototype.getScriptInfos = function (ids) {
-  var _this = this;
-  var tx = _this.db.transaction('scripts');
-  return Promise.all(ids.map(function (id) {
-    return _this.getScript(id, tx);
-  })).then(function (scripts) {
-    return scripts.filter(function (x) {return x;})
-    .map(scriptUtils.getScriptInfo);
-  });
-};
+  VMDB.prototype.initPosition = function () {
+    var _this = this;
+    _this.position = 0;
+    var o = _this.db.transaction('scripts', 'readwrite').objectStore('scripts');
+    return new Promise(function (resolve, _reject) {
+      o.index('position').openCursor(null, 'prev').onsuccess = function (e) {
+        var result = e.target.result;
+        if (result) _this.position = result.key;
+        resolve();
+      };
+    });
+  };
 
-VMDB.prototype.getValues = function (uris, tx) {
-  var _this = this;
-  tx = tx || _this.db.transaction('values');
-  var o = tx.objectStore('values');
-  return Promise.all(uris.map(function (uri) {
-    return new Promise(function (resolve, reject) {
-      o.get(uri).onsuccess = function (e) {
+  VMDB.prototype.getScript = function (id, tx) {
+    tx = tx || this.db.transaction('scripts');
+    var os = tx.objectStore('scripts');
+    return new Promise(function (resolve, _reject) {
+      os.get(id).onsuccess = function (e) {
         resolve(e.target.result);
       };
     });
-  })).then(function (data) {
-    return data.reduce(function (result, value, i) {
-      if (value) result[uris[i]] = value.values;
-      return result;
-    }, {});
-  });
-};
+  };
 
-VMDB.prototype.getScriptsByURL = function (url) {
-  function getScripts() {
-    return _this.getScriptsByIndex('position', null, tx).then(function (scripts) {
-      var data = {
-        uris: [],
-      };
-      var require = {};
-      var cache = {};
-      data.scripts = scripts.filter(function (script) {
-        if (tester.testURL(url, script)) {
-          data.uris.push(script.uri);
-          script.meta.require.forEach(function (key) {
-            require[key] = 1;
-          });
-          for (var k in script.meta.resources)
-            cache[script.meta.resources[k]] = 1;
-          return true;
-        }
+  VMDB.prototype.queryScript = function (id, meta, tx) {
+    var _this = this;
+    return id
+      ? _this.getScript(id, tx)
+      : new Promise(function (resolve, _reject) {
+        var uri = scriptUtils.getNameURI({meta: meta});
+        tx = tx || _this.db.transaction('scripts');
+        tx.objectStore('scripts')
+          .index('uri').get(uri).onsuccess = function (e) {
+            resolve(e.target.result);
+          };
       });
-      data.require = Object.keys(require);
-      data.cache = Object.keys(cache);
+  };
+
+  VMDB.prototype.getScriptData = function (id) {
+    return this.getScript(id).then(function (script) {
+      if (!script) return Promise.reject();
+      var data = scriptUtils.getScriptInfo(script);
+      data.code = script.code;
       return data;
     });
-  }
-  function getRequire(uris) {
-    var o = tx.objectStore('require');
+  };
+
+  VMDB.prototype.getScriptInfos = function (ids) {
+    var _this = this;
+    var tx = _this.db.transaction('scripts');
+    return Promise.all(ids.map(function (id) {
+      return _this.getScript(id, tx);
+    })).then(function (scripts) {
+      return scripts.filter(function (x) {return x;})
+        .map(scriptUtils.getScriptInfo);
+    });
+  };
+
+  VMDB.prototype.getValues = function (uris, tx) {
+    var _this = this;
+    tx = tx || _this.db.transaction('values');
+    var o = tx.objectStore('values');
     return Promise.all(uris.map(function (uri) {
-      return new Promise(function (resolve, reject) {
+      return new Promise(function (resolve, _reject) {
         o.get(uri).onsuccess = function (e) {
           resolve(e.target.result);
         };
       });
     })).then(function (data) {
       return data.reduce(function (result, value, i) {
-        if (value) result[uris[i]] = value.code;
+        if (value) result[uris[i]] = value.values;
         return result;
       }, {});
     });
-  }
-  var _this = this;
-  var tx = _this.db.transaction(['scripts', 'require', 'values', 'cache']);
-  return getScripts().then(function (data) {
-    return Promise.all([
-      getRequire(data.require),
-      _this.getValues(data.uris, tx),
-      _this.getCacheB64(data.cache, tx),
-    ]).then(function (res) {
-      return {
-        scripts: data.scripts,
-        require: res[0],
-        values: res[1],
-        cache: res[2],
-      };
-    });
-  });
-};
+  };
 
-VMDB.prototype.getData = function () {
-  function getScripts() {
-    return _this.getScriptsByIndex('position', null, tx).then(function (scripts) {
-      var data = {};
-      var cache = {};
-      data.scripts = scripts.map(function (script) {
-        var icon = script.meta.icon;
-        if (scriptUtils.isRemote(icon)) cache[icon] = 1;
-        return scriptUtils.getScriptInfo(script);
+  VMDB.prototype.getScriptsByURL = function (url) {
+    function getScripts() {
+      return _this.getScriptsByIndex('position', null, tx).then(function (scripts) {
+        var data = {
+          uris: [],
+        };
+        var require = {};
+        var cache = {};
+        data.scripts = scripts.filter(function (script) {
+          if (tester.testURL(url, script)) {
+            data.uris.push(script.uri);
+            script.meta.require.forEach(function (key) {
+              require[key] = 1;
+            });
+            for (var k in script.meta.resources)
+              cache[script.meta.resources[k]] = 1;
+            return true;
+          }
+        });
+        data.require = Object.keys(require);
+        data.cache = Object.keys(cache);
+        return data;
+      });
+    }
+    function getRequire(uris) {
+      var o = tx.objectStore('require');
+      return Promise.all(uris.map(function (uri) {
+        return new Promise(function (resolve, _reject) {
+          o.get(uri).onsuccess = function (e) {
+            resolve(e.target.result);
+          };
+        });
+      })).then(function (data) {
+        return data.reduce(function (result, value, i) {
+          if (value) result[uris[i]] = value.code;
+          return result;
+        }, {});
+      });
+    }
+    var _this = this;
+    var tx = _this.db.transaction(['scripts', 'require', 'values', 'cache']);
+    return getScripts().then(function (data) {
+      return Promise.all([
+        getRequire(data.require),
+        _this.getValues(data.uris, tx),
+        _this.getCacheB64(data.cache, tx),
+      ]).then(function (res) {
+        return {
+          scripts: data.scripts,
+          require: res[0],
+          values: res[1],
+          cache: res[2],
+        };
       });
-      data.cache = Object.keys(cache);
-      return data;
     });
-  }
-  function getCache(uris) {
-    return _this.getCacheB64(uris, tx).then(function (cache) {
-      for (var k in cache)
-        cache[k] = 'data:image/png;base64,' + cache[k];
-      return cache;
+  };
+
+  VMDB.prototype.getData = function () {
+    function getScripts() {
+      return _this.getScriptsByIndex('position', null, tx).then(function (scripts) {
+        var data = {};
+        var cache = {};
+        data.scripts = scripts.map(function (script) {
+          var icon = script.meta.icon;
+          if (scriptUtils.isRemote(icon)) cache[icon] = 1;
+          return scriptUtils.getScriptInfo(script);
+        });
+        data.cache = Object.keys(cache);
+        return data;
+      });
+    }
+    function getCache(uris) {
+      return _this.getCacheB64(uris, tx).then(function (cache) {
+        for (var k in cache)
+          cache[k] = 'data:image/png;base64,' + cache[k];
+        return cache;
+      });
+    }
+    var _this = this;
+    var tx = _this.db.transaction(['scripts', 'cache']);
+    return getScripts().then(function (data) {
+      return getCache(data.cache).then(function (cache) {
+        return {
+          scripts: data.scripts,
+          cache: cache,
+        };
+      });
     });
-  }
-  var _this = this;
-  var tx = _this.db.transaction(['scripts', 'cache']);
-  return getScripts().then(function (data) {
-    return getCache(data.cache).then(function (cache) {
-      return {
-        scripts: data.scripts,
-        cache: cache,
+  };
+
+  VMDB.prototype.removeScript = function (id) {
+    var tx = this.db.transaction('scripts', 'readwrite');
+    return new Promise(function (resolve, _reject) {
+      var o = tx.objectStore('scripts');
+      o.delete(id).onsuccess = function () {
+        resolve();
       };
     });
-  });
-};
+  };
 
-VMDB.prototype.removeScript = function (id) {
-  var tx = this.db.transaction('scripts', 'readwrite');
-  return new Promise(function (resolve, reject) {
+  VMDB.prototype.moveScript = function (id, offset) {
+    var tx = this.db.transaction('scripts', 'readwrite');
     var o = tx.objectStore('scripts');
-    o.delete(id).onsuccess = function () {
-      resolve();
-    };
-  });
-};
-
-VMDB.prototype.moveScript = function (id, offset) {
-  var tx = this.db.transaction('scripts', 'readwrite');
-  var o = tx.objectStore('scripts');
-  return this.getScript(id, tx).then(function (script) {
-    var pos = script.position;
-    var range, order;
-    if (offset < 0) {
-      range = IDBKeyRange.upperBound(pos, true);
-      order = 'prev';
-      offset = -offset;
-    } else {
-      range = IDBKeyRange.lowerBound(pos, true);
-      order = 'next';
-    }
-    return new Promise(function (resolve, reject) {
-      o.index('position').openCursor(range, order).onsuccess = function (e) {
-        var result = e.target.result;
-        if (result) {
-          offset --;
-          var value = result.value;
-          value.position = pos;
-          pos = result.key;
-          result.update(value);
-          if (offset) result.continue();
-          else {
-            script.position = pos;
-            o.put(script).onsuccess = function () {
-              resolve();
-            };
+    return this.getScript(id, tx).then(function (script) {
+      var pos = script.position;
+      var range, order;
+      if (offset < 0) {
+        range = IDBKeyRange.upperBound(pos, true);
+        order = 'prev';
+        offset = -offset;
+      } else {
+        range = IDBKeyRange.lowerBound(pos, true);
+        order = 'next';
+      }
+      return new Promise(function (resolve, _reject) {
+        o.index('position').openCursor(range, order).onsuccess = function (e) {
+          var result = e.target.result;
+          if (result) {
+            offset --;
+            var value = result.value;
+            value.position = pos;
+            pos = result.key;
+            result.update(value);
+            if (offset) result.continue();
+            else {
+              script.position = pos;
+              o.put(script).onsuccess = function () {
+                resolve();
+              };
+            }
           }
-        }
-      };
+        };
+      });
     });
-  });
-};
+  };
 
-VMDB.prototype.getCacheB64 = function (urls, tx) {
-  tx = tx || this.db.transaction('cache');
-  var o = tx.objectStore('cache');
-  return Promise.all(urls.map(function (url) {
-    return new Promise(function (resolve, reject) {
-      o.get(url).onsuccess = function (e) {
-        resolve(e.target.result);
-      };
+  VMDB.prototype.getCacheB64 = function (urls, tx) {
+    tx = tx || this.db.transaction('cache');
+    var o = tx.objectStore('cache');
+    return Promise.all(urls.map(function (url) {
+      return new Promise(function (resolve, _reject) {
+        o.get(url).onsuccess = function (e) {
+          resolve(e.target.result);
+        };
+      });
+    })).then(function (data) {
+      return data.reduce(function (map, value, i) {
+        if (value) map[urls[i]] = value.data;
+        return map;
+      }, {});
     });
-  })).then(function (data) {
-    return data.reduce(function (map, value, i) {
-      if (value) map[urls[i]] = value.data;
-      return map;
-    }, {});
-  });
-};
-
-VMDB.prototype.saveCache = function (url, data, tx) {
-  tx = tx || this.db.transaction('cache', 'readwrite');
-  var o = tx.objectStore('cache');
-  return new Promise(function (resolve, reject) {
-    o.put({uri: url, data: data}).onsuccess = function () {
-      resolve();
-    };
-  });
-};
-
-VMDB.prototype.saveRequire = function (url, data, tx) {
-  tx = tx || this.db.transaction('require', 'readwrite');
-  var o = tx.objectStore('require');
-  return new Promise(function (resolve, reject) {
-    o.put({uri: url, code: data}).onsuccess = function () {
-      resolve();
-    };
-  });
-};
+  };
 
-VMDB.prototype.saveScript = function (script, tx) {
-  script.enabled = script.enabled ? 1 : 0;
-  script.update = script.update ? 1 : 0;
-  if (!script.position) script.position = ++ this.position;
-  tx = tx || this.db.transaction('scripts', 'readwrite');
-  var o = tx.objectStore('scripts');
-  return new Promise(function (resolve, reject) {
-    var res = o.put(script);
-    res.onsuccess = function (e) {
-      script.id = e.target.result;
-      resolve(script);
-    };
-    res.onerror = function () {
-      reject(_.i18n('msgNamespaceConflict'));
-    };
-  });
-};
+  VMDB.prototype.saveCache = function (url, data, tx) {
+    tx = tx || this.db.transaction('cache', 'readwrite');
+    var o = tx.objectStore('cache');
+    return new Promise(function (resolve, _reject) {
+      o.put({uri: url, data: data}).onsuccess = function () {
+        resolve();
+      };
+    });
+  };
 
-VMDB.prototype.fetchCache = function () {
-  var requests = {};
-  return function (url, check) {
-    var _this = this;
-    return requests[url]
-    || (requests[url] = scriptUtils.fetch(url, 'blob').then(function (res) {
-      return (check ? check(res.response) : Promise.resolve()).then(function () {
-        return res.response;
-      });
-    }).then(function (data) {
-      return new Promise(function (resolve, reject) {
-        var reader = new FileReader;
-        reader.onload = function (e) {
-          _this.saveCache(url, window.btoa(this.result)).then(function () {
-            delete requests[url];
-            resolve();
-          });
-        };
-        reader.onerror = function (e) {
-          reject(e);
-        };
-        reader.readAsBinaryString(data);
-      });
-    }));
+  VMDB.prototype.saveRequire = function (url, data, tx) {
+    tx = tx || this.db.transaction('require', 'readwrite');
+    var o = tx.objectStore('require');
+    return new Promise(function (resolve, _reject) {
+      o.put({uri: url, code: data}).onsuccess = function () {
+        resolve();
+      };
+    });
   };
-}();
 
-VMDB.prototype.fetchRequire = function () {
-  var requests = {};
-  return function (url) {
-    var _this = this;
-    return requests[url]
-    || (requests[url] = scriptUtils.fetch(url).then(function (res) {
-      return _this.saveRequire(url, res.responseText);
-    }).then(function () {
-      delete requests[url];
-    }));
+  VMDB.prototype.saveScript = function (script, tx) {
+    script.enabled = script.enabled ? 1 : 0;
+    script.update = script.update ? 1 : 0;
+    if (!script.position) script.position = ++ this.position;
+    tx = tx || this.db.transaction('scripts', 'readwrite');
+    var o = tx.objectStore('scripts');
+    return new Promise(function (resolve, reject) {
+      var res = o.put(script);
+      res.onsuccess = function (e) {
+        script.id = e.target.result;
+        resolve(script);
+      };
+      res.onerror = function () {
+        reject(_.i18n('msgNamespaceConflict'));
+      };
+    });
   };
-}();
 
-VMDB.prototype.setValue = function (uri, values) {
-  var o = this.db.transaction('values', 'readwrite').objectStore('values');
-  return new Promise(function (resolve, reject) {
-    o.put({uri: uri, values: values}).onsuccess = function () {
-      resolve();
+  VMDB.prototype.fetchCache = function () {
+    var requests = {};
+    return function (url, check) {
+      var _this = this;
+      return requests[url]
+        || (requests[url] = scriptUtils.fetch(url, 'blob').then(function (res) {
+          return (check ? check(res.response) : Promise.resolve()).then(function () {
+            return res.response;
+          });
+        }).then(function (data) {
+          return new Promise(function (resolve, reject) {
+            var reader = new FileReader;
+            reader.onload = function (_e) {
+              _this.saveCache(url, window.btoa(this.result)).then(function () {
+                delete requests[url];
+                resolve();
+              });
+            };
+            reader.onerror = function (e) {
+              reject(e);
+            };
+            reader.readAsBinaryString(data);
+          });
+        }));
     };
-  });
-};
+  }();
 
-VMDB.prototype.updateScriptInfo = function (id, data, custom) {
-  var o = this.db.transaction('scripts', 'readwrite').objectStore('scripts');
-  return new Promise(function (resolve, reject) {
-    o.get(id).onsuccess = function (e) {
-      var script = e.target.result;
-      if (!script) return reject();
-      for (var k in data)
-        if (k in script) script[k] = data[k];
-      _.assign(script.custom, custom);
-      o.put(script).onsuccess = function (e) {
-        resolve(scriptUtils.getScriptInfo(script));
-      };
+  VMDB.prototype.fetchRequire = function () {
+    var requests = {};
+    return function (url) {
+      var _this = this;
+      return requests[url]
+        || (requests[url] = scriptUtils.fetch(url).then(function (res) {
+          return _this.saveRequire(url, res.responseText);
+        }).then(function () {
+          delete requests[url];
+        }));
     };
-  });
-};
+  }();
 
-VMDB.prototype.getExportData = function (ids, withValues) {
-  function getScripts(ids) {
-    var o = tx.objectStore('scripts');
-    return Promise.all(ids.map(function (id) {
-      return new Promise(function (resolve, reject) {
-        o.get(id).onsuccess = function (e) {
-          resolve(e.target.result);
-        };
-      });
-    })).then(function (data) {
-      return data.filter(function (x) {return x;});
+  VMDB.prototype.setValue = function (uri, values) {
+    var o = this.db.transaction('values', 'readwrite').objectStore('values');
+    return new Promise(function (resolve, _reject) {
+      o.put({uri: uri, values: values}).onsuccess = function () {
+        resolve();
+      };
     });
-  }
-  var _this = this;
-  var tx = _this.db.transaction(['scripts', 'values']);
-  return getScripts(ids).then(function (scripts) {
-    var res = {
-      scripts: scripts,
-    };
-    return withValues
-    ? _this.getValues(scripts.map(function (script) {
-      return script.uri;
-    }), tx).then(function (values) {
-      res.values = values;
-      return res;
-    }) : res;
-  });
-};
+  };
 
-VMDB.prototype.vacuum = function () {
-  function getScripts() {
-    return _this.getScriptsByIndex('position', null, tx).then(function (scripts) {
-      var data = {
-        require: {},
-        cache: {},
-        values: {},
+  VMDB.prototype.updateScriptInfo = function (id, data, custom) {
+    var o = this.db.transaction('scripts', 'readwrite').objectStore('scripts');
+    return new Promise(function (resolve, reject) {
+      o.get(id).onsuccess = function (e) {
+        var script = e.target.result;
+        if (!script) return reject();
+        for (var k in data)
+          if (k in script) script[k] = data[k];
+        _.assign(script.custom, custom);
+        o.put(script).onsuccess = function (_e) {
+          resolve(scriptUtils.getScriptInfo(script));
+        };
       };
-      data.ids = scripts.map(function (script) {
-        script.meta.require.forEach(function (uri) {data.require[uri] = 1;});
-        for (var k in script.meta.resources)
-          data.cache[script.meta.resources[k]] = 1;
-        if (scriptUtils.isRemote(script.meta.icon))
-          data.cache[script.meta.icon] = 1;
-        data.values[script.uri] = 1;
-        return script.id;
-      });
-      return data;
     });
-  }
-  function vacuumPosition(ids) {
-    var o = tx.objectStore('scripts');
-    return ids.reduce(function (res, id, i) {
-      return res.then(function () {
-        return new Promise(function (resolve, reject) {
+  };
+
+  VMDB.prototype.getExportData = function (ids, withValues) {
+    function getScripts(ids) {
+      var o = tx.objectStore('scripts');
+      return Promise.all(ids.map(function (id) {
+        return new Promise(function (resolve, _reject) {
           o.get(id).onsuccess = function (e) {
-            var result = e.target.result;
-            result.position = i + 1;
-            o.put(result).onsuccess = function () {
-              resolve();
-            };
+            resolve(e.target.result);
           };
         });
+      })).then(function (data) {
+        return data.filter(function (x) {return x;});
       });
-    }, Promise.resolve());
-  }
-  function vacuumCache(dbName, dict) {
-    return new Promise(function (resolve, reject) {
-      var o = tx.objectStore(dbName);
-      o.openCursor().onsuccess = function (e) {
-        var result = e.target.result;
-        if (result) {
-          var value = result.value;
-          (new Promise(function (resolve, reject) {
-            if (!dict[value.uri])
-              o.delete(value.uri).onsuccess = function () {
-                resolve();
-              };
-            else {
-              dict[value.uri] ++;
-              resolve();
-            }
-          })).then(function () {
-            result.continue();
-          });
-        } else resolve();
-      };
-    });
-  }
-  var _this = this;
-  var tx = _this.db.transaction(['scripts', 'require', 'cache', 'values'], 'readwrite');
-  return getScripts().then(function (data) {
-    return Promise.all([
-      vacuumPosition(data.ids),
-      vacuumCache('require', data.require),
-      vacuumCache('cache', data.cache),
-      vacuumCache('values', data.values),
-    ]).then(function () {
-      return {
-        require: data.require,
-        cache: data.cache,
+    }
+    var _this = this;
+    var tx = _this.db.transaction(['scripts', 'values']);
+    return getScripts(ids).then(function (scripts) {
+      var res = {
+        scripts: scripts,
       };
+      return withValues
+        ? _this.getValues(scripts.map(function (script) {
+          return script.uri;
+        }), tx).then(function (values) {
+          res.values = values;
+          return res;
+        }) : res;
     });
-  }).then(function (data) {
-    return Promise.all([
-      Object.keys(data.require).map(function (k) {
-        return data.require[k] === 1 && _this.fetchRequire(k);
-      }),
-      Object.keys(data.cache).map(function (k) {
-        return data.cache[k] === 1 && _this.fetchCache(k);
-      }),
-    ]);
-  });
-};
-
-VMDB.prototype.getScriptsByIndex = function (index, value, tx) {
-  tx = tx || this.db.transaction('scripts');
-  return new Promise(function (resolve, reject) {
-    var o = tx.objectStore('scripts');
-    var list = [];
-    o.index(index).openCursor(value).onsuccess = function (e) {
-      var result = e.target.result;
-      if (result) {
-        list.push(result.value);
-        result.continue();
-      } else resolve(list);
-    };
-  });
-};
-
-VMDB.prototype.parseScript = function (data) {
-  var res = {
-    cmd: 'update',
-    data: {
-      message: data.message == null ? _.i18n('msgUpdated') : data.message || '',
-    },
   };
-  var meta = scriptUtils.parseMeta(data.code);
-  var _this = this;
-  var tx = _this.db.transaction(['scripts', 'require'], 'readwrite');
-  // @require
-  meta.require.forEach(function (url) {
-    var cache = data.require && data.require[url];
-    cache ? _this.saveRequire(url, cache, tx) : _this.fetchRequire(url);
-  });
-  // @resource
-  Object.keys(meta.resources).forEach(function (k) {
-    var url = meta.resources[k];
-    var cache = data.resources && data.resources[url];
-    cache ? _this.saveCache(url, cache) : _this.fetchCache(url);
-  });
-  // @icon
-  if (scriptUtils.isRemote(meta.icon))
-    _this.fetchCache(meta.icon, function (blob) {
-      return new Promise(function (resolve, reject) {
-        var url = URL.createObjectURL(blob);
-        var image = new Image;
-        var free = function () {
-          URL.revokeObjectURL(url);
+
+  VMDB.prototype.vacuum = function () {
+    function getScripts() {
+      return _this.getScriptsByIndex('position', null, tx).then(function (scripts) {
+        var data = {
+          require: {},
+          cache: {},
+          values: {},
         };
-        image.onload = function () {
-          free();
-          resolve(blob);
+        data.ids = scripts.map(function (script) {
+          script.meta.require.forEach(function (uri) {data.require[uri] = 1;});
+          for (var k in script.meta.resources)
+            data.cache[script.meta.resources[k]] = 1;
+          if (scriptUtils.isRemote(script.meta.icon))
+            data.cache[script.meta.icon] = 1;
+          data.values[script.uri] = 1;
+          return script.id;
+        });
+        return data;
+      });
+    }
+    function vacuumPosition(ids) {
+      var o = tx.objectStore('scripts');
+      return ids.reduce(function (res, id, i) {
+        return res.then(function () {
+          return new Promise(function (resolve, _reject) {
+            o.get(id).onsuccess = function (e) {
+              var result = e.target.result;
+              result.position = i + 1;
+              o.put(result).onsuccess = function () {
+                resolve();
+              };
+            };
+          });
+        });
+      }, Promise.resolve());
+    }
+    function vacuumCache(dbName, dict) {
+      return new Promise(function (resolve, _reject) {
+        var o = tx.objectStore(dbName);
+        o.openCursor().onsuccess = function (e) {
+          var result = e.target.result;
+          if (result) {
+            var value = result.value;
+            new Promise(function (resolve, _reject) {
+              if (!dict[value.uri]) {
+                o.delete(value.uri).onsuccess = function () {
+                  resolve();
+                };
+              } else {
+                dict[value.uri] ++;
+                resolve();
+              }
+            }).then(function () {
+              result.continue();
+            });
+          } else resolve();
         };
-        image.onerror = function () {
-          free();
-          reject();
+      });
+    }
+    var _this = this;
+    var tx = _this.db.transaction(['scripts', 'require', 'cache', 'values'], 'readwrite');
+    return getScripts().then(function (data) {
+      return Promise.all([
+        vacuumPosition(data.ids),
+        vacuumCache('require', data.require),
+        vacuumCache('cache', data.cache),
+        vacuumCache('values', data.values),
+      ]).then(function () {
+        return {
+          require: data.require,
+          cache: data.cache,
         };
-        image.src = url;
       });
+    }).then(function (data) {
+      return Promise.all([
+        Object.keys(data.require).map(function (k) {
+          return data.require[k] === 1 && _this.fetchRequire(k);
+        }),
+        Object.keys(data.cache).map(function (k) {
+          return data.cache[k] === 1 && _this.fetchCache(k);
+        }),
+      ]);
     });
-  return _this.queryScript(data.id, meta, tx).then(function (script) {
-    if (script) {
-      if (data.isNew) throw _.i18n('msgNamespaceConflict');
-    } else {
-      script = scriptUtils.newScript();
-      res.cmd = 'add';
-      res.data.message = _.i18n('msgInstalled');
-    }
-    if (data.more) for (var k in data.more)
-      if (k in script) script[k] = data.more[k];
-    script.meta = meta;
-    script.code = data.code;
-    script.uri = scriptUtils.getNameURI(script);
-    // use referer page as default homepage
-    if (!meta.homepageURL && !script.custom.homepageURL && scriptUtils.isRemote(data.from))
-      script.custom.homepageURL = data.from;
-    if (scriptUtils.isRemote(data.url))
-      script.custom.lastInstallURL = data.url;
-    script.custom.modified = data.modified || Date.now();
-    return _this.saveScript(script, tx);
-  }).then(function (script) {
-    _.assign(res.data, scriptUtils.getScriptInfo(script));
-    return res;
-  });
-};
+  };
+
+  VMDB.prototype.getScriptsByIndex = function (index, value, tx) {
+    tx = tx || this.db.transaction('scripts');
+    return new Promise(function (resolve, _reject) {
+      var o = tx.objectStore('scripts');
+      var list = [];
+      o.index(index).openCursor(value).onsuccess = function (e) {
+        var result = e.target.result;
+        if (result) {
+          list.push(result.value);
+          result.continue();
+        } else resolve(list);
+      };
+    });
+  };
 
-VMDB.prototype.checkUpdate = function () {
-  function check(script) {
+  VMDB.prototype.parseScript = function (data) {
     var res = {
       cmd: 'update',
       data: {
-        id: script.id,
-        checking: true,
+        message: data.message == null ? _.i18n('msgUpdated') : data.message || '',
       },
     };
-    var downloadURL = script.custom.downloadURL || script.meta.downloadURL || script.custom.lastInstallURL;
-    var updateURL = script.custom.updateURL || script.meta.updateURL || downloadURL;
-    var okHandler = function (xhr) {
-      var meta = scriptUtils.parseMeta(xhr.responseText);
-      if (scriptUtils.compareVersion(script.meta.version, meta.version) < 0)
-        return Promise.resolve();
-      res.data.checking = false;
-      res.data.message = _.i18n('msgNoUpdate');
-      _.messenger.post(res);
-      return Promise.reject();
-    };
-    var errHandler = function (xhr) {
-      res.data.checking = false;
-      res.data.message = _.i18n('msgErrorFetchingUpdateInfo');
-      _.messenger.post(res);
-      return Promise.reject();
-    };
-    var update = function () {
-      if (!downloadURL) {
-        res.data.message = '<span class="new">' + _.i18n('msgNewVersion') + '</span>';
+    var meta = scriptUtils.parseMeta(data.code);
+    var _this = this;
+    var tx = _this.db.transaction(['scripts', 'require'], 'readwrite');
+    // @require
+    meta.require.forEach(function (url) {
+      var cache = data.require && data.require[url];
+      cache ? _this.saveRequire(url, cache, tx) : _this.fetchRequire(url);
+    });
+    // @resource
+    Object.keys(meta.resources).forEach(function (k) {
+      var url = meta.resources[k];
+      var cache = data.resources && data.resources[url];
+      cache ? _this.saveCache(url, cache) : _this.fetchCache(url);
+    });
+    // @icon
+    if (scriptUtils.isRemote(meta.icon))
+      _this.fetchCache(meta.icon, function (blob) {
+        return new Promise(function (resolve, reject) {
+          var url = URL.createObjectURL(blob);
+          var image = new Image;
+          var free = function () {
+            URL.revokeObjectURL(url);
+          };
+          image.onload = function () {
+            free();
+            resolve(blob);
+          };
+          image.onerror = function () {
+            free();
+            reject();
+          };
+          image.src = url;
+        });
+      });
+      return _this.queryScript(data.id, meta, tx).then(function (script) {
+        if (script) {
+          if (data.isNew) throw _.i18n('msgNamespaceConflict');
+        } else {
+          script = scriptUtils.newScript();
+          res.cmd = 'add';
+          res.data.message = _.i18n('msgInstalled');
+        }
+        if (data.more) for (var k in data.more)
+          if (k in script) script[k] = data.more[k];
+        script.meta = meta;
+        script.code = data.code;
+        script.uri = scriptUtils.getNameURI(script);
+        // use referer page as default homepage
+        if (!meta.homepageURL && !script.custom.homepageURL && scriptUtils.isRemote(data.from))
+          script.custom.homepageURL = data.from;
+        if (scriptUtils.isRemote(data.url))
+          script.custom.lastInstallURL = data.url;
+        script.custom.modified = data.modified || Date.now();
+        return _this.saveScript(script, tx);
+      }).then(function (script) {
+        _.assign(res.data, scriptUtils.getScriptInfo(script));
+        return res;
+      });
+  };
+
+  VMDB.prototype.checkUpdate = function () {
+    function check(script) {
+      var res = {
+        cmd: 'update',
+        data: {
+          id: script.id,
+          checking: true,
+        },
+      };
+      var downloadURL = script.custom.downloadURL || script.meta.downloadURL || script.custom.lastInstallURL;
+      var updateURL = script.custom.updateURL || script.meta.updateURL || downloadURL;
+      var okHandler = function (xhr) {
+        var meta = scriptUtils.parseMeta(xhr.responseText);
+        if (scriptUtils.compareVersion(script.meta.version, meta.version) < 0)
+          return Promise.resolve();
+        res.data.checking = false;
+        res.data.message = _.i18n('msgNoUpdate');
         _.messenger.post(res);
         return Promise.reject();
-      }
-      res.data.message = _.i18n('msgUpdating');
-      _.messenger.post(res);
-      return scriptUtils.fetch(downloadURL).then(function (xhr) {
-        return xhr.responseText;
-      }, function (xhr) {
+      };
+      var errHandler = function (_xhr) {
         res.data.checking = false;
-        res.data.message = _.i18n('msgErrorFetchingScript');
+        res.data.message = _.i18n('msgErrorFetchingUpdateInfo');
         _.messenger.post(res);
         return Promise.reject();
-      });
-    };
-    if (!updateURL) return Promise.reject();
-    res.data.message = _.i18n('msgCheckingForUpdate');
-    _.messenger.post(res);
-    return scriptUtils.fetch(updateURL, null, {
-      Accept: 'text/x-userscript-meta',
-    }).then(okHandler, errHandler).then(update);
-  }
-
-  var processes = {};
-  return function (script) {
-    var _this = this;
-    var promise = processes[script.id];
-    if (!promise)
-      promise = processes[script.id] = check(script).then(function (code) {
-        delete processes[script.id];
-        return _this.parseScript({
-          id: script.id,
-          code: code,
-        }).then(function (res) {
+      };
+      var update = function () {
+        if (!downloadURL) {
+          res.data.message = '<span class="new">' + _.i18n('msgNewVersion') + '</span>';
+          _.messenger.post(res);
+          return Promise.reject();
+        }
+        res.data.message = _.i18n('msgUpdating');
+        _.messenger.post(res);
+        return scriptUtils.fetch(downloadURL).then(function (xhr) {
+          return xhr.responseText;
+        }, function (_xhr) {
           res.data.checking = false;
+          res.data.message = _.i18n('msgErrorFetchingScript');
           _.messenger.post(res);
+          return Promise.reject();
         });
-      }, function () {
-        delete processes[script.id];
-        //return Promise.reject();
-      });
-    return promise;
-  };
-}();
+      };
+      if (!updateURL) return Promise.reject();
+      res.data.message = _.i18n('msgCheckingForUpdate');
+      _.messenger.post(res);
+      return scriptUtils.fetch(updateURL, null, {
+        Accept: 'text/x-userscript-meta',
+      }).then(okHandler, errHandler).then(update);
+    }
+
+    var processes = {};
+    return function (script) {
+      var _this = this;
+      var promise = processes[script.id];
+      if (!promise)
+        promise = processes[script.id] = check(script).then(function (code) {
+          delete processes[script.id];
+          return _this.parseScript({
+            id: script.id,
+            code: code,
+          }).then(function (res) {
+            res.data.checking = false;
+            _.messenger.post(res);
+          });
+        }, function () {
+          delete processes[script.id];
+          //return Promise.reject();
+        });
+        return promise;
+    };
+  }();
+
+  module.exports = VMDB;
+});

+ 28 - 24
src/background/events.js

@@ -1,26 +1,30 @@
-function getEventEmitter() {
-  var events = {};
-  return {
-    on: on,
-    off: off,
-    fire: fire,
-  };
-  function on(type, func) {
-    var list = events[type];
-    if (!list) list = events[type] = [];
-    list.push(func);
-  }
-  function off(type, func) {
-    var list = events[type];
-    if (list) {
-      var i = list.indexOf(func);
-      if (~i) list.splice(i, 1);
+define('events', function (_require, exports, _module) {
+  function getEventEmitter() {
+    var events = {};
+    return {
+      on: on,
+      off: off,
+      fire: fire,
+    };
+    function on(type, func) {
+      var list = events[type];
+      if (!list) list = events[type] = [];
+      list.push(func);
+    }
+    function off(type, func) {
+      var list = events[type];
+      if (list) {
+        var i = list.indexOf(func);
+        if (~i) list.splice(i, 1);
+      }
+    }
+    function fire(type, data) {
+      var list = events[type];
+      list && list.forEach(function (func) {
+        func(data, type);
+      });
     }
   }
-  function fire(type, data) {
-    var list = events[type];
-    list && list.forEach(function (func) {
-      func(data, type);
-    });
-  }
-}
+
+  exports.getEventEmitter = getEventEmitter;
+});

+ 1 - 0
src/background/index.html

@@ -4,6 +4,7 @@
 		<meta charset="utf-8">
 		<title>ViolentMonkey</title>
     <script src="/lib/underscore-min.js"></script>
+    <script src="/lib/require-lite.js"></script>
 		<script src="/common.js"></script>
 	</head>
   <body>

+ 17 - 13
src/background/requests.js

@@ -1,4 +1,6 @@
-var requests = function () {
+define('requests', function (require, _exports, module) {
+  var tabsUtils = require('utils/tabs');
+  var cache = require('utils/cache');
   var requests = {};
   var verify = {};
   var special_headers = [
@@ -39,10 +41,10 @@ var requests = function () {
       } catch (e) {}
       if (evt.type === 'loadend') clearRequest(req);
       return lastPromise = lastPromise.then(function () {
-        return new Promise(function (resolve, reject) {
+        return new Promise(function (resolve, _reject) {
           if (xhr.response && xhr.responseType === 'blob') {
             var reader = new FileReader;
-            reader.onload = function (e) {
+            reader.onload = function (_e) {
               data.response = this.result;
               resolve();
             };
@@ -129,12 +131,14 @@ var requests = function () {
     var newHeaders = [];
     var vmHeaders = {};
     headers.forEach(function (header) {
-      if (header.name === 'VM-Task')
-        tasks[details.requestId] = header.value;
-      else if (header.name.slice(0, 3) === 'VM-')
+      // if (header.name === 'VM-Task') {
+      //   tasks[details.requestId] = header.value;
+      // } else
+      if (header.name.slice(0, 3) === 'VM-') {
         vmHeaders[header.name.slice(3)] = header.value;
-      else
+      } else {
         newHeaders.push(header);
+      }
     });
     var reqId = vmHeaders['Verify'];
     if (reqId) {
@@ -200,11 +204,11 @@ var requests = function () {
         return;
       }
       if ((!x.status || x.status == 200) && !/^\s*</.test(x.responseText)) {
-        _.cache.set(req.url, x.responseText);
+        cache.set(req.url, x.responseText);
         var url = chrome.extension.getURL('/options/index.html') + '#confirm/' + encodeURIComponent(req.url);
-        if (req.tabId < 0) _.tabs.create(url);
-        else _.tabs.get(req.tabId).then(function (t) {
-          _.tabs.create(url + '/' + encodeURIComponent(t.url));
+        if (req.tabId < 0) tabsUtils.create(url);
+        else tabsUtils.get(req.tabId).then(function (t) {
+          tabsUtils.create(url + '/' + encodeURIComponent(t.url));
         });
         return noredirect;
       }
@@ -214,9 +218,9 @@ var requests = function () {
     types: ['main_frame'],
   }, ['blocking', 'requestBody']);
 
-  return {
+  module.exports = {
     getRequestId: getRequestId,
     abortRequest: abortRequest,
     httpRequest: httpRequest,
   };
-}();
+});

+ 7 - 4
src/background/sync/dropbox.js

@@ -1,4 +1,7 @@
-setTimeout(function () {
+define('sync_dropbox', function (require, _exports, _module) {
+  var sync = require('sync');
+  var tabsUtils = require('utils/tabs');
+  var searchUtils = require('utils/search');
   var config = {
     client_id: 'f0q12zup2uys5w8',
     redirect_uri: 'https://violentmonkey.github.io/auth_dropbox.html',
@@ -11,9 +14,9 @@ setTimeout(function () {
       redirect_uri: config.redirect_uri,
     };
     var url = 'https://www.dropbox.com/1/oauth2/authorize';
-    var qs = searchParams.dump(params);
+    var qs = searchUtils.dump(params);
     url += '?' + qs;
-    _.tabs.create(url);
+    tabsUtils.create(url);
   }
   function checkAuthenticate(url) {
     var redirect_uri = config.redirect_uri + '#';
@@ -23,7 +26,7 @@ setTimeout(function () {
     }
   }
   function authorized(raw) {
-    var data = searchParams.load(raw);
+    var data = searchUtils.load(raw);
     if (data.access_token) {
       dropbox.config.set({
         uid: data.uid,

+ 16 - 12
src/background/sync/index.js

@@ -1,5 +1,9 @@
 /* eslint-disable no-console */
-var sync = function () {
+define('sync', function (require, _exports, module) {
+  var events = require('events');
+  var app = require('app');
+  var tabs = require('utils/tabs');
+
   var services = [];
   var servicesReady = [];
   var inited;
@@ -156,7 +160,7 @@ var sync = function () {
     metaFile: 'Violentmonkey',
     delay: function (time) {
       if (time == null) time = this.delayTime;
-      return new Promise(function (resolve, reject) {
+      return new Promise(function (resolve, _reject) {
         setTimeout(resolve, time);
       });
     },
@@ -179,7 +183,7 @@ var sync = function () {
         'error',
       ], null, _this.onStateChange),
       _this.initHeaders();
-      _this.events = getEventEmitter();
+      _this.events = events.getEventEmitter();
       _this.lastFetch = Promise.resolve();
       _this.startSync = _this.syncFactory();
     },
@@ -209,7 +213,7 @@ var sync = function () {
         console.log('Ready to sync:', _this.displayName);
         _this.syncState.set('ready');
         promise = current = current.then(function () {
-          return new Promise(function (resolve, reject) {
+          return new Promise(function (resolve, _reject) {
             debouncedResolve = _.debounce(resolve, 10 * 1000);
             debouncedResolve();
           });
@@ -339,7 +343,7 @@ var sync = function () {
         return Promise.all([
           meta,
           _this.list(),
-          vmdb.getScriptsByIndex('position'),
+          app.vmdb.getScriptsByIndex('position'),
         ]);
       }).then(function (res) {
         var remote = {
@@ -400,7 +404,7 @@ var sync = function () {
                 data.code = raw;
               }
               data.modified = item.modified;
-              return vmdb.parseScript(data)
+              return app.vmdb.parseScript(data)
               .then(function (res) {
                 _.messenger.post(res);
               });
@@ -421,7 +425,7 @@ var sync = function () {
             .then(function (data) {
               if (item.custom.modified !== data.modified) {
                 item.custom.modified = data.modified;
-                return vmdb.saveScript(item);
+                return app.vmdb.saveScript(item);
               }
             });
           }),
@@ -431,7 +435,7 @@ var sync = function () {
           }),
           delLocal.map(function (item) {
             console.log('Remove local script:', item.uri);
-            return vmdb.removeScript(item.id)
+            return app.vmdb.removeScript(item.id)
             .then(function () {
               _.messenger.post({
                 cmd: 'del',
@@ -476,13 +480,13 @@ var sync = function () {
     },
   });
 
-  _.tabs.update(function (tab) {
+  tabs.update(function (tab) {
     tab.url && services.some(function (service) {
       return service.checkAuthenticate && service.checkAuthenticate(tab.url);
-    }) && _.tabs.remove(tab.id);
+    }) && tabs.remove(tab.id);
   });
 
-  return {
+  module.exports = {
     init: init,
     sync: sync,
     service: service,
@@ -494,4 +498,4 @@ var sync = function () {
     },
     BaseService: BaseService,
   };
-}();
+});

+ 7 - 4
src/background/sync/onedrive.js

@@ -1,4 +1,7 @@
-setTimeout(function () {
+define('sync_onedrive', function (require, _exports, _module) {
+  var sync = require('sync');
+  var tabsUtils = require('utils/tabs');
+  var searchUtils = require('utils/search');
   var config = _.assign({
     client_id: '000000004418358A',
     redirect_uri: 'https://violentmonkey.github.io/auth_onedrive.html',
@@ -15,9 +18,9 @@ setTimeout(function () {
       scope: 'onedrive.appfolder wl.offline_access',
     };
     var url = 'https://login.live.com/oauth20_authorize.srf';
-    var qs = searchParams.dump(params);
+    var qs = searchUtils.dump(params);
     url += '?' + qs;
-    _.tabs.create(url);
+    tabsUtils.create(url);
   }
   function checkAuthenticate(url) {
     var redirect_uri = config.redirect_uri + '?code=';
@@ -39,7 +42,7 @@ setTimeout(function () {
       headers: {
         'Content-Type': 'application/x-www-form-urlencoded',
       },
-      body: searchParams.dump(_.assign({}, {
+      body: searchUtils.dump(_.assign({}, {
         client_id: config.client_id,
         client_secret: config.client_secret,
         redirect_uri: config.redirect_uri,

+ 0 - 241
src/background/utils.js

@@ -1,241 +0,0 @@
-var scriptUtils = {
-  isRemote: function (url) {
-    return url && !(/^(file|data):/.test(url));
-  },
-  fetch: function (url, type, headers) {
-    var xhr = new XMLHttpRequest;
-    xhr.open('GET', url, true);
-    if (type) xhr.responseType = type;
-    if (headers) for (var k in headers)
-      xhr.setRequestHeader(k, headers[k]);
-    return new Promise(function (resolve, reject) {
-      xhr.onload = function () {
-        resolve(this);
-      };
-      xhr.onerror = function () {
-        reject(this);
-      };
-      xhr.send();
-    });
-  },
-  parseMeta: function (code) {
-    // initialize meta, specify those with multiple values allowed
-    var meta = {
-      include: [],
-      exclude: [],
-      match: [],
-      require: [],
-      resource: [],
-      grant: [],
-    };
-    var flag = -1;
-    code.replace(/(?:^|\n)\/\/\s*([@=]\S+)(.*)/g, function(m, group1, group2) {
-      if (flag < 0 && group1 == '==UserScript==')
-        // start meta
-        flag = 1;
-      else if(flag > 0 && group1 == '==/UserScript==')
-        // end meta
-        flag = 0;
-      if (flag == 1 && group1[0] == '@') {
-        var key = group1.slice(1);
-        var val = group2.replace(/^\s+|\s+$/g, '');
-        var value = meta[key];
-        // multiple values allowed
-        if (value && value.push) value.push(val);
-        // only first value will be stored
-        else if (!(key in meta)) meta[key] = val;
-      }
-    });
-    meta.resources = {};
-    meta.resource.forEach(function(line) {
-      var pair = line.match(/^(\w\S*)\s+(.*)/);
-      if (pair) meta.resources[pair[1]] = pair[2];
-    });
-    delete meta.resource;
-    // @homepageURL: compatible with @homepage
-    if (!meta.homepageURL && meta.homepage) meta.homepageURL = meta.homepage;
-    return meta;
-  },
-  newScript: function () {
-    var script = {
-      custom: {},
-      enabled: 1,
-      update: 1,
-      code: '// ==UserScript==\n// @name New Script\n// @namespace Violentmonkey Scripts\n// @grant none\n// ==/UserScript==\n',
-    };
-    script.meta = scriptUtils.parseMeta(script.code);
-    return script;
-  },
-  getScriptInfo: function (script) {
-    return {
-      id: script.id,
-      custom: script.custom,
-      meta: script.meta,
-      enabled: script.enabled,
-      update: script.update,
-    };
-  },
-  getNameURI: function (script) {
-    var ns = script.meta.namespace || '';
-    var name = script.meta.name || '';
-    var nameURI = escape(ns) + ':' + escape(name) + ':';
-    if (!ns && !name) nameURI += script.id || '';
-    return nameURI;
-  },
-  compareVersion: function (ver1, ver2) {
-    ver1 = (ver1 || '').split('.');
-    ver2 = (ver2 || '').split('.');
-    var len1 = ver1.length, len2 = ver2.length;
-    for (var i = 0; i < len1 || i < len2; i ++) {
-      var delta = (parseInt(ver1[i], 10) || 0) - (parseInt(ver2[i], 10) || 0);
-      if (delta) return delta < 0 ? -1 : 1;
-    }
-    return 0;
-  },
-};
-
-var tester = function () {
-  function testURL(url, script) {
-    var custom = script.custom;
-    var meta = script.meta;
-    var inc = [], exc = [], mat = [];
-    if (custom._match !== false && meta.match) mat = mat.concat(meta.match);
-    if (custom.match) mat = mat.concat(custom.match);
-    if (custom._include !== false && meta.include) inc = inc.concat(meta.include);
-    if (custom.include) inc = inc.concat(custom.include);
-    if (custom._exclude !== false && meta.exclude) exc = exc.concat(meta.exclude);
-    if (custom.exclude) exc = exc.concat(custom.exclude);
-    var ok = !mat.length && !inc.length;
-    // @match
-    ok = ok || mat.length && function (urlParts) {
-      return mat.some(function (str) {
-        return matchTest(str, urlParts);
-      });
-    }(url.match(match_reg));
-    // @include
-    ok = ok || inc.some(function (str) {
-      return autoReg(str).test(url);
-    });
-    // exclude
-    ok = ok && !exc.some(function (str) {
-      return autoReg(str).test(url);
-    });
-    return ok;
-  }
-
-  function str2RE(str) {
-    return RegExp('^' + str.replace(/([.?\/])/g, '\\$1').replace(/\*/g, '.*?') + '$');
-  }
-
-  function autoReg(str) {
-    if (/^\/.*\/$/.test(str))
-      return RegExp(str.slice(1, -1));  // Regular-expression
-    else
-      return str2RE(str); // String with wildcards
-  }
-
-  var match_reg = /(.*?):\/\/([^\/]*)\/(.*)/;
-  function matchTest(str, urlParts) {
-    if (str == '<all_urls>') return true;
-    var parts = str.match(match_reg);
-    var ok = !!parts;
-    // scheme
-    ok = ok && (
-      // exact match
-      parts[1] == urlParts[1]
-      // * = http | https
-      || parts[1] == '*' && /^https?$/i.test(urlParts[1])
-    );
-    // host
-    ok = ok && (
-      // * matches all
-      parts[2] == '*'
-      // exact match
-      || parts[2] == urlParts[2]
-      // *.example.com
-      || /^\*\.[^*]*$/.test(parts[2]) && str2RE(parts[2]).test(urlParts[2])
-    );
-    // pathname
-    ok = ok && str2RE(parts[3]).test(urlParts[3]);
-    return ok;
-  }
-
-  return {
-    testURL: testURL,
-  };
-}();
-
-var searchParams = {
-  load: function (string) {
-    return string.split('&').reduce(function (data, piece) {
-      parts = piece.split('=');
-      data[decodeURIComponent(parts[0])] = decodeURIComponent(parts[1]);
-      return data;
-    }, {});
-  },
-  dump: function (dict) {
-    var qs = [];
-    for (var k in dict) {
-      qs.push(encodeURIComponent(k) + '=' + encodeURIComponent(dict[k]));
-    }
-    return qs.join('&');
-  },
-};
-
-_.cache = function () {
-  function get(key) {
-    var obj = cache[key];
-    return obj && obj.value;
-  }
-  function set(key, value) {
-    if (value) {
-      var obj = cache[key];
-      if (!obj) obj = cache[key] = {
-        key: key,
-      };
-      obj.value = value;
-      if (obj.timer) clearTimeout(obj.timer);
-      obj.timer = setTimeout(function () {
-        set(key);
-      }, 3000);
-    } else {
-      delete cache[key];
-    }
-  }
-  var cache = {};
-  return {
-    get: get,
-    set: set,
-  };
-}();
-
-_.tabs = {
-  create: function (url) {
-    chrome.tabs.create({url: url});
-  },
-  update: function (cb) {
-    chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
-      cb({
-        id: tabId,
-        url: changeInfo.url,
-      });
-    });
-  },
-  remove: function (id) {
-    chrome.tabs.remove(id);
-  },
-  get: function (id) {
-    return new Promise(function (resolve, reject) {
-      chrome.tabs.get(id, function (tab) {
-        resolve(tab);
-      });
-    });
-  },
-  broadcast: function (data) {
-    chrome.tabs.query({}, function (tabs) {
-      _.forEach(tabs, function (tab) {
-        chrome.tabs.sendMessage(tab.id, data);
-      });
-    });
-  },
-};

+ 26 - 0
src/background/utils/cache.js

@@ -0,0 +1,26 @@
+define('utils/cache', function (_require, _exports, module) {
+  function get(key) {
+    var obj = cache[key];
+    return obj && obj.value;
+  }
+  function set(key, value) {
+    if (value) {
+      var obj = cache[key];
+      if (!obj) obj = cache[key] = {
+        key: key,
+      };
+      obj.value = value;
+      if (obj.timer) clearTimeout(obj.timer);
+      obj.timer = setTimeout(function () {
+        set(key);
+      }, 3000);
+    } else {
+      delete cache[key];
+    }
+  }
+  var cache = {};
+  module.exports = {
+    get: get,
+    set: set,
+  };
+});

+ 97 - 0
src/background/utils/script.js

@@ -0,0 +1,97 @@
+define('utils/script', function (_require, _exports, module) {
+  module.exports = {
+    isRemote: function (url) {
+      return url && !(/^(file|data):/.test(url));
+    },
+    fetch: function (url, type, headers) {
+      var xhr = new XMLHttpRequest;
+      xhr.open('GET', url, true);
+      if (type) xhr.responseType = type;
+      if (headers) for (var k in headers)
+        xhr.setRequestHeader(k, headers[k]);
+      return new Promise(function (resolve, reject) {
+        xhr.onload = function () {
+          resolve(this);
+        };
+        xhr.onerror = function () {
+          reject(this);
+        };
+        xhr.send();
+      });
+    },
+    parseMeta: function (code) {
+      // initialize meta, specify those with multiple values allowed
+      var meta = {
+        include: [],
+        exclude: [],
+        match: [],
+        require: [],
+        resource: [],
+        grant: [],
+      };
+      var flag = -1;
+      code.replace(/(?:^|\n)\/\/\s*([@=]\S+)(.*)/g, function(_match, group1, group2) {
+        if (flag < 0 && group1 == '==UserScript==')
+          // start meta
+          flag = 1;
+          else if(flag > 0 && group1 == '==/UserScript==')
+            // end meta
+            flag = 0;
+            if (flag == 1 && group1[0] == '@') {
+              var key = group1.slice(1);
+              var val = group2.replace(/^\s+|\s+$/g, '');
+              var value = meta[key];
+              // multiple values allowed
+              if (value && value.push) value.push(val);
+              // only first value will be stored
+              else if (!(key in meta)) meta[key] = val;
+            }
+      });
+      meta.resources = {};
+      meta.resource.forEach(function(line) {
+        var pair = line.match(/^(\w\S*)\s+(.*)/);
+        if (pair) meta.resources[pair[1]] = pair[2];
+      });
+      delete meta.resource;
+      // @homepageURL: compatible with @homepage
+      if (!meta.homepageURL && meta.homepage) meta.homepageURL = meta.homepage;
+      return meta;
+    },
+    newScript: function () {
+      var script = {
+        custom: {},
+        enabled: 1,
+        update: 1,
+        code: '// ==UserScript==\n// @name New Script\n// @namespace Violentmonkey Scripts\n// @grant none\n// ==/UserScript==\n',
+      };
+      script.meta = module.exports.parseMeta(script.code);
+      return script;
+    },
+    getScriptInfo: function (script) {
+      return {
+        id: script.id,
+        custom: script.custom,
+        meta: script.meta,
+        enabled: script.enabled,
+        update: script.update,
+      };
+    },
+    getNameURI: function (script) {
+      var ns = script.meta.namespace || '';
+      var name = script.meta.name || '';
+      var nameURI = escape(ns) + ':' + escape(name) + ':';
+      if (!ns && !name) nameURI += script.id || '';
+      return nameURI;
+    },
+    compareVersion: function (ver1, ver2) {
+      ver1 = (ver1 || '').split('.');
+      ver2 = (ver2 || '').split('.');
+      var len1 = ver1.length, len2 = ver2.length;
+      for (var i = 0; i < len1 || i < len2; i ++) {
+        var delta = (parseInt(ver1[i], 10) || 0) - (parseInt(ver2[i], 10) || 0);
+        if (delta) return delta < 0 ? -1 : 1;
+      }
+      return 0;
+    },
+  };
+});

+ 18 - 0
src/background/utils/search.js

@@ -0,0 +1,18 @@
+define('utils/search', function (_require, _exports, module) {
+  module.exports = {
+    load: function (string) {
+      return string.split('&').reduce(function (data, piece) {
+        var parts = piece.split('=');
+        data[decodeURIComponent(parts[0])] = decodeURIComponent(parts[1]);
+        return data;
+      }, {});
+    },
+    dump: function (dict) {
+      var qs = [];
+      for (var k in dict) {
+        qs.push(encodeURIComponent(k) + '=' + encodeURIComponent(dict[k]));
+      }
+      return qs.join('&');
+    },
+  };
+});

+ 32 - 0
src/background/utils/tabs.js

@@ -0,0 +1,32 @@
+define('utils/tabs', function (_require, _exports, module) {
+  module.exports = {
+    create: function (url) {
+      chrome.tabs.create({url: url});
+    },
+    update: function (cb) {
+      chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, _tab) {
+        cb({
+          id: tabId,
+          url: changeInfo.url,
+        });
+      });
+    },
+    remove: function (id) {
+      chrome.tabs.remove(id);
+    },
+    get: function (id) {
+      return new Promise(function (resolve, _reject) {
+        chrome.tabs.get(id, function (tab) {
+          resolve(tab);
+        });
+      });
+    },
+    broadcast: function (data) {
+      chrome.tabs.query({}, function (tabs) {
+        _.forEach(tabs, function (tab) {
+          chrome.tabs.sendMessage(tab.id, data);
+        });
+      });
+    },
+  };
+});

+ 68 - 0
src/background/utils/tester.js

@@ -0,0 +1,68 @@
+define('utils/tester', function (_require, exports, _module) {
+  function testURL(url, script) {
+    var custom = script.custom;
+    var meta = script.meta;
+    var inc = [], exc = [], mat = [];
+    if (custom._match !== false && meta.match) mat = mat.concat(meta.match);
+    if (custom.match) mat = mat.concat(custom.match);
+    if (custom._include !== false && meta.include) inc = inc.concat(meta.include);
+    if (custom.include) inc = inc.concat(custom.include);
+    if (custom._exclude !== false && meta.exclude) exc = exc.concat(meta.exclude);
+    if (custom.exclude) exc = exc.concat(custom.exclude);
+    var ok = !mat.length && !inc.length;
+    // @match
+    ok = ok || mat.length && function (urlParts) {
+      return mat.some(function (str) {
+        return matchTest(str, urlParts);
+      });
+    }(url.match(match_reg));
+    // @include
+    ok = ok || inc.some(function (str) {
+      return autoReg(str).test(url);
+    });
+    // exclude
+    ok = ok && !exc.some(function (str) {
+      return autoReg(str).test(url);
+    });
+    return ok;
+  }
+
+  function str2RE(str) {
+    return RegExp('^' + str.replace(/([.?\/])/g, '\\$1').replace(/\*/g, '.*?') + '$');
+  }
+
+  function autoReg(str) {
+    if (/^\/.*\/$/.test(str))
+      return RegExp(str.slice(1, -1));  // Regular-expression
+    else
+      return str2RE(str); // String with wildcards
+  }
+
+  var match_reg = /(.*?):\/\/([^\/]*)\/(.*)/;
+  function matchTest(str, urlParts) {
+    if (str == '<all_urls>') return true;
+    var parts = str.match(match_reg);
+    var ok = !!parts;
+    // scheme
+    ok = ok && (
+      // exact match
+      parts[1] == urlParts[1]
+      // * = http | https
+      || parts[1] == '*' && /^https?$/i.test(urlParts[1])
+    );
+    // host
+    ok = ok && (
+      // * matches all
+      parts[2] == '*'
+      // exact match
+      || parts[2] == urlParts[2]
+      // *.example.com
+      || /^\*\.[^*]*$/.test(parts[2]) && str2RE(parts[2]).test(urlParts[2])
+    );
+    // pathname
+    ok = ok && str2RE(parts[3]).test(urlParts[3]);
+    return ok;
+  }
+
+  exports.testURL = testURL;
+});

+ 64 - 63
src/cache.js

@@ -1,4 +1,4 @@
-!function () {
+define('cache', function (require, _exports, module) {
   function Cache(allowOverride) {
     this.data = {};
     this.allowOverride = allowOverride;
@@ -24,69 +24,70 @@
     });
   };
 
-  _.cache = new Cache();
-}();
+  var cache = module.exports = new Cache();
+  require('templates');
 
-var BaseView = Backbone.View.extend({
-  initialize: function () {
-    var _this = this;
-    if (_this.templateUrl)
-      _this.__gotTemplate = _.cache.get(_this.templateUrl)
-      .then(function (fn) {
-        _this.templateFn = fn;
+  var BaseView = cache.BaseView = Backbone.View.extend({
+    initialize: function () {
+      var _this = this;
+      if (_this.templateUrl)
+        _this.__gotTemplate = cache.get(_this.templateUrl)
+        .then(function (fn) {
+          _this.templateFn = fn;
+        });
+      _.bindAll(_this, 'render', 'postrender');
+      _this.render();
+    },
+    _render: function () {
+      this.$el.html(this.templateFn());
+    },
+    render: function () {
+      var render = this._render.bind(this);
+      (this.__gotTemplate || Promise.resolve()).then(render)
+      .then(this.postrender);
+      return this;
+    },
+    postrender: function () {
+      _.forEach(this.$('[data-i18n]'), function (node) {
+        node.innerHTML = _.i18n(node.dataset.i18n);
       });
-    _.bindAll(_this, 'render', 'postrender');
-    _this.render();
-  },
-  _render: function () {
-    this.$el.html(this.templateFn());
-  },
-  render: function () {
-    var render = this._render.bind(this);
-    (this.__gotTemplate || Promise.resolve()).then(render)
-    .then(this.postrender);
-    return this;
-  },
-  postrender: function () {
-    _.forEach(this.$('[data-i18n]'), function (node) {
-      node.innerHTML = _.i18n(node.dataset.i18n);
-    });
-    _.forEach(this.$('[data-feature]'), function (node) {
-      _.features.isHit(node.dataset.feature) || node.classList.add('feature');
-    });
-  },
-  getValue: function (target) {
-    var key = target.dataset.id;
-    var value;
-    switch (key[0]) {
-    case '!':
-      key = key.slice(1);
-      value = target.checked;
-      break;
-    case '[':
-      key = key.slice(1);
-      value = _.filter(target.value.split('\n').map(function (s) {return s.trim();}));
-      break;
-    default:
-      value = target.value;
-    }
-    return {
-      key: key,
-      value: value,
-    };
-  },
-});
+      _.forEach(this.$('[data-feature]'), function (node) {
+        _.features.isHit(node.dataset.feature) || node.classList.add('feature');
+      });
+    },
+    getValue: function (target) {
+      var key = target.dataset.id;
+      var value;
+      switch (key[0]) {
+      case '!':
+        key = key.slice(1);
+        value = target.checked;
+        break;
+      case '[':
+        key = key.slice(1);
+        value = _.filter(target.value.split('\n').map(function (s) {return s.trim();}));
+        break;
+      default:
+        value = target.value;
+      }
+      return {
+        key: key,
+        value: value,
+      };
+    },
+  });
 
-BaseView.prototype.postrender.call(window);
+  BaseView.prototype.postrender.call(window);
 
-!function () {
-  var xhr = new XMLHttpRequest;
-  xhr.open('GET', '/images/sprite.svg', true);
-  xhr.onload = function () {
-    var div = document.createElement('div');
-    div.style.display = 'none';
-    div.innerHTML = xhr.responseText;
-    document.body.insertBefore(div, document.body.firstChild);
-  };
-  xhr.send();
-}();
+  !function () {
+    var xhr = new XMLHttpRequest;
+    xhr.open('GET', '/images/sprite.svg', true);
+    xhr.onload = function () {
+      var div = document.createElement('div');
+      div.style.display = 'none';
+      div.innerHTML = xhr.responseText;
+      document.body.insertBefore(div, document.body.firstChild);
+    };
+    xhr.send();
+  }();
+});

+ 1 - 1
src/injected.js

@@ -34,7 +34,7 @@ var _ = {
 function utf8decode (utftext) {
   var string = "";
   var i = 0;
-  var c = 0, c1 = 0, c2 = 0, c3 = 0;
+  var c = 0, c2 = 0, c3 = 0;
   while ( i < utftext.length ) {
     c = utftext.charCodeAt(i);
     if (c < 128) {string += String.fromCharCode(c);i++;}

+ 55 - 55
src/options/app.js

@@ -1,59 +1,59 @@
-zip.workerScriptsPath = '/lib/zip.js/';
+define('app', function (require, exports, _module) {
+  var MainView = require('views/Main');
+  var ConfirmView = require('views/Confirm');
+  var EditView = require('views/Edit');
+  var models = require('models');
+  zip.workerScriptsPath = '/lib/zip.js/';
 
-_.showMessage = function (options) {
-  new MessageView(options);
-};
-
-var App = Backbone.Router.extend({
-  routes: {
-    '': 'renderMain',
-    'main/:tab': 'renderMain',
-    'confirm/:url': 'renderConfirm',
-    'confirm/:url/:from': 'renderConfirm',
-  },
-  renderMain: function (tab) {
-    scriptList || initMain();
-    this.view = new MainView(tab);
-  },
-  renderConfirm: function (url, _from) {
-    this.view = new ConfirmView(url, _from);
-  },
-  renderEdit: function (id) {
-    this.view = new EditView(id);
-  },
-});
-var app = new App();
-if (!Backbone.history.start())
-  app.navigate('', {trigger: true, replace: true});
+  var App = Backbone.Router.extend({
+    routes: {
+      '': 'renderMain',
+      'main/:tab': 'renderMain',
+      'confirm/:url': 'renderConfirm',
+      'confirm/:url/:from': 'renderConfirm',
+    },
+    renderMain: function (tab) {
+      exports.scriptList || initMain();
+      this.view = new MainView(tab);
+    },
+    renderConfirm: function (url, _from) {
+      this.view = new ConfirmView(url, _from);
+    },
+    renderEdit: function (id) {
+      this.view = new EditView(id);
+    },
+  });
+  var app = new App();
+  Backbone.history.start() || app.navigate('', {trigger: true, replace: true});
 
-$(document).on('click', '[data-feature]', function (e) {
-  var target = e.currentTarget;
-  _.features.hit(target.dataset.feature);
-  target.classList.remove('feature');
-});
+  $(document).on('click', '[data-feature]', function (e) {
+    var target = e.currentTarget;
+    _.features.hit(target.dataset.feature);
+    target.classList.remove('feature');
+  });
 
-var scriptList, syncData;
-function initMain() {
-  scriptList = new ScriptList;
-  syncData = new SyncList;
-  var port = chrome.runtime.connect({name: 'Options'});
-  port.onMessage.addListener(function (res) {
-    switch (res.cmd) {
-    case 'sync':
-      syncData.set(res.data);
-      break;
-    case 'add':
-      res.data.message = '';
-      scriptList.push(res.data);
-      break;
-    case 'update':
-      if (res.data) {
-        var model = scriptList.get(res.data.id);
-        if (model) model.set(res.data);
+  function initMain() {
+    var scriptList = exports.scriptList = new models.ScriptList;
+    var syncData = exports.syncData = new models.SyncList;
+    var port = chrome.runtime.connect({name: 'Options'});
+    port.onMessage.addListener(function (res) {
+      switch (res.cmd) {
+      case 'sync':
+        syncData.set(res.data);
+        break;
+      case 'add':
+        res.data.message = '';
+        scriptList.push(res.data);
+        break;
+      case 'update':
+        if (res.data) {
+          var model = scriptList.get(res.data.id);
+          if (model) model.set(res.data);
+        }
+        break;
+      case 'del':
+        scriptList.remove(res.data);
       }
-      break;
-    case 'del':
-      scriptList.remove(res.data);
-    }
-  });
-}
+    });
+  }
+});

+ 3 - 3
src/options/editor.js

@@ -1,4 +1,4 @@
-!function () {
+define('editor', function (_require, exports, _module) {
   function addScripts(data) {
     function add(data) {
       var s = document.createElement('script');
@@ -63,7 +63,7 @@
 
   var readyCodeMirror;
 
-  _.initEditor = function (options) {
+  exports.init = function (options) {
     options = options || {};
     readyCodeMirror = readyCodeMirror || initCodeMirror();
     return readyCodeMirror.then(function(){
@@ -95,4 +95,4 @@
       return editor;
     });
   };
-}();
+});

+ 1 - 0
src/options/index.html

@@ -9,6 +9,7 @@
     <script src="/lib/underscore-min.js"></script>
     <script src="/lib/backbone-min.js"></script>
     <script src="/lib/zip.js/zip.js"></script>
+    <script src="/lib/require-lite.js"></script>
     <script src="/common.js"></script>
     <script src="/cache.js"></script>
   </head>

+ 51 - 47
src/options/model.js

@@ -1,51 +1,55 @@
-var Meta = Backbone.Model.extend({
-  parse: function (script) {
-    this.meta = script.meta;
-    return script.custom;
-  },
-});
+define('models', function (require, exports, _module) {
+  var app = require('app');
 
-var Script = Backbone.Model.extend({
-  getLocaleString: function (key) {
-    var _this = this;
-    var meta = _this.get('meta') || {};
-    return _.getLocaleString(meta, key);
-  },
-  canUpdate: function () {
-    var script = this.toJSON();
-    return script.update && (
-      script.custom.updateURL ||
-      script.meta.updateURL ||
-      script.custom.downloadURL ||
-      script.meta.downloadURL ||
-      script.custom.lastInstallURL
-    );
-  },
-});
+  exports.Meta = Backbone.Model.extend({
+    parse: function (script) {
+      this.meta = script.meta;
+      return script.custom;
+    },
+  });
 
-var ScriptList = Backbone.Collection.extend({
-  model: Script,
-  // comparator: 'position',
-  initialize: function () {
-    this.cache = {};
-    this.reload();
-  },
-  reload: function () {
-    var _this = this;
-    _this.loading = true;
-    _.sendMessage({cmd: 'GetData'}).then(function (data) {
-      _this.loading = false;
-      _.assign(_this.cache, data.cache);
-      _this.reset(data.scripts);
-      syncData.reset(data.sync);
-      _.features.init(data.version);
-    });
-  },
-});
+  exports.Script = Backbone.Model.extend({
+    getLocaleString: function (key) {
+      var _this = this;
+      var meta = _this.get('meta') || {};
+      return _.getLocaleString(meta, key);
+    },
+    canUpdate: function () {
+      var script = this.toJSON();
+      return script.update && (
+        script.custom.updateURL ||
+        script.meta.updateURL ||
+        script.custom.downloadURL ||
+        script.meta.downloadURL ||
+        script.custom.lastInstallURL
+      );
+    },
+  });
 
-var SyncModel = Backbone.Model.extend({
-  idAttribute: 'name',
-});
-var SyncList = Backbone.Collection.extend({
-  model: SyncModel,
+  exports.ScriptList = Backbone.Collection.extend({
+    model: exports.Script,
+    // comparator: 'position',
+    initialize: function () {
+      this.cache = {};
+      this.reload();
+    },
+    reload: function () {
+      var _this = this;
+      _this.loading = true;
+      _.sendMessage({cmd: 'GetData'}).then(function (data) {
+        _this.loading = false;
+        _.assign(_this.cache, data.cache);
+        _this.reset(data.scripts);
+        app.syncData.reset(data.sync);
+        _.features.init(data.version);
+      });
+    },
+  });
+
+  exports.SyncModel = Backbone.Model.extend({
+    idAttribute: 'name',
+  });
+  exports.SyncList = Backbone.Collection.extend({
+    model: exports.SyncModel,
+  });
 });

+ 19 - 16
src/options/views/confirm-options.js

@@ -1,17 +1,20 @@
-var ConfirmOptionsView = BaseView.extend({
-  className: 'button-panel options-panel',
-  events: {
-    'mousedown': 'stopPropagation',
-    'change [data-check]': 'updateCheckbox',
-    'change #cbClose': 'render',
-  },
-  templateUrl: '/options/templates/confirm-options.html',
-  _render: function () {
-    var options = _.options.getAll();
-    this.$el.html(this.templateFn(options));
-  },
-  stopPropagation: function (e) {
-    e.stopPropagation();
-  },
-  updateCheckbox: _.updateCheckbox,
+define('views/ConfirmOptions', function (require, _exports, module) {
+  var BaseView = require('cache').BaseView;
+  module.exports = BaseView.extend({
+    className: 'button-panel options-panel',
+    events: {
+      'mousedown': 'stopPropagation',
+      'change [data-check]': 'updateCheckbox',
+      'change #cbClose': 'render',
+    },
+    templateUrl: '/options/templates/confirm-options.html',
+    _render: function () {
+      var options = _.options.getAll();
+      this.$el.html(this.templateFn(options));
+    },
+    stopPropagation: function (e) {
+      e.stopPropagation();
+    },
+    updateCheckbox: _.updateCheckbox,
+  });
 });

+ 191 - 186
src/options/views/confirm.js

@@ -1,203 +1,208 @@
-var ConfirmView = BaseView.extend({
-  el: '#app',
-  events: {
-    'click .button-toggle': 'toggleOptions',
-    'click #btnInstall': 'installScript',
-    'click #btnClose': 'close',
-  },
-  templateUrl: '/options/templates/confirm.html',
-  initialize: function (url, _from) {
-    var _this = this;
-    _this.url = url;
-    _this.from = _from;
-    _.bindAll(_this, 'hideOptions', 'trackLocalFile');
-    BaseView.prototype.initialize.call(_this);
-  },
-  _render: function () {
-    var _this = this;
-    _this.$el.html(_this.templateFn({
-      url: _this.url,
-    }));
-    _this.showMessage(_.i18n('msgLoadingData'));
-    _this.loadedEditor = _.initEditor({
-      container: _this.$('.editor-code')[0],
-      readonly: true,
-      onexit: _this.close,
-    }).then(function (editor) {
-      _this.editor = editor;
-    });
-    _this.loadData().then(function () {
-      _this.parseMeta();
-    });
-    _this.$toggler = _this.$('.button-toggle');
-  },
-  loadData: function (changedOnly) {
-    var _this = this;
-    var _text = _this.data && _this.data.code;
-    _this.$('#btnInstall').prop('disabled', true);
-    _this.data = {
-      code: _text,
-      require: {},
-      resources: {},
-      dependencyOK: false,
-      isLocal: /^file:\/\/\//.test(_this.url),
-    };
-    return _this.getScript(_this.url).then(function (text) {
-      if (changedOnly && _text == text) return Promise.reject();
-      _this.data.code = text;
-      _this.loadedEditor.then(function () {
-        _this.editor.setValueAndFocus(_this.data.code);
+define('views/Confirm', function (require, _exports, module) {
+  var editor = require('editor');
+  var ConfirmOptionsView = require('views/ConfirmOptions');
+  var BaseView = require('cache').BaseView;
+  module.exports = BaseView.extend({
+    el: '#app',
+    events: {
+      'click .button-toggle': 'toggleOptions',
+      'click #btnInstall': 'installScript',
+      'click #btnClose': 'close',
+    },
+    templateUrl: '/options/templates/confirm.html',
+    initialize: function (url, _from) {
+      var _this = this;
+      _this.url = url;
+      _this.from = _from;
+      _.bindAll(_this, 'hideOptions', 'trackLocalFile');
+      BaseView.prototype.initialize.call(_this);
+    },
+    _render: function () {
+      var _this = this;
+      _this.$el.html(_this.templateFn({
+        url: _this.url,
+      }));
+      _this.showMessage(_.i18n('msgLoadingData'));
+      _this.loadedEditor = editor.init({
+        container: _this.$('.editor-code')[0],
+        readonly: true,
+        onexit: _this.close,
+      }).then(function (editor) {
+        _this.editor = editor;
+      });
+      _this.loadData().then(function () {
+        _this.parseMeta();
       });
-    });
-  },
-  parseMeta: function () {
-    var _this = this;
-    return _.sendMessage({
-      cmd: 'ParseMeta',
-      data: _this.data.code,
-    }).then(function (script) {
-      var urls = _.values(script.resources);
-      var length = script.require.length + urls.length;
-      var finished = 0;
-      if (!length) return;
-      var error = [];
-      var updateStatus = function () {
-        _this.showMessage(_.i18n('msgLoadingDependency', [finished, length]));
+      _this.$toggler = _this.$('.button-toggle');
+    },
+    loadData: function (changedOnly) {
+      var _this = this;
+      var _text = _this.data && _this.data.code;
+      _this.$('#btnInstall').prop('disabled', true);
+      _this.data = {
+        code: _text,
+        require: {},
+        resources: {},
+        dependencyOK: false,
+        isLocal: /^file:\/\/\//.test(_this.url),
       };
-      updateStatus();
-      var promises = script.require.map(function (url) {
-        return _this.getFile(url).then(function (res) {
-          _this.data.require[url] = res;
+      return _this.getScript(_this.url).then(function (text) {
+        if (changedOnly && _text == text) return Promise.reject();
+        _this.data.code = text;
+        _this.loadedEditor.then(function () {
+          _this.editor.setValueAndFocus(_this.data.code);
         });
       });
-      promises = promises.concat(urls.map(function (url) {
-        return _this.getFile(url, true).then(function (res) {
-          _this.data.resources[url] = res;
+    },
+    parseMeta: function () {
+      var _this = this;
+      return _.sendMessage({
+        cmd: 'ParseMeta',
+        data: _this.data.code,
+      }).then(function (script) {
+        var urls = _.values(script.resources);
+        var length = script.require.length + urls.length;
+        var finished = 0;
+        if (!length) return;
+        var error = [];
+        var updateStatus = function () {
+          _this.showMessage(_.i18n('msgLoadingDependency', [finished, length]));
+        };
+        updateStatus();
+        var promises = script.require.map(function (url) {
+          return _this.getFile(url).then(function (res) {
+            _this.data.require[url] = res;
+          });
         });
-      }));
-      promises = promises.map(function (promise) {
-        return promise.then(function () {
-          finished += 1;
-          updateStatus();
-        }, function (url) {
-          error.push(url);
+        promises = promises.concat(urls.map(function (url) {
+          return _this.getFile(url, true).then(function (res) {
+            _this.data.resources[url] = res;
+          });
+        }));
+        promises = promises.map(function (promise) {
+          return promise.then(function () {
+            finished += 1;
+            updateStatus();
+          }, function (url) {
+            error.push(url);
+          });
         });
+        return Promise.all(promises).then(function () {
+          if (error.length) return Promise.reject(error.join('\n'));
+          _this.data.dependencyOK = true;
+        });
+      }).then(function () {
+        _this.showMessage(_.i18n('msgLoadedData'));
+        _this.$('#btnInstall').prop('disabled', false);
+      }, function (error) {
+        _this.showMessage(_.i18n('msgErrorLoadingDependency'), error);
+        return Promise.reject();
       });
-      return Promise.all(promises).then(function () {
-        if (error.length) return Promise.reject(error.join('\n'));
-        _this.data.dependencyOK = true;
-      });
-    }).then(function () {
-      _this.showMessage(_.i18n('msgLoadedData'));
-      _this.$('#btnInstall').prop('disabled', false);
-    }, function (error) {
-      _this.showMessage(_.i18n('msgErrorLoadingDependency'), error);
-      return Promise.reject();
-    });
-  },
-  hideOptions: function () {
-    if (!this.optionsView) return;
-    this.$toggler.removeClass('active');
-    this.optionsView.remove();
-    this.optionsView = null;
-  },
-  toggleOptions: function (e) {
-    if (this.optionsView) {
-      this.hideOptions();
-    } else {
-      this.$toggler.addClass('active');
-      this.optionsView = new ConfirmOptionsView;
-      this.optionsView.$el.insertAfter(this.$toggler);
-      $(document).one('mousedown', this.hideOptions);
-    }
-  },
-  close: function () {
-    window.close();
-  },
-  showMessage: function (msg) {
-    this.$('#msg').html(msg);
-  },
-  getFile: function (url, isBlob) {
-    var xhr = new XMLHttpRequest;
-    xhr.open('GET', url, true);
-    if (isBlob) xhr.responseType = 'blob';
-    return new Promise(function (resolve, reject) {
-      xhr.onload = function () {
-        if (isBlob) {
-          var reader = new FileReader;
-          reader.onload = function (e) {
-            resolve(window.btoa(this.result));
-          };
-          reader.readAsBinaryString(this.response);
-        } else {
-          resolve(xhr.responseText);
-        }
-      };
-      xhr.onerror = function () {
-        reject(url);
-      };
-      xhr.send();
-    });
-  },
-  getScript: function (url) {
-    var _this = this;
-    return _.sendMessage({
-      cmd: 'GetFromCache',
-      data: url,
-    })
-    .then(function (text) {
-      return text || Promise.reject();
-    })
-    .catch(function () {
+    },
+    hideOptions: function () {
+      if (!this.optionsView) return;
+      this.$toggler.removeClass('active');
+      this.optionsView.remove();
+      this.optionsView = null;
+    },
+    toggleOptions: function () {
+      if (this.optionsView) {
+        this.hideOptions();
+      } else {
+        this.$toggler.addClass('active');
+        this.optionsView = new ConfirmOptionsView;
+        this.optionsView.$el.insertAfter(this.$toggler);
+        $(document).one('mousedown', this.hideOptions);
+      }
+    },
+    close: function () {
+      window.close();
+    },
+    showMessage: function (msg) {
+      this.$('#msg').html(msg);
+    },
+    getFile: function (url, isBlob) {
+      var xhr = new XMLHttpRequest;
+      xhr.open('GET', url, true);
+      if (isBlob) xhr.responseType = 'blob';
       return new Promise(function (resolve, reject) {
-        var xhr = new XMLHttpRequest;
-        xhr.open('GET', url, true);
         xhr.onload = function () {
-          resolve(this.responseText);
+          if (isBlob) {
+            var reader = new FileReader;
+            reader.onload = function () {
+              resolve(window.btoa(this.result));
+            };
+            reader.readAsBinaryString(this.response);
+          } else {
+            resolve(xhr.responseText);
+          }
         };
         xhr.onerror = function () {
-          _this.showMessage(_.i18n('msgErrorLoadingData'));
-          reject(this);
+          reject(url);
         };
         xhr.send();
       });
-    });
-  },
-  getTimeString: function () {
-    var now = new Date();
-    return _.zfill(now.getHours(), 2) + ':' +
+    },
+    getScript: function (url) {
+      var _this = this;
+      return _.sendMessage({
+        cmd: 'GetFromCache',
+        data: url,
+      })
+      .then(function (text) {
+        return text || Promise.reject();
+      })
+      .catch(function () {
+        return new Promise(function (resolve, reject) {
+          var xhr = new XMLHttpRequest;
+          xhr.open('GET', url, true);
+          xhr.onload = function () {
+            resolve(this.responseText);
+          };
+          xhr.onerror = function () {
+            _this.showMessage(_.i18n('msgErrorLoadingData'));
+            reject(this);
+          };
+          xhr.send();
+        });
+      });
+    },
+    getTimeString: function () {
+      var now = new Date();
+      return _.zfill(now.getHours(), 2) + ':' +
       _.zfill(now.getMinutes(), 2) + ':' +
       _.zfill(now.getSeconds(), 2);
-  },
-  installScript: function () {
-    var _this = this;
-    _this.$('#btnInstall').prop('disabled', true);
-    _.sendMessage({
-      cmd:'ParseScript',
-      data:{
-        url: _this.url,
-        from: _this.from,
-        code: _this.data.code,
-        require: _this.data.require,
-        resources: _this.data.resources,
-      },
-    }).then(function (res) {
-      _this.showMessage(res.message + '[' + _this.getTimeString() + ']');
-      if (res.code < 0) return;
-      if (_.options.get('closeAfterInstall')) _this.close();
-      else if (_this.data.isLocal && _.options.get('trackLocalFile')) _this.trackLocalFile();
-    });
-  },
-  trackLocalFile: function () {
-    var _this = this;
-    setTimeout(function () {
-      _this.loadData(true).then(function () {
-        var track = _.options.get('trackLocalFile');
-        if (!track) return;
-        _this.parseMeta().then(function () {
-          track && _this.installScript();
-        });
-      }, _this.trackLocalFile);
-    }, 2000);
-  },
+    },
+    installScript: function () {
+      var _this = this;
+      _this.$('#btnInstall').prop('disabled', true);
+      _.sendMessage({
+        cmd:'ParseScript',
+        data:{
+          url: _this.url,
+          from: _this.from,
+          code: _this.data.code,
+          require: _this.data.require,
+          resources: _this.data.resources,
+        },
+      }).then(function (res) {
+        _this.showMessage(res.message + '[' + _this.getTimeString() + ']');
+        if (res.code < 0) return;
+        if (_.options.get('closeAfterInstall')) _this.close();
+        else if (_this.data.isLocal && _.options.get('trackLocalFile')) _this.trackLocalFile();
+      });
+    },
+    trackLocalFile: function () {
+      var _this = this;
+      setTimeout(function () {
+        _this.loadData(true).then(function () {
+          var track = _.options.get('trackLocalFile');
+          if (!track) return;
+          _this.parseMeta().then(function () {
+            track && _this.installScript();
+          });
+        }, _this.trackLocalFile);
+      }, 2000);
+    },
+  });
 });

+ 27 - 24
src/options/views/edit-meta.js

@@ -1,25 +1,28 @@
-var MetaView = BaseView.extend({
-  className: 'button-panel',
-  templateUrl: '/options/templates/edit-meta.html',
-  events: {
-    'change [data-id]': 'onChange',
-    'mousedown': 'onMousedown',
-  },
-  _render: function () {
-    var model = this.model;
-    var it = model.toJSON();
-    it.__name = model.meta.name;
-    it.__homepageURL = model.meta.homepageURL;
-    it.__updateURL = model.meta.updateURL || _.i18n('hintUseDownloadURL');
-    it.__downloadURL = model.meta.downloadURL || it.lastInstallURL;
-    this.$el.html(this.templateFn(it));
-  },
-  onChange: function (e) {
-    e.stopPropagation();
-    var res = this.getValue(e.target);
-    this.model.set(res.key, res.value);
-  },
-  onMousedown: function (e) {
-    e.stopPropagation();
-  },
+define('views/Meta', function (require, _exports, module) {
+  var BaseView = require('cache').BaseView;
+  module.exports = BaseView.extend({
+    className: 'button-panel',
+    templateUrl: '/options/templates/edit-meta.html',
+    events: {
+      'change [data-id]': 'onChange',
+      'mousedown': 'onMousedown',
+    },
+    _render: function () {
+      var model = this.model;
+      var it = model.toJSON();
+      it.__name = model.meta.name;
+      it.__homepageURL = model.meta.homepageURL;
+      it.__updateURL = model.meta.updateURL || _.i18n('hintUseDownloadURL');
+      it.__downloadURL = model.meta.downloadURL || it.lastInstallURL;
+      this.$el.html(this.templateFn(it));
+    },
+    onChange: function (e) {
+      e.stopPropagation();
+      var res = this.getValue(e.target);
+      this.model.set(res.key, res.value);
+    },
+    onMousedown: function (e) {
+      e.stopPropagation();
+    },
+  });
 });

+ 114 - 106
src/options/views/edit.js

@@ -1,109 +1,117 @@
-var EditView = BaseView.extend({
-  className: 'frame edit',
-  templateUrl: '/options/templates/edit.html',
-  events: {
-    'click .button-toggle': 'toggleMeta',
-    'change [data-id]': 'updateCheckbox',
-    'click #editorSave': 'save',
-    'click #editorClose': 'close',
-    'click #editorSaveClose': 'saveClose',
-  },
-  initialize: function () {
-    var _this = this;
-    BaseView.prototype.initialize.call(_this);
-    _this.metaModel = new Meta(_this.model.toJSON(), {parse: true});
-    _this.listenTo(_this.metaModel, 'change', function (model) {
-      _this.model.set('custom', model.toJSON());
-    });
-    _this.listenTo(_this.model, 'change', function (model) {
-      _this.updateStatus(true);
-    });
-    _.bindAll(_this, 'save', 'close', 'hideMeta');
-  },
-  _render: function () {
-    var _this = this;
-    var it = _this.model.toJSON();
-    _this.$el.html(_this.templateFn(it));
-    _this.$toggler = _this.$('.button-toggle');
-    var gotScript = it.id ? _.sendMessage({
-      cmd: 'GetScript',
-      data: it.id,
-    }) : Promise.resolve(it);
-    _this.loadedEditor = _.initEditor({
-      container: _this.$('.editor-code')[0],
-      onsave: _this.save,
-      onexit: _this.close,
-      onchange: function (e) {
-        _this.model.set('code', _this.editor.getValue());
-      },
-    });
-    Promise.all([
-      gotScript,
-      _this.loadedEditor,
-    ]).then(function (res) {
-      var script = res[0];
-      var editor = _this.editor = res[1];
-      editor.setValueAndFocus(script.code);
-      editor.clearHistory();
-      _this.updateStatus(false);
-    });
-  },
-  updateStatus: function (changed) {
-    this.changed = changed;
-    this.$('#editorSave').prop('disabled', !changed);
-    this.$('#editorSaveClose').prop('disabled', !changed);
-  },
-  save: function () {
-    var _this = this;
-    var data = _this.model.toJSON();
-    return _.sendMessage({
-      cmd: 'ParseScript',
-      data: {
-        id: data.id,
-        code: data.code,
-        // User created scripts MUST be marked `isNew` so that
-        // the backend is able to check namespace conflicts
-        isNew: !data.id,
-        message: '',
-        more: {
-          custom: data.custom,
-          update: data.update,
+define('views/Edit', function (require, _exports, module) {
+  var Message = require('views/Message');
+  var MetaView = require('views/Meta');
+  var BaseView = require('cache').BaseView;
+  var models = require('models');
+  var app = require('app');
+  var editor = require('editor');
+  module.exports = BaseView.extend({
+    className: 'frame edit',
+    templateUrl: '/options/templates/edit.html',
+    events: {
+      'click .button-toggle': 'toggleMeta',
+      'change [data-id]': 'updateCheckbox',
+      'click #editorSave': 'save',
+      'click #editorClose': 'close',
+      'click #editorSaveClose': 'saveClose',
+    },
+    initialize: function () {
+      var _this = this;
+      BaseView.prototype.initialize.call(_this);
+      _this.metaModel = new models.Meta(_this.model.toJSON(), {parse: true});
+      _this.listenTo(_this.metaModel, 'change', function (model) {
+        _this.model.set('custom', model.toJSON());
+      });
+      _this.listenTo(_this.model, 'change', function (_model) {
+        _this.updateStatus(true);
+      });
+      _.bindAll(_this, 'save', 'close', 'hideMeta');
+    },
+    _render: function () {
+      var _this = this;
+      var it = _this.model.toJSON();
+      _this.$el.html(_this.templateFn(it));
+      _this.$toggler = _this.$('.button-toggle');
+      var gotScript = it.id ? _.sendMessage({
+        cmd: 'GetScript',
+        data: it.id,
+      }) : Promise.resolve(it);
+      _this.loadedEditor = editor.init({
+        container: _this.$('.editor-code')[0],
+        onsave: _this.save,
+        onexit: _this.close,
+        onchange: function () {
+          _this.model.set('code', _this.editor.getValue());
+        },
+      });
+      Promise.all([
+        gotScript,
+        _this.loadedEditor,
+      ]).then(function (res) {
+        var script = res[0];
+        var editor = _this.editor = res[1];
+        editor.setValueAndFocus(script.code);
+        editor.clearHistory();
+        _this.updateStatus(false);
+      });
+    },
+    updateStatus: function (changed) {
+      this.changed = changed;
+      this.$('#editorSave').prop('disabled', !changed);
+      this.$('#editorSaveClose').prop('disabled', !changed);
+    },
+    save: function () {
+      var _this = this;
+      var data = _this.model.toJSON();
+      return _.sendMessage({
+        cmd: 'ParseScript',
+        data: {
+          id: data.id,
+          code: data.code,
+          // User created scripts MUST be marked `isNew` so that
+          // the backend is able to check namespace conflicts
+          isNew: !data.id,
+          message: '',
+          more: {
+            custom: data.custom,
+            update: data.update,
+          }
         }
-      }
-    }).then(function (script) {
-      _this.model.set('id', script.id);
-      _this.updateStatus(false);
-    }, function (err) {
-      _.showMessage({
-        data: err,
+      }).then(function (script) {
+        _this.model.set('id', script.id);
+        _this.updateStatus(false);
+      }, function (err) {
+        new Message({
+          data: err,
+        });
       });
-    });
-  },
-  close: function () {
-    if (!this.changed || confirm(_.i18n('confirmNotSaved')))
-      scriptList.trigger('edit:close');
-  },
-  saveClose: function () {
-    this.save().then(this.close);
-  },
-  hideMeta: function () {
-    if (!this.metaView) return;
-    this.$toggler.removeClass('active');
-    this.metaView.remove();
-    this.metaView = null;
-  },
-  toggleMeta: function (e) {
-    if (this.metaView) {
-      this.hideMeta();
-    } else {
-      this.$toggler.addClass('active');
-      this.metaView = new MetaView({model: this.metaModel});
-      this.metaView.$el.insertAfter(e.target);
-      $(document).one('mousedown', this.hideMeta);
-    }
-  },
-  updateCheckbox: function (e) {
-    var res = this.getValue(e.target);
-    this.model.set(res.key, res.value);
-  },
+    },
+    close: function () {
+      if (!this.changed || confirm(_.i18n('confirmNotSaved')))
+      app.scriptList.trigger('edit:close');
+    },
+    saveClose: function () {
+      this.save().then(this.close);
+    },
+    hideMeta: function () {
+      if (!this.metaView) return;
+      this.$toggler.removeClass('active');
+      this.metaView.remove();
+      this.metaView = null;
+    },
+    toggleMeta: function (e) {
+      if (this.metaView) {
+        this.hideMeta();
+      } else {
+        this.$toggler.addClass('active');
+        this.metaView = new MetaView({model: this.metaModel});
+        this.metaView.$el.insertAfter(e.target);
+        $(document).one('mousedown', this.hideMeta);
+      }
+    },
+    updateCheckbox: function (e) {
+      var res = this.getValue(e.target);
+      this.model.set(res.key, res.value);
+    },
+  });
 });

+ 23 - 17
src/options/views/main.js

@@ -1,18 +1,24 @@
-var MainView = BaseView.extend({
-  el: '#app',
-  templateUrl: '/options/templates/main.html',
-  tabs: {
-    '': MainTab,
-    settings: SettingsTab,
-    about: AboutTab,
-  },
-  initialize: function (tab) {
-    var _this = this;
-    _this.tab = _this.tabs[tab] || _this.tabs[''];
-    BaseView.prototype.initialize.call(_this);
-  },
-  _render: function () {
-    this.$el.html(this.templateFn({tab: this.tab.prototype.name}));
-    this.view = new this.tab;
-  },
+define('views/Main', function (require, _exports, module) {
+  var BaseView = require('cache').BaseView;
+  var MainTab = require('views/TabInstalled');
+  var SettingsTab = require('views/TabSettings');
+  var AboutTab = require('views/TabAbout');
+  module.exports = BaseView.extend({
+    el: '#app',
+    templateUrl: '/options/templates/main.html',
+    tabs: {
+      '': MainTab,
+      settings: SettingsTab,
+      about: AboutTab,
+    },
+    initialize: function (tab) {
+      var _this = this;
+      _this.tab = _this.tabs[tab] || _this.tabs[''];
+      BaseView.prototype.initialize.call(_this);
+    },
+    _render: function () {
+      this.$el.html(this.templateFn({tab: this.tab.prototype.name}));
+      this.view = new this.tab;
+    },
+  });
 });

+ 37 - 34
src/options/views/message.js

@@ -1,35 +1,38 @@
-var MessageView = BaseView.extend({
-  className: 'message',
-  templateUrl: '/options/templates/message.html',
-  transitionTime: 500,
-  initialize: function (options) {
-    var _this = this;
-    _this.options = options;
-    BaseView.prototype.initialize.call(_this);
-    _.bindAll(_this, 'toggle', 'delay', 'remove');
-  },
-  _render: function () {
-    var _this = this;
-    _this.$el
-    .html(_this.templateFn(_this.options))
-    .appendTo(document.body);
-    _this.delay(16)
-    .then(_this.toggle)
-    .then(function () {
-      return _this.delay(_this.options.delay || 2000);
-    })
-    .then(_this.toggle)
-    .then(_this.remove);
-  },
-  delay: function (time) {
-    if (time == null) time = this.transitionTime;
-    return new Promise(function (resolve, reject) {
-      setTimeout(resolve, time);
-    });
-  },
-  toggle: function () {
-    var _this = this;
-    _this.$el.toggleClass('message-show');
-    return _this.delay();
-  },
+define('views/Message', function (require, _exports, module) {
+  var BaseView = require('cache').BaseView;
+  module.exports = BaseView.extend({
+    className: 'message',
+    templateUrl: '/options/templates/message.html',
+    transitionTime: 500,
+    initialize: function (options) {
+      var _this = this;
+      _this.options = options;
+      BaseView.prototype.initialize.call(_this);
+      _.bindAll(_this, 'toggle', 'delay', 'remove');
+    },
+    _render: function () {
+      var _this = this;
+      _this.$el
+      .html(_this.templateFn(_this.options))
+      .appendTo(document.body);
+      _this.delay(16)
+      .then(_this.toggle)
+      .then(function () {
+        return _this.delay(_this.options.delay || 2000);
+      })
+      .then(_this.toggle)
+      .then(_this.remove);
+    },
+    delay: function (time) {
+      if (time == null) time = this.transitionTime;
+      return new Promise(function (resolve, _reject) {
+        setTimeout(resolve, time);
+      });
+    },
+    toggle: function () {
+      var _this = this;
+      _this.$el.toggleClass('message-show');
+      return _this.delay();
+    },
+  });
 });

+ 257 - 253
src/options/views/script.js

@@ -1,270 +1,274 @@
-var DEFAULT_ICON = '/images/icon48.png';
-var ScriptView = BaseView.extend({
-  className: 'script',
-  attributes: {
-    draggable: true,
-  },
-  templateUrl: '/options/templates/script.html',
-  events: {
-    'click [data-id=edit]': 'onEdit',
-    'click [data-id=remove]': 'onRemove',
-    'click [data-id=enable]': 'onEnable',
-    'click [data-id=update]': 'onUpdate',
-    'dragstart': 'onDragStart',
-  },
-  initialize: function () {
-    var _this = this;
-    _this.model.set('_icon', DEFAULT_ICON);
-    // MUST call `super` before `render`
-    BaseView.prototype.initialize.call(_this);
-    _this.listenTo(_this.model, 'change', _this.render);
-    _this.listenTo(_this.model, 'remove', _this.onRemoved);
-  },
-  loadIcon: function () {
-    var _this = this;
-    var icon = _this.model.get('meta').icon;
-    if (icon && icon !== _this.model.get('_icon'))
+define('views/Script', function (require, _exports, module) {
+  var BaseView = require('cache').BaseView;
+  var app = require('app');
+  var DEFAULT_ICON = '/images/icon48.png';
+  module.exports = BaseView.extend({
+    className: 'script',
+    attributes: {
+      draggable: true,
+    },
+    templateUrl: '/options/templates/script.html',
+    events: {
+      'click [data-id=edit]': 'onEdit',
+      'click [data-id=remove]': 'onRemove',
+      'click [data-id=enable]': 'onEnable',
+      'click [data-id=update]': 'onUpdate',
+      'dragstart': 'onDragStart',
+    },
+    initialize: function () {
+      var _this = this;
+      _this.model.set('_icon', DEFAULT_ICON);
+      // MUST call `super` before `render`
+      BaseView.prototype.initialize.call(_this);
+      _this.listenTo(_this.model, 'change', _this.render);
+      _this.listenTo(_this.model, 'remove', _this.onRemoved);
+    },
+    loadIcon: function () {
+      var _this = this;
+      var icon = _this.model.get('meta').icon;
+      if (icon && icon !== _this.model.get('_icon'))
       _this.loadImage(icon).then(function (url) {
         _this.model.set('_icon', url);
-      }, function (url) {
+      }, function (_url) {
         _this.model.set('_icon', DEFAULT_ICON);
       });
-  },
-  _render: function () {
-    var _this = this;
-    var model = _this.model;
-    var it = model.toJSON();
-    it.getLocaleString = model.getLocaleString.bind(model);
-    it.canUpdate = model.canUpdate();
-    it.homepageURL = it.custom.homepageURL || it.meta.homepageURL || it.meta.homepage;
-    it.author = _this.getAuthor(it.meta.author);
-    _this.$el.html(_this.templateFn(it));
-    if (!it.enabled) _this.$el.addClass('disabled');
-    else _this.$el.removeClass('disabled');
-    _this.loadIcon();
-  },
-  getAuthor: function (text) {
-    if (!text) return '';
-    var matches = text.match(/^(.*?)\s<(\S*?@\S*?)>$/);
-    var label = _.i18n('labelAuthor');
-    return matches
+    },
+    _render: function () {
+      var _this = this;
+      var model = _this.model;
+      var it = model.toJSON();
+      it.getLocaleString = model.getLocaleString.bind(model);
+      it.canUpdate = model.canUpdate();
+      it.homepageURL = it.custom.homepageURL || it.meta.homepageURL || it.meta.homepage;
+      it.author = _this.getAuthor(it.meta.author);
+      _this.$el.html(_this.templateFn(it));
+      if (!it.enabled) _this.$el.addClass('disabled');
+      else _this.$el.removeClass('disabled');
+      _this.loadIcon();
+    },
+    getAuthor: function (text) {
+      if (!text) return '';
+      var matches = text.match(/^(.*?)\s<(\S*?@\S*?)>$/);
+      var label = _.i18n('labelAuthor');
+      return matches
       ? label + '<a href=mailto:' + matches[2] + '>' + matches[1] + '</a>'
       : label + _.escape(text);
-  },
-  images: {},
-  loadImage: function (url) {
-    if (!url) return;
-    var promise = this.images[url];
-    if (!promise) {
-      var cache = scriptList.cache[url];
-      promise = cache ? Promise.resolve(cache)
-      : new Promise(function (resolve, reject) {
-        var img = new Image;
-        img.onload = function () {
-          resolve(url);
-        };
-        img.onerror = function () {
-          reject(url);
-        };
-        img.src = url;
+    },
+    images: {},
+    loadImage: function (url) {
+      if (!url) return;
+      var promise = this.images[url];
+      if (!promise) {
+        var cache = app.scriptList.cache[url];
+        promise = cache ? Promise.resolve(cache)
+        : new Promise(function (resolve, reject) {
+          var img = new Image;
+          img.onload = function () {
+            resolve(url);
+          };
+          img.onerror = function () {
+            reject(url);
+          };
+          img.src = url;
+        });
+        this.images[url] = promise;
+      }
+      return promise;
+    },
+    onEdit: function () {
+      app.scriptList.trigger('edit:open', this.model);
+    },
+    onRemove: function () {
+      var _this = this;
+      _.sendMessage({
+        cmd: 'RemoveScript',
+        data: _this.model.id,
       });
-      this.images[url] = promise;
-    }
-    return promise;
-  },
-  onEdit: function () {
-    scriptList.trigger('edit:open', this.model);
-  },
-  onRemove: function () {
-    var _this = this;
-    _.sendMessage({
-      cmd: 'RemoveScript',
-      data: _this.model.id,
-    });
-  },
-  onRemoved: function () {
-    this.$el.remove();
-  },
-  onEnable: function () {
-    var _this = this;
-    _.sendMessage({
-      cmd: 'UpdateScriptInfo',
-      data: {
-        id: _this.model.id,
-        enabled: _this.model.get('enabled') ? 0 : 1,
-      },
-    });
-  },
-  onUpdate: function () {
-    _.sendMessage({
-      cmd: 'CheckUpdate',
-      data: this.model.id,
-    });
-  },
-  onDragStart: function (e) {
-    var model = this.model;
-    new DND(e, function (data) {
-      if (data.from === data.to) return;
+    },
+    onRemoved: function () {
+      this.$el.remove();
+    },
+    onEnable: function () {
+      var _this = this;
       _.sendMessage({
-        cmd: 'Move',
+        cmd: 'UpdateScriptInfo',
         data: {
-          id: model.id,
-          offset: data.to - data.from,
-        }
-      }).then(function () {
-        var collection = model.collection;
-        var models = collection.models;
-        var i = Math.min(data.from, data.to);
-        var j = Math.max(data.from, data.to);
-        var seq = [
-          models.slice(0, i),
-          models.slice(i, j + 1),
-          models.slice(j + 1),
-        ];
-        i === data.to
-        ? seq[1].unshift(seq[1].pop())
-        : seq[1].push(seq[1].shift());
-        collection.models = seq.concat.apply([], seq);
+          id: _this.model.id,
+          enabled: _this.model.get('enabled') ? 0 : 1,
+        },
       });
-    });
-  },
-});
+    },
+    onUpdate: function () {
+      _.sendMessage({
+        cmd: 'CheckUpdate',
+        data: this.model.id,
+      });
+    },
+    onDragStart: function (e) {
+      var model = this.model;
+      new DND(e, function (data) {
+        if (data.from === data.to) return;
+        _.sendMessage({
+          cmd: 'Move',
+          data: {
+            id: model.id,
+            offset: data.to - data.from,
+          }
+        }).then(function () {
+          var collection = model.collection;
+          var models = collection.models;
+          var i = Math.min(data.from, data.to);
+          var j = Math.max(data.from, data.to);
+          var seq = [
+            models.slice(0, i),
+            models.slice(i, j + 1),
+            models.slice(j + 1),
+          ];
+          i === data.to
+          ? seq[1].unshift(seq[1].pop())
+          : seq[1].push(seq[1].shift());
+          collection.models = seq.concat.apply([], seq);
+        });
+      });
+    },
+  });
 
-function DND(e, cb) {
-  this.mousemove = this.mousemove.bind(this);
-  this.mouseup = this.mouseup.bind(this);
-  if (e) {
-    e.preventDefault();
-    this.start(e);
+  function DND(e, cb) {
+    this.mousemove = this.mousemove.bind(this);
+    this.mouseup = this.mouseup.bind(this);
+    if (e) {
+      e.preventDefault();
+      this.start(e);
+    }
+    this.onDrop = cb;
   }
-  this.onDrop = cb;
-}
-DND.prototype.start = function (e) {
-  var dragging = this.dragging = {
-    el: e.currentTarget,
-  };
-  var $el = dragging.$el = $(dragging.el);
-  var parent = $el.parent();
-  var offset = $el.offset();
-  dragging.offset = {
-    x: e.clientX - offset.left,
-    y: e.clientY - offset.top,
-  };
-  var next = $el.next();
-  dragging.delta = (next.length ? next.offset().top : parent.height()) - offset.top;
-  var children = parent.children();
-  dragging.lastIndex = dragging.index = children.index($el);
-  dragging.$elements = children.not($el);
-  dragging.$dragged = $el.clone().addClass('dragging').css({
-    left: offset.left,
-    top: offset.top,
-    width: offset.width,
-  }).appendTo(parent);
-  $el.addClass('dragging-placeholder');
-  $(document).on('mousemove', this.mousemove).on('mouseup', this.mouseup);
-};
-DND.prototype.mousemove = function (e) {
-  var dragging = this.dragging;
-  dragging.$dragged.css({
-    left: e.clientX - dragging.offset.x,
-    top: e.clientY - dragging.offset.y,
-  });
-  var hovered = null;
-  dragging.$elements.each(function (i, el) {
-    var $el = $(el);
-    if ($el.hasClass('dragging-moving')) return;
+  DND.prototype.start = function (e) {
+    var dragging = this.dragging = {
+      el: e.currentTarget,
+    };
+    var $el = dragging.$el = $(dragging.el);
+    var parent = $el.parent();
     var offset = $el.offset();
-    var pad = 10;
-    if (
-      e.clientX >= offset.left + pad
-      && e.clientX <= offset.left + offset.width - pad
-      && e.clientY >= offset.top + pad
-      && e.clientY <= offset.top + offset.height - pad
-    ) {
-      hovered = {
-        index: i,
-        el: el,
-      };
-      return false;
+    dragging.offset = {
+      x: e.clientX - offset.left,
+      y: e.clientY - offset.top,
+    };
+    var next = $el.next();
+    dragging.delta = (next.length ? next.offset().top : parent.height()) - offset.top;
+    var children = parent.children();
+    dragging.lastIndex = dragging.index = children.index($el);
+    dragging.$elements = children.not($el);
+    dragging.$dragged = $el.clone().addClass('dragging').css({
+      left: offset.left,
+      top: offset.top,
+      width: offset.width,
+    }).appendTo(parent);
+    $el.addClass('dragging-placeholder');
+    $(document).on('mousemove', this.mousemove).on('mouseup', this.mouseup);
+  };
+  DND.prototype.mousemove = function (e) {
+    var dragging = this.dragging;
+    dragging.$dragged.css({
+      left: e.clientX - dragging.offset.x,
+      top: e.clientY - dragging.offset.y,
+    });
+    var hovered = null;
+    dragging.$elements.each(function (i, el) {
+      var $el = $(el);
+      if ($el.hasClass('dragging-moving')) return;
+      var offset = $el.offset();
+      var pad = 10;
+      if (
+        e.clientX >= offset.left + pad
+        && e.clientX <= offset.left + offset.width - pad
+        && e.clientY >= offset.top + pad
+        && e.clientY <= offset.top + offset.height - pad
+      ) {
+        hovered = {
+          index: i,
+          el: el,
+        };
+        return false;
+      }
+    });
+    if (hovered) {
+      var lastIndex = dragging.lastIndex;
+      var index = hovered.index;
+      var isDown = index >= lastIndex;
+      var $el = dragging.$el;
+      var delta = dragging.delta;
+      if (isDown) {
+        // If moving down, the actual index should be `index + 1`
+        index ++;
+        $el.insertAfter(hovered.el);
+      } else {
+        delta = -delta;
+        $el.insertBefore(hovered.el);
+      }
+      dragging.lastIndex = index;
+      this.animate(dragging.$elements.slice(
+        isDown ? lastIndex : index,
+        isDown ? index : lastIndex
+      ), delta);
     }
-  });
-  if (hovered) {
-    var lastIndex = dragging.lastIndex;
-    var index = hovered.index;
-    var isDown = index >= lastIndex;
-    var $el = dragging.$el;
-    var delta = dragging.delta;
-    if (isDown) {
-      // If moving down, the actual index should be `index + 1`
-      index ++;
-      $el.insertAfter(hovered.el);
+    this.checkScroll(e.clientY);
+  };
+  DND.prototype.animate = function ($elements, delta) {
+    $elements.each(function (_i, el) {
+      var $el = $(el);
+      $el.addClass('dragging-moving').css({
+        transition: 'none',
+        transform: 'translateY(' + delta + 'px)',
+      }).one('transitionend', function (e) {
+        $(e.target).removeClass('dragging-moving');
+      });
+      setTimeout(function () {
+        $el.css({
+          transition: '',
+          transform: '',
+        });
+      }, 20);
+    });
+  };
+  DND.prototype.mouseup = function (_e) {
+    $(document).off('mousemove', this.mousemove).off('mouseup', this.mouseup);
+    var dragging = this.dragging;
+    dragging.$dragged.remove();
+    dragging.$el.removeClass('dragging-placeholder');
+    this.dragging = null;
+    this.onDrop && this.onDrop({
+      from: dragging.index,
+      to: dragging.lastIndex,
+    });
+  };
+  DND.prototype.checkScroll = function (y) {
+    var dragging = this.dragging;
+    var scrollThreshold = 10;
+    dragging.scroll = 0;
+    var offset = dragging.el.parentNode.getBoundingClientRect();
+    var delta = (y - (offset.bottom - scrollThreshold)) / scrollThreshold;
+    if (delta > 0) {
+      dragging.scroll = 1 + Math.min(~~ (delta * 5), 10);
     } else {
-      delta = -delta;
-      $el.insertBefore(hovered.el);
+      delta = (offset.top + scrollThreshold - y) / scrollThreshold;
+      if (delta > 0) dragging.scroll = -1 - Math.min(~~ (delta * 5), 10);
     }
-    dragging.lastIndex = index;
-    this.animate(dragging.$elements.slice(
-      isDown ? lastIndex : index,
-      isDown ? index : lastIndex
-    ), delta);
-  }
-  this.checkScroll(e.clientY);
-};
-DND.prototype.animate = function ($elements, delta) {
-  $elements.each(function (i, el) {
-    var $el = $(el);
-    $el.addClass('dragging-moving').css({
-      transition: 'none',
-      transform: 'translateY(' + delta + 'px)',
-    }).one('transitionend', function (e) {
-      $(e.target).removeClass('dragging-moving');
-    });
-    setTimeout(function () {
-      $el.css({
-        transition: '',
-        transform: '',
-      });
-    }, 20);
-  });
-};
-DND.prototype.mouseup = function (e) {
-  $(document).off('mousemove', this.mousemove).off('mouseup', this.mouseup);
-  var dragging = this.dragging;
-  dragging.$dragged.remove();
-  dragging.$el.removeClass('dragging-placeholder');
-  this.dragging = null;
-  this.onDrop && this.onDrop({
-    from: dragging.index,
-    to: dragging.lastIndex,
-  });
-};
-DND.prototype.checkScroll = function (y) {
-  var dragging = this.dragging;
-  var scrollThreshold = 10;
-  dragging.scroll = 0;
-  var offset = dragging.el.parentNode.getBoundingClientRect();
-  var delta = (y - (offset.bottom - scrollThreshold)) / scrollThreshold;
-  if (delta > 0) {
-    dragging.scroll = 1 + Math.min(~~ (delta * 5), 10);
-  } else {
-    delta = (offset.top + scrollThreshold - y) / scrollThreshold;
-    if (delta > 0) dragging.scroll = -1 - Math.min(~~ (delta * 5), 10);
-  }
-  if (dragging.scroll) this.scrollParent();
-};
-DND.prototype.scrollParent = function () {
-  function scroll() {
-    var dragging = _this.dragging;
-    if (dragging) {
-      if (dragging.scroll) {
-        dragging.el.parentNode.scrollTop += dragging.scroll;
-        setTimeout(scroll, 20);
-      } else dragging.scrolling = false;
+    if (dragging.scroll) this.scrollParent();
+  };
+  DND.prototype.scrollParent = function () {
+    function scroll() {
+      var dragging = _this.dragging;
+      if (dragging) {
+        if (dragging.scroll) {
+          dragging.el.parentNode.scrollTop += dragging.scroll;
+          setTimeout(scroll, 20);
+        } else dragging.scrolling = false;
+      }
     }
-  }
-  var _this = this;
-  if (!_this.dragging.scrolling) {
-    _this.dragging.scrolling = true;
-    scroll();
-  }
-};
+    var _this = this;
+    if (!_this.dragging.scrolling) {
+      _this.dragging.scrolling = true;
+      scroll();
+    }
+  };
+});

+ 25 - 22
src/options/views/sync-service.js

@@ -1,23 +1,26 @@
-var SyncServiceView = BaseView.extend({
-  className: 'line',
-  templateUrl: '/options/templates/sync-service.html',
-  events: {
-    'click .sync-start': 'retry',
-  },
-  initialize: function () {
-    BaseView.prototype.initialize.call(this);
-    this.listenTo(this.model, 'change', this.render);
-  },
-  _render: function () {
-    var it = this.model.toJSON();
-    it.enabled = _.options.get(it.name + 'Enabled');
-    if (it.lastSync) it.lastSync = new Date(it.lastSync).toLocaleString();
-    this.$el.html(this.templateFn(it));
-  },
-  retry: function () {
-    _.sendMessage({
-      cmd: 'SyncStart',
-      data: this.model.get('name'),
-    });
-  },
+define('views/SyncService', function (require, _exports, module) {
+  var BaseView = require('cache').BaseView;
+  module.exports = BaseView.extend({
+    className: 'line',
+    templateUrl: '/options/templates/sync-service.html',
+    events: {
+      'click .sync-start': 'retry',
+    },
+    initialize: function () {
+      BaseView.prototype.initialize.call(this);
+      this.listenTo(this.model, 'change', this.render);
+    },
+    _render: function () {
+      var it = this.model.toJSON();
+      it.enabled = _.options.get(it.name + 'Enabled');
+      if (it.lastSync) it.lastSync = new Date(it.lastSync).toLocaleString();
+      this.$el.html(this.templateFn(it));
+    },
+    retry: function () {
+      _.sendMessage({
+        cmd: 'SyncStart',
+        data: this.model.get('name'),
+      });
+    },
+  });
 });

+ 12 - 9
src/options/views/tab-about.js

@@ -1,10 +1,13 @@
-var AboutTab = BaseView.extend({
-  el: '#tab',
-  name: 'about',
-  templateUrl: '/options/templates/tab-about.html',
-  _render: function () {
-    this.$el.html(this.templateFn({
-      version: chrome.app.getDetails().version,
-    }));
-  },
+define('views/TabAbout', function (require, _exports, module) {
+  var BaseView = require('cache').BaseView;
+  module.exports = BaseView.extend({
+    el: '#tab',
+    name: 'about',
+    templateUrl: '/options/templates/tab-about.html',
+    _render: function () {
+      this.$el.html(this.templateFn({
+        version: chrome.app.getDetails().version,
+      }));
+    },
+  });
 });

+ 77 - 70
src/options/views/tab-installed.js

@@ -1,72 +1,79 @@
-var MainTab = BaseView.extend({
-  el: '#tab',
-  name: 'main',
-  templateUrl: '/options/templates/tab-installed.html',
-  events: {
-    'click #bNew': 'newScript',
-    'click #bUpdate': 'updateAll',
-    'click #bURL': 'installFromURL',
-  },
-  initialize: function () {
-    var _this = this;
-    BaseView.prototype.initialize.call(_this);
-    _this.listenTo(scriptList, 'reset', _this.render);
-    _this.listenTo(scriptList, 'add', _this.addOne);
-    _this.listenTo(scriptList, 'add update', _this.setBackdrop);
-    _this.listenTo(scriptList, 'edit:open', function (model) {
-      _this.closeEdit();
-      _this.editView = new EditView({model: model.clone()});
-      _this.$el.append(_this.editView.$el);
-    });
-    _this.listenTo(scriptList, 'edit:close', _this.closeEdit);
-  },
-  closeEdit: function () {
-    var _this = this;
-    if (_this.editView) {
-      _this.editView.remove();
-      _this.editView = null;
-    }
-  },
-  _render: function () {
-    this.$el.html(this.templateFn());
-    this.$list = this.$('.scripts');
-    this.$bd = this.$('.backdrop');
-    this.$bdm = this.$('.backdrop > div');
-    this.setBackdrop();
-    this.addAll();
-  },
-  setBackdrop: function () {
-    if (scriptList.loading) {
-      this.$bd.addClass('mask').show();
-      this.$bdm.html(_.i18n('msgLoading'));
-    } else if (!scriptList.length) {
-      this.$bd.removeClass('mask').show();
-      this.$bdm.html(_.i18n('labelNoScripts'));
-    } else {
-      this.$bd.hide();
-    }
-  },
-  addOne: function (script) {
-    var view = new ScriptView({model: script});
-    this.$list.append(view.$el);
-  },
-  addAll: function () {
-    scriptList.forEach(this.addOne, this);
-  },
-  newScript: function () {
-    _.sendMessage({cmd: 'NewScript'}).then(function (script) {
-      scriptList.trigger('edit:open', new Script(script));
-    });
-  },
-  updateAll: function () {
-    _.sendMessage({cmd: 'CheckUpdateAll'});
-  },
-  installFromURL: function () {
-    var url = prompt(_.i18n('hintInputURL'));
-    if (~url.indexOf('://')) {
-      chrome.tabs.create({
-        url: chrome.extension.getURL('/options/index.html') + '#confirm/' + encodeURIComponent(url),
+define('views/TabInstalled', function (require, _exports, module) {
+  var BaseView = require('cache').BaseView;
+  var app = require('app');
+  var ScriptView = require('views/Script');
+  var EditView = require('views/Edit');
+  var Script = require('models').Script;
+  module.exports = BaseView.extend({
+    el: '#tab',
+    name: 'main',
+    templateUrl: '/options/templates/tab-installed.html',
+    events: {
+      'click #bNew': 'newScript',
+      'click #bUpdate': 'updateAll',
+      'click #bURL': 'installFromURL',
+    },
+    initialize: function () {
+      var _this = this;
+      BaseView.prototype.initialize.call(_this);
+      _this.listenTo(app.scriptList, 'reset', _this.render);
+      _this.listenTo(app.scriptList, 'add', _this.addOne);
+      _this.listenTo(app.scriptList, 'update', _this.setBackdrop);
+      _this.listenTo(app.scriptList, 'edit:open', function (model) {
+        _this.closeEdit();
+        _this.editView = new EditView({model: model.clone()});
+        _this.$el.append(_this.editView.$el);
       });
-    }
-  },
+      _this.listenTo(app.scriptList, 'edit:close', _this.closeEdit);
+    },
+    closeEdit: function () {
+      var _this = this;
+      if (_this.editView) {
+        _this.editView.remove();
+        _this.editView = null;
+      }
+    },
+    _render: function () {
+      this.$el.html(this.templateFn());
+      this.$list = this.$('.scripts');
+      this.$bd = this.$('.backdrop');
+      this.$bdm = this.$('.backdrop > div');
+      this.setBackdrop();
+      this.addAll();
+    },
+    setBackdrop: function () {
+      if (app.scriptList.loading) {
+        this.$bd.addClass('mask').show();
+        this.$bdm.html(_.i18n('msgLoading'));
+      } else if (!app.scriptList.length) {
+        this.$bd.removeClass('mask').show();
+        this.$bdm.html(_.i18n('labelNoScripts'));
+      } else {
+        this.$bd.hide();
+      }
+    },
+    addOne: function (script) {
+      var view = new ScriptView({model: script});
+      this.$list.append(view.$el);
+    },
+    addAll: function () {
+      app.scriptList.forEach(this.addOne, this);
+    },
+    newScript: function () {
+      _.sendMessage({cmd: 'NewScript'}).then(function (script) {
+        app.scriptList.trigger('edit:open', new Script(script));
+      });
+    },
+    updateAll: function () {
+      _.sendMessage({cmd: 'CheckUpdateAll'});
+    },
+    installFromURL: function () {
+      var url = prompt(_.i18n('hintInputURL'));
+      if (~url.indexOf('://')) {
+        chrome.tabs.create({
+          url: chrome.extension.getURL('/options/index.html') + '#confirm/' + encodeURIComponent(url),
+        });
+      }
+    },
+  });
 });

+ 256 - 251
src/options/views/tab-settings.js

@@ -1,271 +1,276 @@
-var ExportList = BaseView.extend({
-  el: '.export-list',
-  templateUrl: '/options/templates/option.html',
-  initialize: function () {
-    BaseView.prototype.initialize.call(this);
-    this.listenTo(scriptList, 'reset change', this.render);
-  },
-  _render: function () {
-    var _this = this;
-    _this.$el.html(scriptList.map(function (script) {
-      return _this.templateFn(script.toJSON());
-    }).join(''));
-  },
-  getSelected: function () {
-    var selected = [];
-    this.$('option').each(function (i, option) {
-      if (option.selected) selected.push(scriptList.at(i));
-    });
-    return selected;
-  },
-  toggleAll: function () {
-    var options = this.$('option');
-    var select = _.some(options, function (option) {
-      return !option.selected;
-    });
-    options.each(function (i, option) {
-      option.selected = select;
-    });
-  },
-});
-
-var SettingsTab = BaseView.extend({
-  el: '#tab',
-  name: 'settings',
-  events: {
-    'change [data-check]': 'updateCheckbox',
-    // 'change #sInjectMode': 'updateInjectMode',
-    'change #cUpdate': 'updateAutoUpdate',
-    'click #bSelect': 'toggleSelection',
-    'click #bImport': 'importFile',
-    'click #bExport': 'exportData',
-    'click #bVacuum': 'onVacuum',
-    'click [data-auth]': 'authenticate',
-    'change [data-sync]': 'toggleSync',
-  },
-  templateUrl: '/options/templates/tab-settings.html',
-  initialize: function () {
-    BaseView.prototype.initialize.call(this);
-    this.listenTo(syncData, 'reset', this.render);
-  },
-  _render: function () {
-    var options = _.options.getAll();
-    this.$el.html(this.templateFn(options));
-    var syncServices = this.$('.sync-services');
-    syncData.each(function (service) {
-      var serviceView = new SyncServiceView({model: service});
-      syncServices.append(serviceView.$el);
-    });
-    // this.$('#sInjectMode').val(options.injectMode);
-    // this.updateInjectHint();
-    this.exportList = new ExportList;
-  },
-  updateCheckbox: _.updateCheckbox,
-  updateAutoUpdate: function (e) {
-    _.sendMessage({cmd: 'AutoUpdate'});
-  },
-  // updateInjectHint: function () {
-  //   this.$('#sInjectMode+span').text([
-  //     _.i18n('hintInjectModeNormal'),
-  //     _.i18n('hintInjectModeAdvanced'),
-  //   ][this.$('#sInjectMode').val()]);
-  // },
-  // updateInjectMode: function (e) {
-  //   _.options.set('injectMode', e.target.value);
-  //   this.updateInjectHint();
-  // },
-  toggleSelection: function () {
-    this.exportList.toggleAll();
-  },
-  importData: function (file) {
-    function getVMConfig(text) {
-      var vm;
-      try {
-        vm = JSON.parse(text);
-      } catch (e) {
-        console.warn('Error parsing ViolentMonkey configuration.');
-      }
-      vm = vm || {};
-      _.forEach(vm.values, function (value, key) {
-        _.sendMessage({
-          cmd: 'SetValue',
-          data: {
-            uri: key,
-            values: value,
-          }
-        });
+define('views/TabSettings', function (require, _exports, module) {
+  var BaseView = require('cache').BaseView;
+  var app = require('app');
+  var SyncServiceView = require('views/SyncService');
+  var ExportList = BaseView.extend({
+    el: '.export-list',
+    templateUrl: '/options/templates/option.html',
+    initialize: function () {
+      BaseView.prototype.initialize.call(this);
+      this.listenTo(app.scriptList, 'reset change update', this.render);
+    },
+    _render: function () {
+      var _this = this;
+      _this.$el.html(app.scriptList.map(function (script) {
+        return _this.templateFn(script.toJSON());
+      }).join(''));
+    },
+    getSelected: function () {
+      var selected = [];
+      this.$('option').each(function (i, option) {
+        if (option.selected) selected.push(app.scriptList.at(i));
       });
-      _.forEach(vm.settings, function (value, key) {
-        _.options.set(key, value);
+      return selected;
+    },
+    toggleAll: function () {
+      var options = this.$('option');
+      var select = _.some(options, function (option) {
+        return !option.selected;
       });
-      return vm;
-    }
-    function getVMFile(entry, vm) {
-      if (!entry.filename.endsWith('.user.js')) return;
-      vm = vm || {};
-      return new Promise(function (resolve, reject) {
-        var writer = new zip.TextWriter;
-        entry.getData(writer, function (text) {
-          var script = {code: text};
-          if (vm.scripts) {
-            var more = vm.scripts[entry.filename.slice(0, -8)];
-            if (more) script.more = _.omit(more, ['id']);
-          }
+      options.each(function (_i, option) {
+        option.selected = select;
+      });
+    },
+  });
+
+  module.exports = BaseView.extend({
+    el: '#tab',
+    name: 'settings',
+    events: {
+      'change [data-check]': 'updateCheckbox',
+      // 'change #sInjectMode': 'updateInjectMode',
+      'change #cUpdate': 'updateAutoUpdate',
+      'click #bSelect': 'toggleSelection',
+      'click #bImport': 'importFile',
+      'click #bExport': 'exportData',
+      'click #bVacuum': 'onVacuum',
+      'click [data-auth]': 'authenticate',
+      'change [data-sync]': 'toggleSync',
+    },
+    templateUrl: '/options/templates/tab-settings.html',
+    initialize: function () {
+      BaseView.prototype.initialize.call(this);
+      this.listenTo(app.syncData, 'reset', this.render);
+    },
+    _render: function () {
+      var options = _.options.getAll();
+      this.$el.html(this.templateFn(options));
+      var syncServices = this.$('.sync-services');
+      app.syncData.each(function (service) {
+        var serviceView = new SyncServiceView({model: service});
+        syncServices.append(serviceView.$el);
+      });
+      // this.$('#sInjectMode').val(options.injectMode);
+      // this.updateInjectHint();
+      this.exportList = new ExportList;
+    },
+    updateCheckbox: _.updateCheckbox,
+    updateAutoUpdate: function (_e) {
+      _.sendMessage({cmd: 'AutoUpdate'});
+    },
+    // updateInjectHint: function () {
+    //   this.$('#sInjectMode+span').text([
+    //     _.i18n('hintInjectModeNormal'),
+    //     _.i18n('hintInjectModeAdvanced'),
+    //   ][this.$('#sInjectMode').val()]);
+    // },
+    // updateInjectMode: function (e) {
+    //   _.options.set('injectMode', e.target.value);
+    //   this.updateInjectHint();
+    // },
+    toggleSelection: function () {
+      this.exportList.toggleAll();
+    },
+    importData: function (file) {
+      function getVMConfig(text) {
+        var vm;
+        try {
+          vm = JSON.parse(text);
+        } catch (e) {
+          console.warn('Error parsing ViolentMonkey configuration.');
+        }
+        vm = vm || {};
+        _.forEach(vm.values, function (value, key) {
           _.sendMessage({
-            cmd: 'ParseScript',
-            data: script,
-          }).then(function () {
-            resolve(true);
+            cmd: 'SetValue',
+            data: {
+              uri: key,
+              values: value,
+            }
           });
         });
-      });
-    }
-    function getVMFiles(entries) {
-      var i = _.findIndex(entries, function (entry) {
-        return entry.filename === 'ViolentMonkey';
-      });
-      if (~i) return new Promise(function (resolve, reject) {
-        var writer = new zip.TextWriter;
-        entries[i].getData(writer, function (text) {
-          entries.splice(i, 1);
-          resolve({
-            vm: getVMConfig(text),
-            entries: entries,
+        _.forEach(vm.settings, function (value, key) {
+          _.options.set(key, value);
+        });
+        return vm;
+      }
+      function getVMFile(entry, vm) {
+        if (!entry.filename.endsWith('.user.js')) return;
+        vm = vm || {};
+        return new Promise(function (resolve, _reject) {
+          var writer = new zip.TextWriter;
+          entry.getData(writer, function (text) {
+            var script = {code: text};
+            if (vm.scripts) {
+              var more = vm.scripts[entry.filename.slice(0, -8)];
+              if (more) script.more = _.omit(more, ['id']);
+            }
+            _.sendMessage({
+              cmd: 'ParseScript',
+              data: script,
+            }).then(function () {
+              resolve(true);
+            });
           });
         });
-      });
-      return {
-        entries: entries,
-      };
-    }
-    function readZip(file) {
-      return new Promise(function (resolve, reject) {
-        zip.createReader(new zip.BlobReader(file), function (res) {
-          res.getEntries(function (entries) {
-            resolve(entries);
+      }
+      function getVMFiles(entries) {
+        var i = _.findIndex(entries, function (entry) {
+          return entry.filename === 'ViolentMonkey';
+        });
+        if (~i) return new Promise(function (resolve, _reject) {
+          var writer = new zip.TextWriter;
+          entries[i].getData(writer, function (text) {
+            entries.splice(i, 1);
+            resolve({
+              vm: getVMConfig(text),
+              entries: entries,
+            });
           });
-        }, function (err) {reject(err);});
-      });
-    }
-    readZip(file).then(getVMFiles).then(function (data) {
-      var vm = data.vm;
-      var entries = data.entries;
-      return Promise.all(entries.map(function (entry) {
-        return getVMFile(entry, vm);
-      })).then(function (res) {
-        return _.filter(res).length;
-      });
-    }).then(function (count) {
-      scriptList.reload();
-      alert(_.i18n('msgImported', [count]));
-    });
-  },
-  importFile: function () {
-    var _this = this;
-    $('<input type=file accept=".zip">')
-    .change(function (e) {
-      if (this.files && this.files.length)
-        _this.importData(this.files[0]);
-    })
-    .trigger('click');
-  },
-  exportData: function () {
-    function getWriter() {
-      return new Promise(function (resolve, reject) {
-        zip.createWriter(new zip.BlobWriter, function (writer) {
-          resolve(writer);
         });
-      });
-    }
-    function addFile(writer, file) {
-      return new Promise(function (resolve, reject) {
-        writer.add(file.name, new zip.TextReader(file.content), function () {
-          resolve(writer);
+        return {
+          entries: entries,
+        };
+      }
+      function readZip(file) {
+        return new Promise(function (resolve, reject) {
+          zip.createReader(new zip.BlobReader(file), function (res) {
+            res.getEntries(function (entries) {
+              resolve(entries);
+            });
+          }, function (err) {reject(err);});
+        });
+      }
+      readZip(file).then(getVMFiles).then(function (data) {
+        var vm = data.vm;
+        var entries = data.entries;
+        return Promise.all(entries.map(function (entry) {
+          return getVMFile(entry, vm);
+        })).then(function (res) {
+          return _.filter(res).length;
         });
+      }).then(function (count) {
+        app.scriptList.reload();
+        alert(_.i18n('msgImported', [count]));
       });
-    }
-    function download(writer) {
-      return new Promise(function (resolve, reject) {
-        writer.close(function (blob) {
-          resolve(blob);
+    },
+    importFile: function () {
+      var _this = this;
+      $('<input type=file accept=".zip">')
+      .change(function (_e) {
+        if (this.files && this.files.length)
+        _this.importData(this.files[0]);
+      })
+      .trigger('click');
+    },
+    exportData: function () {
+      function getWriter() {
+        return new Promise(function (resolve, _reject) {
+          zip.createWriter(new zip.BlobWriter, function (writer) {
+            resolve(writer);
+          });
         });
-      }).then(function (blob) {
-        var url = URL.createObjectURL(blob);
-        $('<a>').attr({
-          href: url,
-          download: 'scripts.zip',
-        }).trigger('click');
-        setTimeout(function () {
-          URL.revokeObjectURL(url);
+      }
+      function addFile(writer, file) {
+        return new Promise(function (resolve, _reject) {
+          writer.add(file.name, new zip.TextReader(file.content), function () {
+            resolve(writer);
+          });
+        });
+      }
+      function download(writer) {
+        return new Promise(function (resolve, _reject) {
+          writer.close(function (blob) {
+            resolve(blob);
+          });
+        }).then(function (blob) {
+          var url = URL.createObjectURL(blob);
+          $('<a>').attr({
+            href: url,
+            download: 'scripts.zip',
+          }).trigger('click');
+          setTimeout(function () {
+            URL.revokeObjectURL(url);
+          });
         });
-      });
-    }
-    var bExport = this.$('#bExport');
-    bExport.prop('disabled', true);
-    var selected = this.exportList.getSelected();
-    if (!selected.length) return;
-    var withValues = this.$('#cbValues').prop('checked');
-    _.sendMessage({
-      cmd: 'ExportZip',
-      data: {
-        values: withValues,
-        ids: _.pluck(selected, 'id'),
       }
-    }).then(function (data) {
-      var names = {};
-      var vm = {
-        scripts: {},
-        settings: _.options.getAll(),
-      };
-      if (withValues) vm.values = {};
-      var files = data.scripts.map(function (script) {
-        var name = script.custom.name || script.meta.name || 'Noname';
-        if (names[name]) name += '_' + (++ names[name]);
-        else names[name] = 1;
-        vm.scripts[name] = _.pick(script, ['id', 'custom', 'enabled', 'update']);
-        if (withValues) {
-          var values = data.values[script.uri];
-          if (values) vm.values[script.uri] = values;
+      var bExport = this.$('#bExport');
+      bExport.prop('disabled', true);
+      var selected = this.exportList.getSelected();
+      if (!selected.length) return;
+      var withValues = this.$('#cbValues').prop('checked');
+      _.sendMessage({
+        cmd: 'ExportZip',
+        data: {
+          values: withValues,
+          ids: _.pluck(selected, 'id'),
         }
-        return {
-          name: name + '.user.js',
-          content: script.code,
+      }).then(function (data) {
+        var names = {};
+        var vm = {
+          scripts: {},
+          settings: _.options.getAll(),
         };
+        if (withValues) vm.values = {};
+        var files = data.scripts.map(function (script) {
+          var name = script.custom.name || script.meta.name || 'Noname';
+          if (names[name]) name += '_' + (++ names[name]);
+          else names[name] = 1;
+          vm.scripts[name] = _.pick(script, ['id', 'custom', 'enabled', 'update']);
+          if (withValues) {
+            var values = data.values[script.uri];
+            if (values) vm.values[script.uri] = values;
+          }
+          return {
+            name: name + '.user.js',
+            content: script.code,
+          };
+        });
+        files.push({
+          name: 'ViolentMonkey',
+          content: JSON.stringify(vm),
+        });
+        return files;
+      }).then(function (files) {
+        return files.reduce(function (result, file) {
+          return result.then(function (writer) {
+            return addFile(writer, file);
+          });
+        }, getWriter()).then(download);
+      }).then(function () {
+        bExport.prop('disabled', false);
       });
-      files.push({
-        name: 'ViolentMonkey',
-        content: JSON.stringify(vm),
+    },
+    onVacuum: function (e) {
+      var button = $(e.target);
+      button.prop('disabled', true).html(_.i18n('buttonVacuuming'));
+      _.sendMessage({cmd: 'Vacuum'}).then(function () {
+        button.html(_.i18n('buttonVacuumed'));
       });
-      return files;
-    }).then(function (files) {
-      return files.reduce(function (result, file) {
-        return result.then(function (writer) {
-          return addFile(writer, file);
+    },
+    authenticate: function (e) {
+      _.sendMessage({cmd: 'Authenticate', data: e.target.dataset.auth});
+    },
+    toggleSync: function (e) {
+      if (e.target.checked) {
+        this.$('[data-sync]').each(function (_i, target) {
+          if (target !== e.target && target.checked) {
+            target.checked = false;
+            _.updateCheckbox({target: target});
+          }
         });
-      }, getWriter()).then(download);
-    }).then(function () {
-      bExport.prop('disabled', false);
-    });
-  },
-  onVacuum: function (e) {
-    var button = $(e.target);
-    button.prop('disabled', true).html(_.i18n('buttonVacuuming'));
-    _.sendMessage({cmd: 'Vacuum'}).then(function () {
-      button.html(_.i18n('buttonVacuumed'));
-    });
-  },
-  authenticate: function (e) {
-    _.sendMessage({cmd: 'Authenticate', data: e.target.dataset.auth});
-  },
-  toggleSync: function (e) {
-    if (e.target.checked) {
-      this.$('[data-sync]').each(function (i, target) {
-        if (target !== e.target && target.checked) {
-          target.checked = false;
-          _.updateCheckbox({target: target});
-        }
-      });
-      _.sendMessage({cmd: 'SyncStart'});
-    }
-  },
+        _.sendMessage({cmd: 'SyncStart'});
+      }
+    },
+  });
 });

+ 115 - 101
src/popup/app.js

@@ -1,112 +1,126 @@
-var App = Backbone.Router.extend({
-  routes: {
-    '': 'renderMenu',
-    commands: 'renderCommands',
-    domains: 'renderDomains',
-  },
-  renderMenu: function () {
-    this.view = new MenuView;
-  },
-  renderCommands: function () {
-    this.view = new CommandsView;
-  },
-  renderDomains: function () {
-    this.view = new DomainsView;
-  },
-});
-var app = new App();
-if (!Backbone.history.start())
-  app.navigate('', {trigger: true, replace: true});
+define('app', function (require, exports, _module) {
+  var models = require('models');
+  var Menu = models.Menu;
+  var MenuItem = models.MenuItem;
+  var MenuView = require('views/Menu');
+  var CommandsView = require('views/Command');
+  var DomainsView = require('views/Domain');
 
-!function () {
-  function commandClick(e, model) {
-    chrome.tabs.sendMessage(app.currentTab.id, {
-      cmd: 'Command',
-      data: model.get('name'),
-    });
-  }
-  function domainClick(e, model) {
-    chrome.tabs.create({
-      url: 'https://greasyfork.org/scripts/search?q=' + model.get('name'),
-    });
-  }
-  function scriptSymbol(data) {
-    return data ? 'check' : 'remove';
-  }
-  function scriptClick(e, model) {
-    var data = !model.get('data');
-    _.sendMessage({
-      cmd: 'UpdateScriptInfo',
-      data: {
-        id: model.get('id'),
-        enabled: data,
-      },
-    }).then(function () {
-      model.set({data: data});
-      _.options.get('autoReload') && chrome.tabs.reload(app.currentTab.id);
-    });
-  }
-  function init() {
-    chrome.tabs.sendMessage(app.currentTab.id, {cmd: 'GetPopup'});
-    if (app.currentTab && /^https?:\/\//i.test(app.currentTab.url)) {
-      var matches = app.currentTab.url.match(/:\/\/(?:www\.)?([^\/]*)/);
-      var domain = matches[1];
-      var pieces = domain.split('.').reverse();
-      var domains = [];
-      var last = pieces.shift();
-      pieces.forEach(function (piece) {
-        last = piece + '.' + last;
-        domains.unshift(last);
+  exports.scriptsMenu = new Menu;
+  exports.commandsMenu = new Menu;
+  exports.domainsMenu = new Menu;
+
+  var App = Backbone.Router.extend({
+    routes: {
+      '': 'renderMenu',
+      commands: 'renderCommands',
+      domains: 'renderDomains',
+    },
+    renderMenu: function () {
+      this.view = new MenuView;
+    },
+    renderCommands: function () {
+      this.view = new CommandsView;
+    },
+    renderDomains: function () {
+      this.view = new DomainsView;
+    },
+  });
+  var app = new App();
+  Backbone.history.start() || app.navigate('', {trigger: true, replace: true});
+  exports.navigate = app.navigate.bind(app);
+  var currentTab;
+
+  !function () {
+    function commandClick(_e, model) {
+      chrome.tabs.sendMessage(currentTab.id, {
+        cmd: 'Command',
+        data: model.get('name'),
+      });
+    }
+    function domainClick(_e, model) {
+      chrome.tabs.create({
+        url: 'https://greasyfork.org/scripts/search?q=' + model.get('name'),
+      });
+    }
+    function scriptSymbol(data) {
+      return data ? 'check' : 'remove';
+    }
+    function scriptClick(_e, model) {
+      var data = !model.get('data');
+      _.sendMessage({
+        cmd: 'UpdateScriptInfo',
+        data: {
+          id: model.get('id'),
+          enabled: data,
+        },
+      }).then(function () {
+        model.set({data: data});
+        _.options.get('autoReload') && chrome.tabs.reload(currentTab.id);
       });
-      if (!domains.length) domains.push(domain);
-      domainsMenu.reset(domains.map(function (domain) {
-        return new MenuItem({
-          name: domain,
-          title: true,
-          className: 'ellipsis',
-          onClick: domainClick,
+    }
+    function init() {
+      chrome.tabs.sendMessage(currentTab.id, {cmd: 'GetPopup'});
+      if (currentTab && /^https?:\/\//i.test(currentTab.url)) {
+        var matches = currentTab.url.match(/:\/\/(?:www\.)?([^\/]*)/);
+        var domain = matches[1];
+        var pieces = domain.split('.').reverse();
+        var domains = [];
+        var last = pieces.shift();
+        pieces.forEach(function (piece) {
+          last = piece + '.' + last;
+          domains.unshift(last);
         });
-      }));
+        if (!domains.length) domains.push(domain);
+        exports.domainsMenu.reset(domains.map(function (domain) {
+          return new MenuItem({
+            name: domain,
+            title: true,
+            className: 'ellipsis',
+            onClick: domainClick,
+          });
+        }));
+      }
     }
-  }
 
-  var commands = {
-    SetPopup: function (data, src, callback) {
-      if (app.currentTab.id !== src.tab.id) return;
-      commandsMenu.reset(data.menus.map(function (menu) {
-        return new MenuItem({
-          name: menu[0],
-          symbol: 'right-hand',
-          title: true,
-          className: 'ellipsis',
-          onClick: commandClick,
-        });
-      }));
-      _.sendMessage({
-        cmd: 'GetMetas',
-        data: data.ids,
-      }).then(function (scripts) {
-        scriptsMenu.reset(scripts.map(function (script) {
+    var commands = {
+      SetPopup: function (data, src, _callback) {
+        if (currentTab.id !== src.tab.id) return;
+        exports.commandsMenu.reset(data.menus.map(function (menu) {
           return new MenuItem({
-            id: script.id,
-            name: script.custom.name || _.getLocaleString(script.meta, 'name'),
-            data: !!script.enabled,
-            symbol: scriptSymbol,
+            name: menu[0],
+            symbol: 'right-hand',
             title: true,
             className: 'ellipsis',
-            onClick: scriptClick,
+            onClick: commandClick,
           });
         }));
-      });
-    },
-  };
-  chrome.runtime.onMessage.addListener(function (req, src, callback) {
-    var func = commands[req.cmd];
-    if (func) func(req.data, src, callback);
-  });
+        _.sendMessage({
+          cmd: 'GetMetas',
+          data: data.ids,
+        }).then(function (scripts) {
+          exports.scriptsMenu.reset(scripts.map(function (script) {
+            return new MenuItem({
+              id: script.id,
+              name: script.custom.name || _.getLocaleString(script.meta, 'name'),
+              data: !!script.enabled,
+              symbol: scriptSymbol,
+              title: true,
+              className: 'ellipsis',
+              onClick: scriptClick,
+            });
+          }));
+        });
+      },
+    };
+    chrome.runtime.onMessage.addListener(function (req, src, callback) {
+      var func = commands[req.cmd];
+      if (func) func(req.data, src, callback);
+    });
 
-  chrome.tabs.query({currentWindow: true, active: true}, function (tabs) {
-    app.currentTab = tabs[0];
-    init();
-  });
-}();
+    chrome.tabs.query({currentWindow: true, active: true}, function (tabs) {
+      currentTab = exports.currentTab = tabs[0];
+      init();
+    });
+  }();
+});

+ 1 - 0
src/popup/index.html

@@ -7,6 +7,7 @@
 		<script src="/lib/zepto.min.js"></script>
     <script src="/lib/underscore-min.js"></script>
     <script src="/lib/backbone-min.js"></script>
+		<script src="/lib/require-lite.js"></script>
 		<script src="/common.js"></script>
     <script src="/cache.js"></script>
 	</head>

+ 5 - 7
src/popup/model.js

@@ -1,9 +1,7 @@
-var MenuItem = Backbone.Model.extend({});
+define('models', function (_require, exports, _module) {
+  exports.MenuItem = Backbone.Model.extend({});
 
-var Menu = Backbone.Collection.extend({
-  model: MenuItem,
+  exports.Menu = Backbone.Collection.extend({
+    model: exports.MenuItem,
+  });
 });
-
-var scriptsMenu = new Menu;
-var commandsMenu = new Menu;
-var domainsMenu = new Menu;

+ 28 - 22
src/popup/views/base.js

@@ -1,23 +1,29 @@
-var MenuBaseView = BaseView.extend({
-  el: '#popup',
-  templateUrl: '/popup/templates/menu.html',
-  addMenuItem: function (obj, parent) {
-    if (!(obj instanceof MenuItem)) obj = new MenuItem(obj);
-    var item = new MenuItemView({model: obj});
-    parent.append(item.$el);
-  },
-  components: function () {
-    var $el = this.$el;
-    var children = $el.children();
-    return {
-      top: children.first(),
-      bot: children.last(),
-      plh: $el.children('.placeholder'),
-    };
-  },
-  fixStyles: function (div, plh) {
-    plh.html(div.html());
-    var pad = div[0].offsetWidth - div[0].clientWidth + 2;
-    plh.css('padding-right', pad + 'px');
-  },
+define('views/Base', function (require, _exports, module) {
+  var BaseView = require('cache').BaseView;
+  var MenuItem = require('models').MenuItem;
+  var MenuItemView = require('views/MenuItem');
+
+  module.exports = BaseView.extend({
+    el: '#popup',
+    templateUrl: '/popup/templates/menu.html',
+    addMenuItem: function (obj, parent) {
+      if (!(obj instanceof MenuItem)) obj = new MenuItem(obj);
+      var item = new MenuItemView({model: obj});
+      parent.append(item.$el);
+    },
+    components: function () {
+      var $el = this.$el;
+      var children = $el.children();
+      return {
+        top: children.first(),
+        bot: children.last(),
+        plh: $el.children('.placeholder'),
+      };
+    },
+    fixStyles: function (div, plh) {
+      plh.html(div.html());
+      var pad = div[0].offsetWidth - div[0].clientWidth + 2;
+      plh.css('padding-right', pad + 'px');
+    },
+  });
 });

+ 34 - 28
src/popup/views/command.js

@@ -1,30 +1,36 @@
-var CommandsView = MenuBaseView.extend({
-  initialize: function () {
-    MenuBaseView.prototype.initialize.call(this);
-    this.listenTo(commandsMenu, 'reset', this.render);
-  },
-  _render: function () {
-    if (!commandsMenu.length)
+define('views/Command', function (require, _exports, module) {
+  var MenuBaseView = require('views/Base');
+  var app = require('app');
+
+  module.exports = MenuBaseView.extend({
+    initialize: function () {
+      var _this = this;
+      MenuBaseView.prototype.initialize.call(_this);
+      _this.listenTo(app.commandsMenu, 'reset', _this.render);
+    },
+    _render: function () {
+      if (!app.commandsMenu.length)
       return app.navigate('', {trigger: true, replace: true});
-    var _this = this;
-    _this.$el.html(_this.templateFn({
-      hasSep: true
-    }));
-    var comp = _this.components();
-    var top = comp.top;
-    var bot = comp.bot;
-    _this.addMenuItem({
-      name: _.i18n('menuBack'),
-      symbol: 'arrow-left',
-      onClick: function (e) {
-        app.navigate('', {trigger: true});
-      },
-    }, top);
-    commandsMenu.each(function (item) {
-      _this.addMenuItem(item, bot);
-    });
-    setTimeout(function () {
-      _this.fixStyles(bot, comp.plh);
-    });
-  },
+      var _this = this;
+      _this.$el.html(_this.templateFn({
+        hasSep: true
+      }));
+      var comp = _this.components();
+      var top = comp.top;
+      var bot = comp.bot;
+      _this.addMenuItem({
+        name: _.i18n('menuBack'),
+        symbol: 'arrow-left',
+        onClick: function (_e) {
+          app.navigate('', {trigger: true});
+        },
+      }, top);
+      app.commandsMenu.each(function (item) {
+        _this.addMenuItem(item, bot);
+      });
+      setTimeout(function () {
+        _this.fixStyles(bot, comp.plh);
+      });
+    },
+  });
 });

+ 33 - 27
src/popup/views/domain.js

@@ -1,28 +1,34 @@
-var DomainsView = MenuBaseView.extend({
-  initialize: function () {
-    MenuBaseView.prototype.initialize.call(this);
-    this.listenTo(domainsMenu, 'reset', this.render);
-  },
-  _render: function () {
-    var _this = this;
-    _this.$el.html(_this.templateFn({
-      hasSep: true
-    }));
-    var comp = _this.components();
-    var top = comp.top;
-    var bot = comp.bot;
-    _this.addMenuItem({
-      name: _.i18n('menuBack'),
-      symbol: 'arrow-left',
-      onClick: function (e) {
-        app.navigate('', {trigger: true});
-      },
-    }, top);
-    domainsMenu.each(function (item) {
-      _this.addMenuItem(item, bot);
-    });
-    setTimeout(function () {
-      _this.fixStyles(bot, comp.plh);
-    });
-  },
+define('views/Domain', function (require, _exports, module) {
+  var MenuBaseView = require('views/Base');
+  var app = require('app');
+
+  module.exports = MenuBaseView.extend({
+    initialize: function () {
+      var _this = this;
+      MenuBaseView.prototype.initialize.call(_this);
+      _this.listenTo(app.domainsMenu, 'reset', _this.render);
+    },
+    _render: function () {
+      var _this = this;
+      _this.$el.html(_this.templateFn({
+        hasSep: true
+      }));
+      var comp = _this.components();
+      var top = comp.top;
+      var bot = comp.bot;
+      _this.addMenuItem({
+        name: _.i18n('menuBack'),
+        symbol: 'arrow-left',
+        onClick: function (_e) {
+          app.navigate('', {trigger: true});
+        },
+      }, top);
+      app.domainsMenu.each(function (item) {
+        _this.addMenuItem(item, bot);
+      });
+      setTimeout(function () {
+        _this.fixStyles(bot, comp.plh);
+      });
+    },
+  });
 });

+ 35 - 30
src/popup/views/item.js

@@ -1,33 +1,38 @@
-var MenuItemView = BaseView.extend({
-  className: 'menu-item',
-  templateUrl: '/popup/templates/menuitem.html',
-  events: {
-    'click .menu-item-detail': 'onClickDetail',
-    'click .menu-item-label': 'onClick',
-  },
-  initialize: function () {
-    BaseView.prototype.initialize.call(this);
-    this.listenTo(this.model, 'change', this.render);
-  },
-  _render: function () {
-    var it = this.model.toJSON();
-    if (typeof it.symbol === 'function')
+define('views/MenuItem', function (require, _exports, module) {
+  var BaseView = require('cache').BaseView;
+
+  module.exports = BaseView.extend({
+    className: 'menu-item',
+    templateUrl: '/popup/templates/menuitem.html',
+    events: {
+      'click .menu-item-detail': 'onClickDetail',
+      'click .menu-item-label': 'onClick',
+    },
+    initialize: function () {
+      var _this = this;
+      BaseView.prototype.initialize.call(_this);
+      _this.listenTo(_this.model, 'change', _this.render);
+    },
+    _render: function () {
+      var it = this.model.toJSON();
+      if (typeof it.symbol === 'function')
       it.symbol = it.symbol(it.data);
-    if (typeof it.name === 'function')
+      if (typeof it.name === 'function')
       it.name = it.name(it.data);
-    this.$el.html(this.templateFn(it))
-    .attr('title', it.title === true ? it.name : it.title);
-    if (it.data === false) this.$el.addClass('disabled');
-    else this.$el.removeClass('disabled');
-    it.className && this.$el.addClass(it.className);
-    it.onClickDetail && this.$el.addClass('has-detail');
-  },
-  onClick: function (e) {
-    var onClick = this.model.get('onClick');
-    onClick && onClick(e, this.model);
-  },
-  onClickDetail: function (e) {
-    var onClickDetail = this.model.get('onClickDetail');
-    onClickDetail && onClickDetail(e, this.model);
-  },
+      this.$el.html(this.templateFn(it))
+      .attr('title', it.title === true ? it.name : it.title);
+      if (it.data === false) this.$el.addClass('disabled');
+      else this.$el.removeClass('disabled');
+      it.className && this.$el.addClass(it.className);
+      it.onClickDetail && this.$el.addClass('has-detail');
+    },
+    onClick: function (e) {
+      var onClick = this.model.get('onClick');
+      onClick && onClick(e, this.model);
+    },
+    onClickDetail: function (e) {
+      var onClickDetail = this.model.get('onClickDetail');
+      onClickDetail && onClickDetail(e, this.model);
+    },
+  });
 });

+ 75 - 70
src/popup/views/menu.js

@@ -1,82 +1,87 @@
-var MenuView = MenuBaseView.extend({
-  initialize: function () {
-    MenuBaseView.prototype.initialize.call(this);
-    this.listenTo(scriptsMenu, 'reset', this.render);
-    this.listenTo(commandsMenu, 'reset', this.render);
-    this.listenTo(domainsMenu, 'reset', this.render);
-  },
-  _render: function () {
-    var _this = this;
-    _this.$el.html(_this.templateFn({
-      hasSep: !!scriptsMenu.length
-    }));
-    var comp = _this.components();
-    var top = comp.top;
-    var bot = comp.bot;
-    _this.addMenuItem({
-      name: _.i18n('menuManageScripts'),
-      symbol: 'cog',
-      onClick: function (e) {
-        var url = chrome.extension.getURL(chrome.app.getDetails().options_page);
-        chrome.tabs.query({
-          currentWindow: true,
-          url: url,
-        }, function (tabs) {
-          var tab = _.find(tabs, function (tab) {
-            var hash = tab.url.match(/#(\w+)/);
-            return !hash || !_.includes(['confirm'], hash[1]);
-          });
-          if (tab) chrome.tabs.update(tab.id, {active: true});
-          else chrome.tabs.create({url: url});
-        });
-      },
-    }, top);
-    if (domainsMenu.length)
+define('views/Menu', function (require, _exports, module) {
+  var MenuBaseView = require('views/Base');
+  var app = require('app');
+
+  module.exports = MenuBaseView.extend({
+    initialize: function () {
+      var _this = this;
+      MenuBaseView.prototype.initialize.call(_this);
+      _this.listenTo(app.scriptsMenu, 'reset', _this.render);
+      _this.listenTo(app.commandsMenu, 'reset', _this.render);
+      _this.listenTo(app.domainsMenu, 'reset', _this.render);
+    },
+    _render: function () {
+      var _this = this;
+      _this.$el.html(_this.templateFn({
+        hasSep: !!app.scriptsMenu.length,
+      }));
+      var comp = _this.components();
+      var top = comp.top;
+      var bot = comp.bot;
       _this.addMenuItem({
+        name: _.i18n('menuManageScripts'),
+        symbol: 'cog',
+        onClick: function (_e) {
+          var url = chrome.extension.getURL(chrome.app.getDetails().options_page);
+          chrome.tabs.query({
+            currentWindow: true,
+            url: url,
+          }, function (tabs) {
+            var tab = _.find(tabs, function (tab) {
+              var hash = tab.url.match(/#(\w+)/);
+              return !hash || !_.includes(['confirm'], hash[1]);
+            });
+            if (tab) chrome.tabs.update(tab.id, {active: true});
+            else chrome.tabs.create({url: url});
+          });
+        },
+      }, top);
+      app.domainsMenu.length && _this.addMenuItem({
         name: _.i18n('menuFindScripts'),
         symbol: 'search',
-        onClick: function (e) {
+        onClick: function (_e) {
           var matches = app.currentTab.url.match(/:\/\/(?:www\.)?([^\/]*)/);
           chrome.tabs.create({
             url: 'https://greasyfork.org/scripts/search?q=' + matches[1],
           });
         },
-        onClickDetail: function (e) {
+        onClickDetail: function (_e) {
           app.navigate('domains', {trigger: true});
         },
       }, top);
-    if (commandsMenu.length) _this.addMenuItem({
-      name: _.i18n('menuCommands'),
-      symbol: 'arrow-right',
-      onClick: function (e) {
-        app.navigate('commands', {trigger: true});
-      },
-    }, top);
-    _this.addMenuItem({
-      name: function (data) {
-        return data ? _.i18n('menuScriptEnabled') : _.i18n('menuScriptDisabled');
-      },
-      data: _.options.get('isApplied'),
-      symbol: function (data) {
-        return data ? 'check' : 'remove';
-      },
-      onClick: function (e, model) {
-        var isApplied = !model.get('data');
-        _.options.set('isApplied', isApplied);
-        model.set({data: isApplied});
-        chrome.browserAction.setIcon({
-          path: {
-            19: '/images/icon19' + (isApplied ? '' : 'w') + '.png',
-            38: '/images/icon38' + (isApplied ? '' : 'w') + '.png'
-          },
-        });
-      },
-    }, top);
-    scriptsMenu.each(function (item) {
-      _this.addMenuItem(item, bot);
-    });
-    setTimeout(function () {
-      _this.fixStyles(bot, comp.plh);
-    });
-  },
+      app.commandsMenu.length && _this.addMenuItem({
+        name: _.i18n('menuCommands'),
+        symbol: 'arrow-right',
+        onClick: function (_e) {
+          app.navigate('commands', {trigger: true});
+        },
+      }, top);
+      _this.addMenuItem({
+        name: function (data) {
+          return data ? _.i18n('menuScriptEnabled') : _.i18n('menuScriptDisabled');
+        },
+        data: _.options.get('isApplied'),
+        symbol: function (data) {
+          return data ? 'check' : 'remove';
+        },
+        onClick: function (_e, model) {
+          var isApplied = !model.get('data');
+          _.options.set('isApplied', isApplied);
+          model.set({data: isApplied});
+          chrome.browserAction.setIcon({
+            path: {
+              19: '/images/icon19' + (isApplied ? '' : 'w') + '.png',
+              38: '/images/icon38' + (isApplied ? '' : 'w') + '.png'
+            },
+          });
+        },
+      }, top);
+      app.scriptsMenu.each(function (item) {
+        _this.addMenuItem(item, bot);
+      });
+      setTimeout(function () {
+        _this.fixStyles(bot, comp.plh);
+      });
+    },
+  });
 });

+ 37 - 0
src/public/lib/require-lite.js

@@ -0,0 +1,37 @@
+!function (root) {
+  function define(name, factory) {
+    var module = modules[name];
+    if (module) {
+      throw 'Module is already defined: ' + name;
+    }
+    module = modules[name] = {
+      name: name,
+      factory: factory,
+      data: {
+        exports: {},
+      },
+      initialized: false,
+    };
+  }
+
+  function require(name) {
+    var module = modules[name];
+    if (!module) {
+      throw 'Module not found: ' + name;
+    }
+    if (!module.initialized) {
+      module.initialized = true;
+      module.factory(require, module.data.exports, module.data);
+    }
+    return module.data.exports;
+  }
+
+  function use(names) {
+    if (!Array.isArray(names)) names = [names];
+    names.forEach(require);
+  }
+
+  var modules = {};
+  root.define = define;
+  define.use = use;
+}(this);