فهرست منبع

upgrade to vue 2.0, modularize with define-commonjs

Gerald 9 سال پیش
والد
کامیت
5e627778d3
83فایلهای تغییر یافته به همراه4193 افزوده شده و 4242 حذف شده
  1. 1 1
      .eslintrc.yml
  2. 47 21
      gulpfile.js
  3. 3 3
      package.json
  4. 9 11
      scripts/i18n.js
  5. 30 37
      scripts/templateCache.js
  6. 57 0
      scripts/update.js
  7. 0 59
      scripts/updateLib.js
  8. 242 247
      src/background/app.js
  9. 572 574
      src/background/db.js
  10. 0 30
      src/background/events.js
  11. 1 1
      src/background/index.html
  12. 204 205
      src/background/requests.js
  13. 113 115
      src/background/sync/dropbox.js
  14. 471 467
      src/background/sync/index.js
  15. 165 166
      src/background/sync/onedrive.js
  16. 22 24
      src/background/utils/cache.js
  17. 28 0
      src/background/utils/events.js
  18. 92 94
      src/background/utils/script.js
  19. 16 18
      src/background/utils/search.js
  20. 27 29
      src/background/utils/tabs.js
  21. 69 71
      src/background/utils/tester.js
  22. 30 31
      src/cache.js
  23. 117 119
      src/common.js
  24. 99 100
      src/options/app.js
  25. 0 189
      src/options/components/confirm.js
  26. 0 149
      src/options/components/edit.js
  27. 0 127
      src/options/components/editor.js
  28. 0 16
      src/options/components/main.html
  29. 0 30
      src/options/components/main.js
  30. 0 1
      src/options/components/message.html
  31. 0 21
      src/options/components/message.js
  32. 0 254
      src/options/components/script.js
  33. 0 14
      src/options/components/sync-service.html
  34. 0 61
      src/options/components/sync-service.js
  35. 0 28
      src/options/components/tab-about.html
  36. 0 14
      src/options/components/tab-about.js
  37. 0 18
      src/options/components/tab-installed.html
  38. 0 85
      src/options/components/tab-installed.js
  39. 0 40
      src/options/components/tab-settings.html
  40. 0 258
      src/options/components/tab-settings.js
  41. 2 2
      src/options/index.html
  42. 3 2
      src/options/style.css
  43. 21 31
      src/options/utils/dropdown.js
  44. 27 51
      src/options/utils/features.js
  45. 30 31
      src/options/utils/index.js
  46. 35 24
      src/options/utils/settings.js
  47. 6 6
      src/options/views/confirm.html
  48. 184 0
      src/options/views/confirm.js
  49. 26 26
      src/options/views/edit.html
  50. 145 0
      src/options/views/edit.js
  51. 0 0
      src/options/views/editor.html
  52. 124 0
      src/options/views/editor.js
  53. 16 0
      src/options/views/main.html
  54. 23 0
      src/options/views/main.js
  55. 4 0
      src/options/views/message.html
  56. 26 0
      src/options/views/message.js
  57. 6 6
      src/options/views/script.html
  58. 254 0
      src/options/views/script.js
  59. 12 0
      src/options/views/sync-service.html
  60. 60 0
      src/options/views/sync-service.js
  61. 28 0
      src/options/views/tab-about.html
  62. 12 0
      src/options/views/tab-about.js
  63. 18 0
      src/options/views/tab-installed.html
  64. 94 0
      src/options/views/tab-installed.js
  65. 40 0
      src/options/views/tab-settings.html
  66. 260 0
      src/options/views/tab-settings.js
  67. 64 65
      src/popup/app.js
  68. 0 39
      src/popup/components/command.js
  69. 0 37
      src/popup/components/domain.js
  70. 0 22
      src/popup/components/item.js
  71. 0 117
      src/popup/components/menu.js
  72. 0 41
      src/popup/components/mixin.js
  73. 2 2
      src/popup/index.html
  74. 5 7
      src/popup/utils/index.js
  75. 37 0
      src/popup/views/command.js
  76. 35 0
      src/popup/views/domain.js
  77. 1 1
      src/popup/views/item.html
  78. 20 0
      src/popup/views/item.js
  79. 2 2
      src/popup/views/menu.html
  80. 115 0
      src/popup/views/menu.js
  81. 39 0
      src/popup/views/mixin.js
  82. 0 0
      src/public/lib/define.js
  83. 2 2
      src/public/lib/vue.min.js

+ 1 - 1
.eslintrc.yml

@@ -17,9 +17,9 @@ rules:
 
 env:
   browser: true
+  node: true
 
 globals:
-  define: true
   chrome: true
   zip: true
   Vue: true

+ 47 - 21
gulpfile.js

@@ -1,13 +1,14 @@
+const del = require('del');
 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 eslint = require('gulp-eslint');
 const uglify = require('gulp-uglify');
 const svgSprite = require('gulp-svg-sprite');
+const definePack = require('define-commonjs/pack/gulp');
 const templateCache = require('./scripts/templateCache');
 const i18n = require('./scripts/i18n');
 const pkg = require('./package.json');
@@ -20,6 +21,12 @@ const paths = {
     'src/**/*.html',
     '!src/**/index.html',
   ],
+  jsCollect: [
+    'src/**/*.js',
+    '!src/public/**',
+    '!src/injected.js',
+  ],
+  jsCommon: 'src/common.js',
   jsBg: 'src/background/**/*.js',
   jsOptions: 'src/options/**/*.js',
   jsPopup: 'src/popup/**/*.js',
@@ -30,19 +37,18 @@ const paths = {
     'src/**/*.yml',
   ],
   copy: [
-    'src/*.js',
-    '!src/cache.js',
+    'src/injected.js',
     'src/public/**',
     'src/*/*.html',
     'src/*/*.css',
   ],
 };
 
+gulp.task('del', () => del(['dist']));
+
 gulp.task('watch', ['build'], () => {
   gulp.watch([].concat(paths.cache, paths.templates), ['templates']);
-  gulp.watch(paths.jsBg, ['js-bg']);
-  gulp.watch(paths.jsOptions, ['js-options']);
-  gulp.watch(paths.jsPopup, ['js-popup']);
+  gulp.watch(paths.jsCollect, ['js']);
   gulp.watch(paths.copy, ['copy-files']);
   gulp.watch(paths.locales, ['copy-i18n']);
 });
@@ -57,44 +63,66 @@ gulp.task('lint', () => (
 ));
 
 var cacheObj;
+var collect;
 
-gulp.task('templates', () => {
-  cacheObj = templateCache();
+gulp.task('collect-js', () => {
+  collect = definePack();
+  return gulp.src(paths.jsCollect)
+  .pipe(collect);
+});
+
+gulp.task('templates', ['collect-js'], () => {
+  cacheObj = templateCache('cache');
   var stream = merge2([
     gulp.src(paths.cache),
     gulp.src(paths.templates).pipe(cacheObj),
   ])
-  .pipe(concat('cache.js'));
+  .pipe(concat('cache.js'))
+  .pipe(collect.pack(null, file => 'src/cache.js'));
+  if (isProd) stream = stream.pipe(uglify());
+  return stream.pipe(gulp.dest('dist'));
+});
+
+gulp.task('js-common', ['collect-js'], () => {
+  var stream = gulp.src(paths.jsCommon)
+  .pipe(collect.pack());
   if (isProd) stream = stream.pipe(uglify());
   return stream.pipe(gulp.dest('dist'));
 });
 
-gulp.task('js-bg', () => {
+gulp.task('js-bg', ['collect-js'], () => {
   var stream = gulp.src(paths.jsBg)
-  .pipe(concat('background/app.js'))
-  .pipe(footer(';define.use("app");'));
+  .pipe(collect.pack('src/background/app.js'))
+  .pipe(concat('background/app.js'));
   if (isProd) stream = stream.pipe(uglify());
   return stream.pipe(gulp.dest('dist'));
 });
 
-gulp.task('js-options', ['templates'], () => {
+gulp.task('js-options', ['templates', 'collect-js'], () => {
   var stream = gulp.src(paths.jsOptions)
   .pipe(cacheObj.replace())
-  .pipe(concat('options/app.js'))
-  .pipe(footer(';define.use("app");'));
+  .pipe(collect.pack('src/options/app.js'))
+  .pipe(concat('options/app.js'));
   if (isProd) stream = stream.pipe(uglify());
   return stream.pipe(gulp.dest('dist'));
 });
 
-gulp.task('js-popup', ['templates'], () => {
+gulp.task('js-popup', ['templates', 'collect-js'], () => {
   var stream = gulp.src(paths.jsPopup)
   .pipe(cacheObj.replace())
-  .pipe(concat('popup/app.js'))
-  .pipe(footer(';define.use("app");'));
+  .pipe(collect.pack('src/popup/app.js'))
+  .pipe(concat('popup/app.js'));
   if (isProd) stream = stream.pipe(uglify());
   return stream.pipe(gulp.dest('dist'))
 });
 
+gulp.task('js', [
+  'js-common',
+  'js-bg',
+  'js-options',
+  'js-popup',
+]);
+
 gulp.task('manifest', () => (
   gulp.src(paths.manifest, {base: 'src'})
   .pipe(replace('__VERSION__', pkg.version))
@@ -145,9 +173,7 @@ gulp.task('svg', () => (
 ));
 
 gulp.task('build', [
-  'js-bg',
-  'js-options',
-  'js-popup',
+  'js',
   'manifest',
   'copy-files',
   'copy-i18n',

+ 3 - 3
package.json

@@ -2,16 +2,17 @@
   "name": "Violentmonkey",
   "version": "2.4.2",
   "scripts": {
-    "clean": "node -e \"require('del')(['dist'])\"",
+    "clean": "gulp clean",
     "build": "gulp build",
     "dev": "gulp watch",
     "i18n": "gulp i18n",
     "lint": "gulp lint",
-    "update": "node scripts/updateLib"
+    "update": "node scripts/update"
   },
   "description": "Violentmonkey",
   "devDependencies": {
     "codemirror": "^5.14.2",
+    "define-commonjs": "^1.0.0",
     "del": "^2.2.0",
     "glob": "^7.0.3",
     "gulp": "^3.9.1",
@@ -19,7 +20,6 @@
     "gulp-cssnano": "^2.1.2",
     "gulp-eslint": "^2.0.0",
     "gulp-filter": "^4.0.0",
-    "gulp-footer": "^1.0.5",
     "gulp-replace": "^0.5.4",
     "gulp-svg-sprite": "^1.2.19",
     "gulp-uglify": "^1.5.3",

+ 9 - 11
scripts/i18n.js

@@ -1,5 +1,3 @@
-'use strict';
-
 const fs = require('fs');
 const path = require('path');
 const gutil = require('gulp-util');
@@ -26,14 +24,14 @@ Locale.prototype.load = function () {
   const file = this.base + '/' + this.path;
   return (
     this.ext
-      ? readFile(file + this.ext)
-      : this.extensions.reduce((promise, ext) => promise.catch(() => (
-        readFile(file + ext)
-        .then(data => {
-          this.ext = ext;
-          return data;
-        })
-      )), Promise.reject())
+    ? readFile(file + this.ext)
+    : this.extensions.reduce((promise, ext) => promise.catch(() => (
+      readFile(file + ext)
+      .then(data => {
+        this.ext = ext;
+        return data;
+      })
+    )), Promise.reject())
   ).then(data => {
     const desc = {};
     if (this.ext === '.json') {
@@ -142,7 +140,7 @@ function extract(options) {
   const patterns = {
     js: ['_\\.i18n\\(\'(\\w+)\'', 1],
     json: ['__MSG_(\\w+)__', 1],
-    html: ['\'(\\w+)\'\\|i18n', 1],
+    html: ['i18n\\(\'(\\w+)\'\\)', 1],
   };
 
   const locales = new Locales(options.prefix, options.base);

+ 30 - 37
scripts/templateCache.js

@@ -1,63 +1,56 @@
-'use strict';
-
+const path = require('path');
 const gutil = require('gulp-util');
-const replace = require('gulp-replace');
 const through = require('through2');
 const minify = require('html-minifier').minify;
 
-module.exports = function templateCache() {
-  const contentTpl = 'cache.put(<%= name %>, <%= content %>);\n';
-  const header = `/* Templates cached with love :) */
-define('templates', function (require, exports, module) {
-  var cache = require('cache');
-`;
-  const footer = `
-});
-`;
-  const contents = [];
+function replacePlugin(contents, objName) {
+  const re = new RegExp(`${objName}\\.get\\('(.*?)'\\)`, 'g');
+  return through.obj(function (file, enc, cb) {
+    const dirname = path.dirname(file.path);
+    file.contents = new Buffer(String(file.contents).replace(re, (m, name) => {
+      const filepath = path.resolve(dirname, name);
+      const item = contents[filepath];
+      if (!item) console.warn(`Cache not found: ${name}`);
+      return `${objName}.get(${item.id})`;
+    }));
+    cb(null, file);
+  });
+}
+
+module.exports = function templateCache(objName) {
+  const contentTpl = `${objName}.put(<%= name %>, <%= content %>);\n`;
+  const header = `\n\n/* Templates cached with love :) */\n`;
+  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.'));
-    contents.push({
-      filename: ('/' + file.relative).replace(/\\/g, '/'),
+    if (file.isStream()) return this.emit('error', new gutil.PluginError('VM-cache', 'Stream is not supported.'));
+    contents[file.path] = {
       content: minify(String(file.contents), {
         removeComments: true,
         collapseWhitespace: true,
         conservativeCollapse: true,
         removeAttributeQuotes: true,
       }),
-    });
+    };
     cb();
   }
 
   function endStream(cb) {
-    contents.sort((a, b) => {
-      if (a.filename < b.filename) return -1;
-      if (a.filename > b.filename) return 1;
-      return 0;
-    });
-    const nameMap = contents.reduce((res, item, i) => {
-      res[item.filename] = i;
-      return res;
-    }, {});
-    this.replace = () => replace(/cache.get\('(.*?)'\)/g, (cache, filename) => {
-      const key = nameMap[filename];
-      if (key == null) console.warn(`Cache key not found: ${filename}`);
-      return `cache.get(${key})`;
-    });
-    const templates = contents.map(item => {
-      return `cache.put(${nameMap[item.filename]}, ${JSON.stringify(item.content)});`;
+    var keys = Object.keys(contents).sort();
+    keys.forEach((key, i) => contents[key].id = i + 1);
+    this.replace = () => replacePlugin(contents, objName);
+    const templates = keys.map(key => {
+      const item = contents[key];
+      return `${objName}.put(${item.id}, ${JSON.stringify(item.content)});`;
     }).join('\n');
     this.push(new gutil.File({
       base: '',
       path: 'template.js',
-      contents: new Buffer(header + templates + footer),
+      contents: new Buffer(header + templates),
     }));
     cb();
   }
 
-  const cacheObj = through.obj(bufferContents, endStream);
-  return cacheObj;
+  return through.obj(bufferContents, endStream);
 };

+ 57 - 0
scripts/update.js

@@ -0,0 +1,57 @@
+const fs = require('fs');
+const path = require('path');
+const glob = require('glob');
+const ncp = require('ncp');
+
+function promisify(func, ...partialArgs) {
+  return function (...args) {
+    return new Promise((resolve, reject) => {
+      func(...args, ...partialArgs, (err, data) => {
+        err ? reject(err) : resolve(data);
+      });
+    });
+  };
+}
+
+const getFiles = (glob => {
+  return function (pattern, cwd='.') {
+    return glob(pattern, {nodir: true, cwd});
+  };
+})(promisify(glob));
+const readdir = promisify(fs.readdir);
+const stat = promisify(fs.stat);
+const copy = (copy => {
+  return function (src, dest) {
+    console.log(`Copy ${src} => ${dest}`);
+    return copy(src, dest);
+  };
+})(promisify(ncp));
+
+const MOD_DIR = 'node_modules';
+const LIB_DIR = 'src/public/lib';
+const mappings = {
+  CodeMirror: 'codemirror',
+  'define.js': 'define-commonjs',
+};
+
+function updateFile(dest, src) {
+  const srcPath = path.join(MOD_DIR, src);
+  return stat(srcPath)
+  .then(res => res.isDirectory() ? path.join(srcPath, dest) : srcPath)
+  .then(srcPath => copy(srcPath, path.join(LIB_DIR, dest)));
+}
+
+function updateDir(dest, src) {
+  return getFiles('**', path.join(LIB_DIR, dest))
+  .then(files => Promise.all(files.map(file => (
+    copy(path.join(MOD_DIR, src, file), path.join(LIB_DIR, dest, file))
+  ))));
+}
+
+function update(dest, src) {
+  return stat(path.join(LIB_DIR, dest))
+  .then(res => res.isFile() ? updateFile(dest, src) : updateDir(dest, src));
+}
+
+Promise.all(Object.keys(mappings).map(key => update(key, mappings[key])))
+.catch(err => console.log(err));

+ 0 - 59
scripts/updateLib.js

@@ -1,59 +0,0 @@
-'use strict';
-
-const fs = require('fs');
-const path = require('path');
-const glob = require('glob');
-const ncp = require('ncp');
-
-const SRC_DIR = 'src/public/lib';
-const aliases = {
-  CodeMirror: 'codemirror',
-};
-
-function getFiles(pattern, cwd) {
-  return new Promise((resolve, reject) => {
-    glob(pattern, {nodir: true, cwd: cwd || '.'}, (err, files) => {
-      err ? reject(err) : resolve(files);
-    });
-  });
-}
-
-function readdir(dir) {
-  return new Promise((resolve, reject) => {
-    fs.readdir(dir, (err, files) => {
-      err ? reject(err) : resolve(files);
-    });
-  });
-}
-
-function copyFile(src, dest) {
-  return new Promise((resolve, reject) => {
-    ncp(src, dest, err => err ? reject(err) : resolve());
-  }).then(() => {
-    console.log(src + ' => ' + dest);
-  });
-}
-
-function update(lib, files) {
-  let alias = aliases[lib];
-  if (typeof alias === 'string') alias = {
-    lib: alias,
-  };
-  alias.lib = alias.lib || lib;
-  const libdir = `node_modules/${alias.lib}`;
-  const srcdir = `${SRC_DIR}/${lib}`
-  return Promise.all(files.map(file => {
-    let aliasFile = alias.files && alias.files[file] || file;
-    if (aliasFile.endsWith('/')) aliasFile += file;
-    const libfile = path.join(libdir, aliasFile);
-    return copyFile(libfile, path.join(srcdir, file));
-  })).catch(function (err) {
-    console.log(err);
-  });
-}
-
-readdir(SRC_DIR).then(data => data.forEach(name => {
-  if (!aliases[name]) return;
-  getFiles('**', `${SRC_DIR}/${name}`)
-  .then(files => update(name, files));
-}));

+ 242 - 247
src/background/app.js

@@ -1,268 +1,263 @@
-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 _ = require('utils/common');
+var VMDB = require('./db');
+var sync = require('./sync');
+var requests = require('./requests');
+var cache = require('./utils/cache');
+var tabsUtils = require('./utils/tabs');
+var scriptUtils = require('./utils/script');
+var _ = require('../common');
 
-  var vmdb = exports.vmdb = new VMDB;
-  var VM_VER = chrome.app.getDetails().version;
+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;
+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;
+    });
+  }
+  var checking;
+  return function () {
+    checking || check();
+  };
+}();
+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,
       });
-    }
-    var checking;
-    return function () {
-      checking || check();
+    });
+  },
+  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 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,
-          });
-        });
-    },
-    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,
-      };
-      if (src.url == src.tab.url)
-        chrome.tabs.sendMessage(src.tab.id, {cmd: 'GetBadge'});
-      return data.isApplied
-        ? vmdb.getScriptsByURL(url).then(function (res) {
-          return Object.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 () {
-          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;
+    if (src.url == src.tab.url)
+      chrome.tabs.sendMessage(src.tab.id, {cmd: 'GetBadge'});
+    return data.isApplied
+      ? vmdb.getScriptsByURL(url).then(function (res) {
+        return Object.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,
       });
-    },
-    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,
-        });
+    });
+  },
+  SetValue: function (data, _src) {
+    return vmdb.setValue(data.uri, data.values)
+    .then(function () {
+      tabsUtils.broadcast({
+        cmd: 'UpdateValues',
+        data: {
+          uri: data.uri,
+          values: data.values,
+        },
       });
-      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
-          }
-        };
-        Promise.resolve(res).then(function (data) {
-          finish({
-            data: data,
-            error: null,
-          });
-        }, function (data) {
-          finish({
-            error: data,
-          });
+    });
+  },
+  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,
         });
-        return true;
-      }
+      _.messenger.post(res);
+      sync.sync();
+      return res.data;
     });
-    setTimeout(autoUpdate, 2e4);
-    sync.init();
-  });
-
-  // Common functions
-
-  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,
+  },
+  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));
     });
-  }
-
-  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 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;
+  },
+};
 
-    return {
-      post: function (data) {
+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 {
-          port && port.postMessage(data);
+          callback(data);
         } catch (e) {
-          port = null;
+          // callback fails if not given in content page
         }
-      },
-      send: function (tabId, data) {
-        chrome.tabs.sendMessage(tabId, data);
-      },
-    };
-  }();
+      };
+      Promise.resolve(res).then(function (data) {
+        finish({
+          data: data,
+          error: null,
+        });
+      }, function (data) {
+        finish({
+          error: data,
+        });
+      });
+      return true;
+    }
+  });
+  setTimeout(autoUpdate, 2e4);
+  sync.init();
+});
+
+// Common functions
+
+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 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);
+  };
+}();
 
-  !function (isApplied) {
-    chrome.browserAction.setIcon({
-      path: {
-        19: '/images/icon19' + (isApplied ? '' : 'w') + '.png',
-        38: '/images/icon38' + (isApplied ? '' : 'w') + '.png'
-      },
+_.messenger = function () {
+  var port;
+  chrome.runtime.onConnect.addListener(function (_port) {
+    port = _port;
+    _port.onDisconnect.addListener(function () {
+      if (port === _port) port = null;
     });
-  }(_.options.get('isApplied'));
+  });
 
-  chrome.notifications.onClicked.addListener(function(id) {
-    id == 'VM-NoGrantWarning' && tabsUtils.create('http://wiki.greasespot.net/@grant');
+  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'
+    },
   });
+}(_.options.get('isApplied'));
 
-  require('sync_dropbox');
-  require('sync_onedrive');
+chrome.notifications.onClicked.addListener(function(id) {
+  id == 'VM-NoGrantWarning' && tabsUtils.create('http://wiki.greasespot.net/@grant');
 });

+ 572 - 574
src/background/db.js

@@ -1,109 +1,148 @@
-define('vmdb', function (require, _exports, module) {
-  var scriptUtils = require('utils/script');
-  var tester = require('utils/tester');
-  var _ = require('utils/common');
+var scriptUtils = require('./utils/script');
+var tester = require('./utils/tester');
+var _ = require('../common');
 
-  function VMDB() {
-    var _this = this;
-    _this.initialized = _this.openDB().then(_this.initPosition.bind(_this));
-    _this.checkUpdate = _this.checkUpdate.bind(_this);
-  }
+function VMDB() {
+  var _this = this;
+  _this.initialized = _this.openDB().then(_this.initPosition.bind(_this));
+  _this.checkUpdate = _this.checkUpdate.bind(_this);
+}
 
-  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.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.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);
+    };
+  });
+};
+
+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.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.getScript = function (id, tx) {
-    tx = tx || this.db.transaction('scripts');
-    var os = tx.objectStore('scripts');
+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) {
-      os.get(id).onsuccess = function (e) {
+      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.values;
+      return result;
+    }, {});
+  });
+};
 
-  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.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.getScriptData = function (id) {
-    return this.getScript(id).then(function (script) {
-      if (!script) return Promise.reject();
-      var data = scriptUtils.getScriptInfo(script);
-      data.code = script.code;
+      data.require = Object.keys(require);
+      data.cache = Object.keys(cache);
       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.getValues = function (uris, tx) {
-    var _this = this;
-    tx = tx || _this.db.transaction('values');
-    var o = tx.objectStore('values');
+  }
+  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) {
@@ -112,544 +151,503 @@ define('vmdb', function (require, _exports, module) {
       });
     })).then(function (data) {
       return data.reduce(function (result, value, i) {
-        if (value) result[uris[i]] = value.values;
+        if (value) result[uris[i]] = value.code;
         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;
-          }
-        });
-        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],
-        };
-      });
+  }
+  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);
-        });
-        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,
-        };
+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;
     });
-  };
-
-  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();
+  }
+  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,
       };
     });
-  };
+  });
+};
 
-  VMDB.prototype.moveScript = function (id, offset) {
-    var tx = this.db.transaction('scripts', 'readwrite');
+VMDB.prototype.removeScript = function (id) {
+  var tx = this.db.transaction('scripts', 'readwrite');
+  return new Promise(function (resolve, _reject) {
     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();
-              };
-            }
-          }
-        };
-      });
-    });
-  };
-
-  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;
-      }, {});
-    });
-  };
+    o.delete(id).onsuccess = function () {
+      resolve();
+    };
+  });
+};
 
-  VMDB.prototype.saveCache = function (url, data, tx) {
-    tx = tx || this.db.transaction('cache', 'readwrite');
-    var o = tx.objectStore('cache');
+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.put({uri: url, data: data}).onsuccess = function () {
-        resolve();
+      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.saveRequire = function (url, data, tx) {
-    tx = tx || this.db.transaction('require', 'readwrite');
-    var o = tx.objectStore('require');
+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.put({uri: url, code: data}).onsuccess = function () {
-        resolve();
+      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;
+    }, {});
+  });
+};
 
-  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;
-      var promise = requests[url];
-      if (!promise) {
-        promise = requests[url] = scriptUtils.fetch(url)
-        .then(function (res) {
-          return _this.saveRequire(url, res.responseText);
-        })
-        .catch(function () {
-          console.error('Error fetching required script: ' + url);
-        })
-        .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.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;
         });
-      }
-      return promise;
+      }).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.fetchRequire = function () {
+  var requests = {};
+  return function (url) {
+    var _this = this;
+    var promise = requests[url];
+    if (!promise) {
+      promise = requests[url] = scriptUtils.fetch(url)
+      .then(function (res) {
+        return _this.saveRequire(url, res.responseText);
+      })
+      .catch(function () {
+        console.error('Error fetching required script: ' + url);
+      })
+      .then(function () {
+        delete requests[url];
+      });
+    }
+    return promise;
+  };
+}();
+
+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.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.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];
+      Object.assign(script.custom, custom);
+      o.put(script).onsuccess = function (_e) {
+        resolve(scriptUtils.getScriptInfo(script));
       };
-    });
-  };
+    };
+  });
+};
 
-  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];
-        Object.assign(script.custom, custom);
-        o.put(script).onsuccess = function (_e) {
-          resolve(scriptUtils.getScriptInfo(script));
+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;});
     });
-  };
+  }
+  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.getExportData = function (ids, withValues) {
-    function getScripts(ids) {
-      var o = tx.objectStore('scripts');
-      return Promise.all(ids.map(function (id) {
+VMDB.prototype.vacuum = function () {
+  function getScripts() {
+    return _this.getScriptsByIndex('position', null, tx).then(function (scripts) {
+      var data = {
+        require: {},
+        cache: {},
+        values: {},
+      };
+      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) {
-            resolve(e.target.result);
+            var result = e.target.result;
+            result.position = i + 1;
+            o.put(result).onsuccess = function () {
+              resolve();
+            };
           };
         });
-      })).then(function (data) {
-        return data.filter(function (x) {return x;});
       });
-    }
-    var _this = this;
-    var tx = _this.db.transaction(['scripts', 'values']);
-    return getScripts(ids).then(function (scripts) {
-      var res = {
-        scripts: scripts,
+    }, 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();
       };
-      return withValues
-        ? _this.getValues(scripts.map(function (script) {
-          return script.uri;
-        }), tx).then(function (values) {
-          res.values = values;
-          return res;
-        }) : res;
     });
-  };
+  }
+  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,
+      };
+    });
+  }).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.vacuum = function () {
-    function getScripts() {
-      return _this.getScriptsByIndex('position', null, tx).then(function (scripts) {
-        var data = {
-          require: {},
-          cache: {},
-          values: {},
+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);
         };
-        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.onload = function () {
+          free();
+          resolve(blob);
         };
-      });
-    }
-    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.onerror = function () {
+          free();
+          reject();
         };
+        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);
-        }),
-      ]);
-    });
-  };
-
-  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);
-      };
     });
-  };
+  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) {
+    Object.assign(res.data, scriptUtils.getScriptInfo(script));
+    return res;
+  });
+};
 
-  VMDB.prototype.parseScript = function (data) {
+VMDB.prototype.checkUpdate = function () {
+  function check(script) {
     var res = {
       cmd: 'update',
       data: {
-        message: data.message == null ? _.i18n('msgUpdated') : data.message || '',
+        id: script.id,
+        checking: true,
       },
     };
-    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) {
-        Object.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');
+    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>';
         _.messenger.post(res);
         return Promise.reject();
-      };
-      var errHandler = function (_xhr) {
+      }
+      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('msgErrorFetchingUpdateInfo');
+        res.data.message = _.i18n('msgErrorFetchingScript');
         _.messenger.post(res);
         return Promise.reject();
-      };
-      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) {
+      });
+    };
+    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;
-          res.data.message = _.i18n('msgErrorFetchingScript');
           _.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) {
-            res.data.checking = false;
-            _.messenger.post(res);
-          });
-        }, function () {
-          delete processes[script.id];
-          //return Promise.reject();
-        });
-        return promise;
-    };
-  }();
+      }, function () {
+        delete processes[script.id];
+        //return Promise.reject();
+      });
+    return promise;
+  };
+}();
 
-  module.exports = VMDB;
-});
+module.exports = VMDB;

+ 0 - 30
src/background/events.js

@@ -1,30 +0,0 @@
-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);
-      });
-    }
-  }
-
-  exports.getEventEmitter = getEventEmitter;
-});

+ 1 - 1
src/background/index.html

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

+ 204 - 205
src/background/requests.js

@@ -1,225 +1,224 @@
-define('requests', function (require, _exports, module) {
-  var tabsUtils = require('utils/tabs');
-  var cache = require('utils/cache');
-  var _ = require('utils/common');
-  var requests = {};
-  var verify = {};
-  var special_headers = [
-    'user-agent',
-    'referer',
-    'origin',
-    'host',
-  ];
-  // var tasks = {};
+var tabsUtils = require('./utils/tabs');
+var cache = require('./utils/cache');
+var _ = require('../common');
 
-  function getRequestId() {
-    var id = _.getUniqId();
-    requests[id] = {
-      id: id,
-      xhr: new XMLHttpRequest,
-    };
-    return id;
-  }
+var requests = {};
+var verify = {};
+var special_headers = [
+  'user-agent',
+  'referer',
+  'origin',
+  'host',
+];
+// var tasks = {};
 
-  function xhrCallbackWrapper(req) {
-    var lastPromise = Promise.resolve();
-    var xhr = req.xhr;
-    return function (evt) {
-      var res = {
-        id: req.id,
-        type: evt.type,
-        resType: xhr.responseType,
-      };
-      var data = res.data = {
-        finalUrl: req.finalUrl,
-        readyState: xhr.readyState,
-        responseHeaders: xhr.getAllResponseHeaders(),
-        status: xhr.status,
-        statusText: xhr.statusText,
-      };
-      try {
-        data.responseText = xhr.responseText;
-      } catch (e) {}
-      if (evt.type === 'loadend') clearRequest(req);
-      lastPromise = lastPromise.then(function () {
-        return new Promise(function (resolve, _reject) {
-          if (xhr.response && xhr.responseType === 'blob') {
-            var reader = new FileReader;
-            reader.onload = function (_e) {
-              data.response = this.result;
-              resolve();
-            };
-            reader.readAsDataURL(xhr.response);
-          } else {
-            // default `null` for blob and '' for text
-            data.response = xhr.response;
-            resolve();
-          }
-        });
-      }).then(function () {
-        req.cb && req.cb(res);
-      });
-    };
-  }
+function getRequestId() {
+  var id = _.getUniqId();
+  requests[id] = {
+    id: id,
+    xhr: new XMLHttpRequest,
+  };
+  return id;
+}
 
-  function httpRequest(details, cb) {
-    var req = requests[details.id];
-    if (!req || req.cb) return;
-    req.cb = cb;
-    var xhr = req.xhr;
+function xhrCallbackWrapper(req) {
+  var lastPromise = Promise.resolve();
+  var xhr = req.xhr;
+  return function (evt) {
+    var res = {
+      id: req.id,
+      type: evt.type,
+      resType: xhr.responseType,
+    };
+    var data = res.data = {
+      finalUrl: req.finalUrl,
+      readyState: xhr.readyState,
+      responseHeaders: xhr.getAllResponseHeaders(),
+      status: xhr.status,
+      statusText: xhr.statusText,
+    };
     try {
-      xhr.open(details.method, details.url, true, details.user, details.password);
-      xhr.setRequestHeader('VM-Verify', details.id);
-      if (details.headers) {
-        for (var k in details.headers) {
-          xhr.setRequestHeader(
-            ~special_headers.indexOf(k.toLowerCase()) ? 'VM-' + k : k,
-            details.headers[k]
-          );
+      data.responseText = xhr.responseText;
+    } catch (e) {}
+    if (evt.type === 'loadend') clearRequest(req);
+    lastPromise = lastPromise.then(function () {
+      return new Promise(function (resolve, _reject) {
+        if (xhr.response && xhr.responseType === 'blob') {
+          var reader = new FileReader;
+          reader.onload = function (_e) {
+            data.response = this.result;
+            resolve();
+          };
+          reader.readAsDataURL(xhr.response);
+        } else {
+          // default `null` for blob and '' for text
+          data.response = xhr.response;
+          resolve();
         }
-      }
-      if (details.responseType) xhr.responseType = 'blob';
-      if (details.overrideMimeType) xhr.overrideMimeType(details.overrideMimeType);
-      var callback = xhrCallbackWrapper(req);
-      [
-        'abort',
-        'error',
-        'load',
-        'loadend',
-        'progress',
-        'readystatechange',
-        'timeout',
-      ].forEach(function (evt) {
-        xhr['on' + evt] = callback;
       });
-      req.finalUrl = details.url;
-      xhr.send(details.data);
-    } catch (e) {
-      console.warn(e);
+    }).then(function () {
+      req.cb && req.cb(res);
+    });
+  };
+}
+
+function httpRequest(details, cb) {
+  var req = requests[details.id];
+  if (!req || req.cb) return;
+  req.cb = cb;
+  var xhr = req.xhr;
+  try {
+    xhr.open(details.method, details.url, true, details.user, details.password);
+    xhr.setRequestHeader('VM-Verify', details.id);
+    if (details.headers) {
+      for (var k in details.headers) {
+        xhr.setRequestHeader(
+          ~special_headers.indexOf(k.toLowerCase()) ? 'VM-' + k : k,
+          details.headers[k]
+        );
+      }
     }
+    if (details.responseType) xhr.responseType = 'blob';
+    if (details.overrideMimeType) xhr.overrideMimeType(details.overrideMimeType);
+    var callback = xhrCallbackWrapper(req);
+    [
+      'abort',
+      'error',
+      'load',
+      'loadend',
+      'progress',
+      'readystatechange',
+      'timeout',
+    ].forEach(function (evt) {
+      xhr['on' + evt] = callback;
+    });
+    req.finalUrl = details.url;
+    xhr.send(details.data);
+  } catch (e) {
+    console.warn(e);
   }
+}
+
+function clearRequest(req) {
+  if (req.coreId) delete verify[req.coreId];
+  delete requests[req.id];
+}
 
-  function clearRequest(req) {
-    if (req.coreId) delete verify[req.coreId];
-    delete requests[req.id];
+function abortRequest(id) {
+  var req = requests[id];
+  if (req) {
+    req.xhr.abort();
+    clearRequest(req);
   }
+}
 
-  function abortRequest(id) {
-    var req = requests[id];
-    if (req) {
-      req.xhr.abort();
-      clearRequest(req);
-    }
+// Watch URL redirects
+chrome.webRequest.onBeforeRedirect.addListener(function (details) {
+  var reqId = verify[details.requestId];
+  if (reqId) {
+    var req = requests[reqId];
+    if (req) req.finalUrl = details.redirectUrl;
   }
+}, {
+  urls: ['<all_urls>'],
+  types: ['xmlhttprequest'],
+});
 
-  // Watch URL redirects
-  chrome.webRequest.onBeforeRedirect.addListener(function (details) {
-    var reqId = verify[details.requestId];
-    if (reqId) {
-      var req = requests[reqId];
-      if (req) req.finalUrl = details.redirectUrl;
+// Modifications on headers
+chrome.webRequest.onBeforeSendHeaders.addListener(function (details) {
+  var headers = details.requestHeaders;
+  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-') {
+      vmHeaders[header.name.slice(3)] = header.value;
+    } else {
+      newHeaders.push(header);
     }
-  }, {
-    urls: ['<all_urls>'],
-    types: ['xmlhttprequest'],
   });
-
-  // Modifications on headers
-  chrome.webRequest.onBeforeSendHeaders.addListener(function (details) {
-    var headers = details.requestHeaders;
-    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-') {
-        vmHeaders[header.name.slice(3)] = header.value;
-      } else {
-        newHeaders.push(header);
-      }
-    });
-    var reqId = vmHeaders['Verify'];
-    if (reqId) {
-      var req = requests[reqId];
-      if (req) {
-        delete vmHeaders['Verify'];
-        verify[details.requestId] = reqId;
-        req.coreId = details.requestId;
-        for (var i in vmHeaders)
-          if (~special_headers.indexOf(i.toLowerCase()))
-            newHeaders.push({name: i, value: vmHeaders[i]});
-      }
+  var reqId = vmHeaders['Verify'];
+  if (reqId) {
+    var req = requests[reqId];
+    if (req) {
+      delete vmHeaders['Verify'];
+      verify[details.requestId] = reqId;
+      req.coreId = details.requestId;
+      for (var i in vmHeaders)
+        if (~special_headers.indexOf(i.toLowerCase()))
+        newHeaders.push({name: i, value: vmHeaders[i]});
     }
-    return {requestHeaders: newHeaders};
-  }, {
-    urls: ['<all_urls>'],
-    types: ['xmlhttprequest'],
-  }, ['blocking', 'requestHeaders']);
+  }
+  return {requestHeaders: newHeaders};
+}, {
+  urls: ['<all_urls>'],
+  types: ['xmlhttprequest'],
+}, ['blocking', 'requestHeaders']);
 
-  // tasks are not necessary now, turned off
-  // Stop redirects
-  // chrome.webRequest.onHeadersReceived.addListener(function (details) {
-  //   var task = tasks[details.requestId];
-  //   if (task) {
-  //     delete tasks[details.requestId];
-  //     if (task === 'Get-Location' && _.includes([301, 302, 303], details.statusCode)) {
-  //       var locationHeader = details.responseHeaders.find(function (header) {
-  //         return header.name.toLowerCase() === 'location';
-  //       });
-  //       return {
-  //         redirectUrl: 'data:text/plain;charset=utf-8,' + (locationHeader && locationHeader.value || ''),
-  //       };
-  //     }
-  //   }
-  // }, {
-  //   urls: ['<all_urls>'],
-  //   types: ['xmlhttprequest'],
-  // }, ['blocking', 'responseHeaders']);
-  // chrome.webRequest.onCompleted.addListener(function (details) {
-  //   delete tasks[details.requestId];
-  // }, {
-  //   urls: ['<all_urls>'],
-  //   types: ['xmlhttprequest'],
-  // });
-  // chrome.webRequest.onErrorOccurred.addListener(function (details) {
-  //   delete tasks[details.requestId];
-  // }, {
-  //   urls: ['<all_urls>'],
-  //   types: ['xmlhttprequest'],
-  // });
+// tasks are not necessary now, turned off
+// Stop redirects
+// chrome.webRequest.onHeadersReceived.addListener(function (details) {
+//   var task = tasks[details.requestId];
+//   if (task) {
+//     delete tasks[details.requestId];
+//     if (task === 'Get-Location' && _.includes([301, 302, 303], details.statusCode)) {
+//       var locationHeader = details.responseHeaders.find(function (header) {
+//         return header.name.toLowerCase() === 'location';
+//       });
+//       return {
+//         redirectUrl: 'data:text/plain;charset=utf-8,' + (locationHeader && locationHeader.value || ''),
+//       };
+//     }
+//   }
+// }, {
+//   urls: ['<all_urls>'],
+//   types: ['xmlhttprequest'],
+// }, ['blocking', 'responseHeaders']);
+// chrome.webRequest.onCompleted.addListener(function (details) {
+//   delete tasks[details.requestId];
+// }, {
+//   urls: ['<all_urls>'],
+//   types: ['xmlhttprequest'],
+// });
+// chrome.webRequest.onErrorOccurred.addListener(function (details) {
+//   delete tasks[details.requestId];
+// }, {
+//   urls: ['<all_urls>'],
+//   types: ['xmlhttprequest'],
+// });
 
-  chrome.webRequest.onBeforeRequest.addListener(function (req) {
-    // onBeforeRequest is fired for local files too
-    if (/\.user\.js([\?#]|$)/.test(req.url)) {
-      // {cancel: true} will redirect to a blocked view
-      var noredirect = {redirectUrl: 'javascript:history.back()'};
-      var x = new XMLHttpRequest();
-      x.open('GET', req.url, false);
-      try {
-        x.send();
-      } catch (e) {
-        // Request is redirected
-        return;
-      }
-      if ((!x.status || x.status == 200) && !/^\s*</.test(x.responseText)) {
-        cache.set(req.url, x.responseText);
-        var url = chrome.extension.getURL('/options/index.html') + '#confirm/' + encodeURIComponent(req.url);
-        if (req.tabId < 0) tabsUtils.create(url);
-        else tabsUtils.get(req.tabId).then(function (t) {
-          tabsUtils.create(url + '/' + encodeURIComponent(t.url));
-        });
-        return noredirect;
-      }
+chrome.webRequest.onBeforeRequest.addListener(function (req) {
+  // onBeforeRequest is fired for local files too
+  if (/\.user\.js([\?#]|$)/.test(req.url)) {
+    // {cancel: true} will redirect to a blocked view
+    var noredirect = {redirectUrl: 'javascript:history.back()'};
+    var x = new XMLHttpRequest();
+    x.open('GET', req.url, false);
+    try {
+      x.send();
+    } catch (e) {
+      // Request is redirected
+      return;
     }
-  }, {
-    urls: ['<all_urls>'],
-    types: ['main_frame'],
-  }, ['blocking', 'requestBody']);
+    if ((!x.status || x.status == 200) && !/^\s*</.test(x.responseText)) {
+      cache.set(req.url, x.responseText);
+      var url = chrome.extension.getURL('/options/index.html') + '#confirm/' + encodeURIComponent(req.url);
+      if (req.tabId < 0) tabsUtils.create(url);
+      else tabsUtils.get(req.tabId).then(function (t) {
+        tabsUtils.create(url + '/' + encodeURIComponent(t.url));
+      });
+      return noredirect;
+    }
+  }
+}, {
+  urls: ['<all_urls>'],
+  types: ['main_frame'],
+}, ['blocking', 'requestBody']);
 
-  module.exports = {
-    getRequestId: getRequestId,
-    abortRequest: abortRequest,
-    httpRequest: httpRequest,
-  };
-});
+module.exports = {
+  getRequestId: getRequestId,
+  abortRequest: abortRequest,
+  httpRequest: httpRequest,
+};

+ 113 - 115
src/background/sync/dropbox.js

@@ -1,121 +1,119 @@
-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',
-  };
+var sync = require('.');
+var tabsUtils = require('../utils/tabs');
+var searchUtils = require('../utils/search');
+var config = {
+  client_id: 'f0q12zup2uys5w8',
+  redirect_uri: 'https://violentmonkey.github.io/auth_dropbox.html',
+};
 
-  function authenticate() {
-    var params = {
-      response_type: 'token',
-      client_id: config.client_id,
-      redirect_uri: config.redirect_uri,
-    };
-    var url = 'https://www.dropbox.com/oauth2/authorize';
-    var qs = searchUtils.dump(params);
-    url += '?' + qs;
-    tabsUtils.create(url);
-  }
-  function checkAuthenticate(url) {
-    var redirect_uri = config.redirect_uri + '#';
-    if (url.slice(0, redirect_uri.length) === redirect_uri) {
-      authorized(url.slice(redirect_uri.length));
-      dropbox.checkSync();
-      return true;
-    }
-  }
-  function authorized(raw) {
-    var data = searchUtils.load(raw);
-    if (data.access_token) {
-      dropbox.config.set({
-        uid: data.uid,
-        token: data.access_token,
-      });
-    }
+function authenticate() {
+  var params = {
+    response_type: 'token',
+    client_id: config.client_id,
+    redirect_uri: config.redirect_uri,
+  };
+  var url = 'https://www.dropbox.com/oauth2/authorize';
+  var qs = searchUtils.dump(params);
+  url += '?' + qs;
+  tabsUtils.create(url);
+}
+function checkAuthenticate(url) {
+  var redirect_uri = config.redirect_uri + '#';
+  if (url.slice(0, redirect_uri.length) === redirect_uri) {
+    authorized(url.slice(redirect_uri.length));
+    dropbox.checkSync();
+    return true;
   }
-  function normalize(item) {
-    return {
-      size: item.size,
-      uri: sync.utils.getURI(item.name),
-      modified: new Date(item.server_modified).getTime(),
-      //is_deleted: item.is_deleted,
-    };
+}
+function authorized(raw) {
+  var data = searchUtils.load(raw);
+  if (data.access_token) {
+    dropbox.config.set({
+      uid: data.uid,
+      token: data.access_token,
+    });
   }
+}
+function normalize(item) {
+  return {
+    size: item.size,
+    uri: sync.utils.getURI(item.name),
+    modified: new Date(item.server_modified).getTime(),
+    //is_deleted: item.is_deleted,
+  };
+}
 
-  var Dropbox = sync.BaseService.extend({
-    name: 'dropbox',
-    displayName: 'Dropbox',
-    user: function () {
-      return this.request({
-        method: 'POST',
-        url: 'https://api.dropboxapi.com/2/users/get_current_account',
-      });
-    },
-    getMeta: function () {
-      return sync.BaseService.prototype.getMeta.call(this)
-      .catch(function (res) {
-        if (res.status === 409) return {};
-        throw res;
-      });
-    },
-    list: function () {
-      var _this = this;
-      return _this.request({
-        method: 'POST',
-        url: 'https://api.dropboxapi.com/2/files/list_folder',
-        body: {
-          path: '',
-        },
-      }).then(function (text) {
-        return JSON.parse(text);
-      }).then(function (data) {
-        return data.entries.filter(function (item) {
-          return item['.tag'] === 'file' && sync.utils.isScriptFile(item.name);
-        }).map(normalize);
-      });
-    },
-    get: function (path) {
-      return this.request({
-        method: 'POST',
-        url: 'https://content.dropboxapi.com/2/files/download',
-        headers: {
-          'Dropbox-API-Arg': JSON.stringify({
-            path: '/' + path,
-          }),
-        },
-      });
-    },
-    put: function (path, data) {
-      return this.request({
-        method: 'POST',
-        url: 'https://content.dropboxapi.com/2/files/upload',
-        headers: {
-          'Dropbox-API-Arg': JSON.stringify({
-            path: '/' + path,
-            mode: 'overwrite',
-          }),
-          'Content-Type': 'application/octet-stream',
-        },
-        body: data,
-      }).then(function (text) {
-        return JSON.parse(text);
-      }).then(normalize);
-    },
-    remove: function (path) {
-      return this.request({
-        method: 'POST',
-        url: 'https://api.dropboxapi.com/2/files/delete',
-        body: {
+var Dropbox = sync.BaseService.extend({
+  name: 'dropbox',
+  displayName: 'Dropbox',
+  user: function () {
+    return this.request({
+      method: 'POST',
+      url: 'https://api.dropboxapi.com/2/users/get_current_account',
+    });
+  },
+  getMeta: function () {
+    return sync.BaseService.prototype.getMeta.call(this)
+    .catch(function (res) {
+      if (res.status === 409) return {};
+      throw res;
+    });
+  },
+  list: function () {
+    var _this = this;
+    return _this.request({
+      method: 'POST',
+      url: 'https://api.dropboxapi.com/2/files/list_folder',
+      body: {
+        path: '',
+      },
+    }).then(function (text) {
+      return JSON.parse(text);
+    }).then(function (data) {
+      return data.entries.filter(function (item) {
+        return item['.tag'] === 'file' && sync.utils.isScriptFile(item.name);
+      }).map(normalize);
+    });
+  },
+  get: function (path) {
+    return this.request({
+      method: 'POST',
+      url: 'https://content.dropboxapi.com/2/files/download',
+      headers: {
+        'Dropbox-API-Arg': JSON.stringify({
+          path: '/' + path,
+        }),
+      },
+    });
+  },
+  put: function (path, data) {
+    return this.request({
+      method: 'POST',
+      url: 'https://content.dropboxapi.com/2/files/upload',
+      headers: {
+        'Dropbox-API-Arg': JSON.stringify({
           path: '/' + path,
-        },
-      }).then(function (text) {
-        return JSON.parse(text);
-      }).then(normalize);
-    },
-    authenticate: authenticate,
-    checkAuthenticate: checkAuthenticate,
-  });
-  var dropbox = sync.service('dropbox', Dropbox);
+          mode: 'overwrite',
+        }),
+        'Content-Type': 'application/octet-stream',
+      },
+      body: data,
+    }).then(function (text) {
+      return JSON.parse(text);
+    }).then(normalize);
+  },
+  remove: function (path) {
+    return this.request({
+      method: 'POST',
+      url: 'https://api.dropboxapi.com/2/files/delete',
+      body: {
+        path: '/' + path,
+      },
+    }).then(function (text) {
+      return JSON.parse(text);
+    }).then(normalize);
+  },
+  authenticate: authenticate,
+  checkAuthenticate: checkAuthenticate,
 });
+var dropbox = sync.service('dropbox', Dropbox);

+ 471 - 467
src/background/sync/index.js

@@ -1,188 +1,193 @@
 /* eslint-disable no-console */
-define('sync', function (require, _exports, module) {
-  var events = require('events');
-  var app = require('app');
-  var tabs = require('utils/tabs');
-  var _ = require('utils/common');
+var events = require('../utils/events');
+var app = require('../app');
+var tabs = require('../utils/tabs');
+var _ = require('../../common');
 
-  var services = [];
-  var servicesReady = [];
-  var inited;
-  var current = Promise.resolve();
-  var autoSync = _.debounce(function () {
-    sync();
-  }, 60 * 60 * 1000);
+setTimeout(function () {
+  // import sync modules
+  require('./dropbox');
+  require('./onedrive');
+});
 
-  function ServiceConfig(name) {
-    this.prefix = name;
-    this.load();
-  }
-  ServiceConfig.prototype.get = function (key, def) {
-    var val = this.data[key];
-    if (val == null) val = def;
-    return val;
-  };
-  ServiceConfig.prototype.set = function (key, val) {
-    if (typeof key === 'object') {
-      if (arguments.length === 1) {
-        Object.assign(this.data, key);
-        this.dump();
-      }
-    } else {
-      // val may be an object, so equal test does not work
-      this.data[key] = val;
+var services = [];
+var servicesReady = [];
+var inited;
+var current = Promise.resolve();
+var autoSync = _.debounce(function () {
+  sync();
+}, 60 * 60 * 1000);
+
+function ServiceConfig(name) {
+  this.prefix = name;
+  this.load();
+}
+ServiceConfig.prototype.get = function (key, def) {
+  var val = this.data[key];
+  if (val == null) val = def;
+  return val;
+};
+ServiceConfig.prototype.set = function (key, val) {
+  if (typeof key === 'object') {
+    if (arguments.length === 1) {
+      Object.assign(this.data, key);
       this.dump();
     }
-  };
-  ServiceConfig.prototype.clear = function () {
-    this.data = {};
+  } else {
+    // val may be an object, so equal test does not work
+    this.data[key] = val;
     this.dump();
-  };
-  ServiceConfig.prototype.capitalize = function (string) {
-    return string[0].toUpperCase() + string.slice(1);
-  };
-  ServiceConfig.prototype.getOption = function (key, def) {
-    key = this.capitalize(key);
-    return _.options.get(this.prefix + key, def);
-  };
-  ServiceConfig.prototype.setOption = function (key, val) {
-    key = this.capitalize(key);
-    return _.options.set(this.prefix + key, val);
-  };
-  ServiceConfig.prototype.load = function () {
-    this.data = _.options.get(this.prefix, {});
-  };
-  ServiceConfig.prototype.dump = function () {
-    _.options.set(this.prefix, this.data);
-  };
-
-  function serviceState(validStates, initialState, onChange) {
-    var state = initialState || validStates[0];
-    return {
-      get: function () {return state;},
-      set: function (_state) {
-        if (~validStates.indexOf(_state)) {
-          state = _state;
-          onChange && onChange();
-        } else {
-          console.warn('Invalid state:', _state);
-        }
-        return state;
-      },
-      is: function (states) {
-        if (!Array.isArray(states)) states = [states];
-        return ~states.indexOf(state);
-      },
-    };
   }
-  function service(name, Service) {
-    var service;
-    if (typeof name === 'function') {
-      Service = name;
-      name = Service.prototype.name || Service.name;
-    }
-    if (Service) {
-      // initialize
-      service = new Service(name);
-      setTimeout(function () {
-        services.push(service);
-        inited && service.checkSync();
-      });
-    } else {
-      // get existent instance
-      for (var i = services.length; i --; ) {
-        if (services[i].name === name) break;
+};
+ServiceConfig.prototype.clear = function () {
+  this.data = {};
+  this.dump();
+};
+ServiceConfig.prototype.capitalize = function (string) {
+  return string[0].toUpperCase() + string.slice(1);
+};
+ServiceConfig.prototype.getOption = function (key, def) {
+  key = this.capitalize(key);
+  return _.options.get(this.prefix + key, def);
+};
+ServiceConfig.prototype.setOption = function (key, val) {
+  key = this.capitalize(key);
+  return _.options.set(this.prefix + key, val);
+};
+ServiceConfig.prototype.load = function () {
+  this.data = _.options.get(this.prefix, {});
+};
+ServiceConfig.prototype.dump = function () {
+  _.options.set(this.prefix, this.data);
+};
+
+function serviceState(validStates, initialState, onChange) {
+  var state = initialState || validStates[0];
+  return {
+    get: function () {return state;},
+    set: function (_state) {
+      if (~validStates.indexOf(_state)) {
+        state = _state;
+        onChange && onChange();
+      } else {
+        console.warn('Invalid state:', _state);
       }
-      // i may be -1 if not found
-      service = services[i];
-    }
-    return service;
-  }
-  function getStates() {
-    return services.map(function (service) {
-      return {
-        name: service.name,
-        displayName: service.displayName,
-        authState: service.authState.get(),
-        syncState: service.syncState.get(),
-        lastSync: service.config.get('meta', {}).lastSync,
-        progress: service.progress,
-      };
-    });
-  }
-  function syncOne(service) {
-    if (service.syncState.is(['ready', 'syncing'])) return;
-    if (service.authState.is(['idle', 'error'])) return service.checkSync();
-    if (service.authState.is('authorized')) return service.startSync();
-  }
-  function syncAll() {
-    return Promise.all(servicesReady.filter(function (service) {
-      return service.config.getOption('enabled') && !service.syncState.is(['ready', 'syncing']);
-    }).map(function (service) {
-      return service.startSync();
-    }));
-  }
-  function sync(service) {
-    return (service ? Promise.resolve(syncOne(service)) : syncAll())
-    .then(autoSync);
+      return state;
+    },
+    is: function (states) {
+      if (!Array.isArray(states)) states = [states];
+      return ~states.indexOf(state);
+    },
+  };
+}
+function service(name, Service) {
+  var service;
+  if (typeof name === 'function') {
+    Service = name;
+    name = Service.prototype.name || Service.name;
   }
-  function init() {
-    inited = true;
-    services.forEach(function (service) {
-      service.checkSync();
+  if (Service) {
+    // initialize
+    service = new Service(name);
+    setTimeout(function () {
+      services.push(service);
+      inited && service.checkSync();
     });
-    sync();
-  }
-  function getFilename(uri) {
-    return 'vm-' + encodeURIComponent(uri);
-  }
-  function getURI(name) {
-    return decodeURIComponent(name.slice(3));
-  }
-  function isScriptFile(name) {
-    return /^vm-/.test(name);
+  } else {
+    // get existent instance
+    for (var i = services.length; i --; ) {
+      if (services[i].name === name) break;
+    }
+    // i may be -1 if not found
+    service = services[i];
   }
+  return service;
+}
+function getStates() {
+  return services.map(function (service) {
+    return {
+      name: service.name,
+      displayName: service.displayName,
+      authState: service.authState.get(),
+      syncState: service.syncState.get(),
+      lastSync: service.config.get('meta', {}).lastSync,
+      progress: service.progress,
+    };
+  });
+}
+function syncOne(service) {
+  if (service.syncState.is(['ready', 'syncing'])) return;
+  if (service.authState.is(['idle', 'error'])) return service.checkSync();
+  if (service.authState.is('authorized')) return service.startSync();
+}
+function syncAll() {
+  return Promise.all(servicesReady.filter(function (service) {
+    return service.config.getOption('enabled') && !service.syncState.is(['ready', 'syncing']);
+  }).map(function (service) {
+    return service.startSync();
+  }));
+}
+function sync(service) {
+  return (service ? Promise.resolve(syncOne(service)) : syncAll())
+  .then(autoSync);
+}
+function init() {
+  inited = true;
+  services.forEach(function (service) {
+    service.checkSync();
+  });
+  sync();
+}
+function getFilename(uri) {
+  return 'vm-' + encodeURIComponent(uri);
+}
+function getURI(name) {
+  return decodeURIComponent(name.slice(3));
+}
+function isScriptFile(name) {
+  return /^vm-/.test(name);
+}
 
-  function serviceFactory(base, options) {
-    var Service = function () {
-      this.initialize.apply(this, arguments);
+function serviceFactory(base, options) {
+  var Service = function () {
+    this.initialize.apply(this, arguments);
+  };
+  Service.prototype = Object.assign(Object.create(base), options);
+  Service.extend = extendService;
+  return Service;
+}
+function extendService(options) {
+  return serviceFactory(this.prototype, options);
+}
+var BaseService = serviceFactory({
+  name: 'base',
+  displayName: 'BaseService',
+  delayTime: 1000,
+  urlPrefix: '',
+  metaFile: 'Violentmonkey',
+  delay: function (time) {
+    if (time == null) time = this.delayTime;
+    return new Promise(function (resolve, _reject) {
+      setTimeout(resolve, time);
+    });
+  },
+  initialize: function (name) {
+    var _this = this;
+    _this.onStateChange = _.debounce(_this.onStateChange.bind(_this));
+    if (name) _this.name = name;
+    _this.progress = {
+      finished: 0,
+      total: 0,
     };
-    Service.prototype = Object.assign(Object.create(base), options);
-    Service.extend = extendService;
-    return Service;
-  }
-  function extendService(options) {
-    return serviceFactory(this.prototype, options);
-  }
-  var BaseService = serviceFactory({
-    name: 'base',
-    displayName: 'BaseService',
-    delayTime: 1000,
-    urlPrefix: '',
-    metaFile: 'Violentmonkey',
-    delay: function (time) {
-      if (time == null) time = this.delayTime;
-      return new Promise(function (resolve, _reject) {
-        setTimeout(resolve, time);
-      });
-    },
-    initialize: function (name) {
-      var _this = this;
-      _this.onStateChange = _.debounce(_this.onStateChange.bind(_this));
-      if (name) _this.name = name;
-      _this.progress = {
-        finished: 0,
-        total: 0,
-      };
-      _this.config = new ServiceConfig(_this.name);
-      _this.authState = serviceState([
-        'idle',
-        'initializing',
-        'authorizing',  // in case some services require asynchronous requests to get access_tokens
-        'authorized',
-        'unauthorized',
-        'error',
-      ], null, _this.onStateChange),
+    _this.config = new ServiceConfig(_this.name);
+    _this.authState = serviceState([
+      'idle',
+      'initializing',
+      'authorizing',  // in case some services require asynchronous requests to get access_tokens
+      'authorized',
+      'unauthorized',
+      'error',
+    ], null, _this.onStateChange),
       _this.syncState = serviceState([
         'idle',
         'ready',
@@ -190,318 +195,317 @@ define('sync', function (require, _exports, module) {
         'error',
       ], null, _this.onStateChange),
       _this.initHeaders();
-      _this.events = events.getEventEmitter();
-      _this.lastFetch = Promise.resolve();
-      _this.startSync = _this.syncFactory();
-    },
-    on: function () {
-      return this.events.on.apply(null, arguments);
-    },
-    off: function () {
-      return this.events.off.apply(null, arguments);
-    },
-    fire: function () {
-      return this.events.fire.apply(null, arguments);
-    },
-    onStateChange: function () {
-      _.messenger.post({
-        cmd: 'sync',
-        data: getStates(),
-      });
-    },
-    syncFactory: function () {
-      var _this = this;
-      var promise, debouncedResolve;
-      function shouldSync() {
-        return _this.authState.is('authorized') && _this.config.getOption('enabled');
-      }
-      function init() {
-        if (!shouldSync()) return Promise.resolve();
-        console.log('Ready to sync:', _this.displayName);
-        _this.syncState.set('ready');
-        promise = current = current.then(function () {
-          return new Promise(function (resolve, _reject) {
-            debouncedResolve = _.debounce(resolve, 10 * 1000);
-            debouncedResolve();
-          });
-        }).then(function () {
-          if (shouldSync()) {
-            return _this.sync();
-          }
-          _this.syncState.set('idle');
-        }).then(function () {
-          promise = debouncedResolve = null;
+    _this.events = events.getEventEmitter();
+    _this.lastFetch = Promise.resolve();
+    _this.startSync = _this.syncFactory();
+  },
+  on: function () {
+    return this.events.on.apply(null, arguments);
+  },
+  off: function () {
+    return this.events.off.apply(null, arguments);
+  },
+  fire: function () {
+    return this.events.fire.apply(null, arguments);
+  },
+  onStateChange: function () {
+    _.messenger.post({
+      cmd: 'sync',
+      data: getStates(),
+    });
+  },
+  syncFactory: function () {
+    var _this = this;
+    var promise, debouncedResolve;
+    function shouldSync() {
+      return _this.authState.is('authorized') && _this.config.getOption('enabled');
+    }
+    function init() {
+      if (!shouldSync()) return Promise.resolve();
+      console.log('Ready to sync:', _this.displayName);
+      _this.syncState.set('ready');
+      promise = current = current.then(function () {
+        return new Promise(function (resolve, _reject) {
+          debouncedResolve = _.debounce(resolve, 10 * 1000);
+          debouncedResolve();
         });
-      }
-      return function () {
-        if (!promise) init();
-        debouncedResolve && debouncedResolve();
-        return promise;
-      };
-    },
-    prepare: function () {
-      var _this = this;
-      _this.authState.set('initializing');
-      var token = _this.token = _this.config.get('token');
-      _this.initHeaders();
-      return (token ? Promise.resolve(_this.user()) : Promise.reject())
-      .then(function () {
-        _this.authState.set('authorized');
-      }, function (err) {
-        if (err) {
-          if (err.status === 401) {
-            _this.config.clear();
-            _this.authState.set('unauthorized');
-          } else {
-            console.error(err);
-            _this.authState.set('error');
-          }
-          _this.syncState.set('idle');
-          // _this.config.setOption('enabled', false);
-        } else {
-          _this.authState.set('unauthorized');
+      }).then(function () {
+        if (shouldSync()) {
+          return _this.sync();
         }
-        throw err;
-      });
-    },
-    checkSync: function () {
-      var _this = this;
-      return _this.prepare()
-      .then(function () {
-        servicesReady.push(_this);
-        return _this.startSync();
-      }, function () {
-        var i = servicesReady.indexOf(_this);
-        if (~i) servicesReady.splice(i, 1);
-      });
-    },
-    user: _.noop,
-    getMeta: function () {
-      var _this = this;
-      return _this.get(_this.metaFile)
-      .then(function (data) {
-        return JSON.parse(data);
+        _this.syncState.set('idle');
+      }).then(function () {
+        promise = debouncedResolve = null;
       });
-    },
-    initHeaders: function () {
-      var headers = this.headers = {};
-      var token = this.token;
-      if (token) headers.Authorization = 'Bearer ' + token;
-    },
-    request: function (options) {
-      var _this = this;
-      var progress = _this.progress;
-      var lastFetch;
-      if (options.noDelay) {
-        lastFetch = Promise.resolve();
+    }
+    return function () {
+      if (!promise) init();
+      debouncedResolve && debouncedResolve();
+      return promise;
+    };
+  },
+  prepare: function () {
+    var _this = this;
+    _this.authState.set('initializing');
+    var token = _this.token = _this.config.get('token');
+    _this.initHeaders();
+    return (token ? Promise.resolve(_this.user()) : Promise.reject())
+    .then(function () {
+      _this.authState.set('authorized');
+    }, function (err) {
+      if (err) {
+        if (err.status === 401) {
+          _this.config.clear();
+          _this.authState.set('unauthorized');
+        } else {
+          console.error(err);
+          _this.authState.set('error');
+        }
+        _this.syncState.set('idle');
+        // _this.config.setOption('enabled', false);
       } else {
-        lastFetch = _this.lastFetch;
-        _this.lastFetch = lastFetch.then(function () {
-          return _this.delay();
-        });
+        _this.authState.set('unauthorized');
       }
-      progress.total ++;
-      _this.onStateChange();
-      return lastFetch.then(function () {
-        return new Promise(function (resolve, reject) {
-          var xhr = new XMLHttpRequest;
-          var prefix = options.prefix;
-          if (prefix == null) prefix = _this.urlPrefix;
-          xhr.open(options.method || 'GET', prefix + options.url, true);
-          var headers = Object.assign({}, _this.headers, options.headers);
-          if (options.body && typeof options.body === 'object') {
-            headers['Content-Type'] = 'application/json';
-            options.body = JSON.stringify(options.body);
-          }
-          for (var k in headers) {
-            var v = headers[k];
-            v && xhr.setRequestHeader(k, v);
+      throw err;
+    });
+  },
+  checkSync: function () {
+    var _this = this;
+    return _this.prepare()
+    .then(function () {
+      servicesReady.push(_this);
+      return _this.startSync();
+    }, function () {
+      var i = servicesReady.indexOf(_this);
+      if (~i) servicesReady.splice(i, 1);
+    });
+  },
+  user: _.noop,
+  getMeta: function () {
+    var _this = this;
+    return _this.get(_this.metaFile)
+    .then(function (data) {
+      return JSON.parse(data);
+    });
+  },
+  initHeaders: function () {
+    var headers = this.headers = {};
+    var token = this.token;
+    if (token) headers.Authorization = 'Bearer ' + token;
+  },
+  request: function (options) {
+    var _this = this;
+    var progress = _this.progress;
+    var lastFetch;
+    if (options.noDelay) {
+      lastFetch = Promise.resolve();
+    } else {
+      lastFetch = _this.lastFetch;
+      _this.lastFetch = lastFetch.then(function () {
+        return _this.delay();
+      });
+    }
+    progress.total ++;
+    _this.onStateChange();
+    return lastFetch.then(function () {
+      return new Promise(function (resolve, reject) {
+        var xhr = new XMLHttpRequest;
+        var prefix = options.prefix;
+        if (prefix == null) prefix = _this.urlPrefix;
+        xhr.open(options.method || 'GET', prefix + options.url, true);
+        var headers = Object.assign({}, _this.headers, options.headers);
+        if (options.body && typeof options.body === 'object') {
+          headers['Content-Type'] = 'application/json';
+          options.body = JSON.stringify(options.body);
+        }
+        for (var k in headers) {
+          var v = headers[k];
+          v && xhr.setRequestHeader(k, v);
+        }
+        xhr.onloadend = function () {
+          progress.finished ++;
+          _this.onStateChange();
+          if (xhr.status === 503) {
+            // TODO Too Many Requests
           }
-          xhr.onloadend = function () {
-            progress.finished ++;
-            _this.onStateChange();
-            if (xhr.status === 503) {
-              // TODO Too Many Requests
-            }
-            xhr.status > 300 ? requestError() : resolve(xhr.responseText);
-          };
-          xhr.send(options.body);
+          xhr.status > 300 ? requestError() : resolve(xhr.responseText);
+        };
+        xhr.send(options.body);
 
-          function requestError() {
-            reject({
-              url: options.url,
-              status: xhr.status,
-              xhr: xhr,
-            });
-          }
-        });
+        function requestError() {
+          reject({
+            url: options.url,
+            status: xhr.status,
+            xhr: xhr,
+          });
+        }
       });
-    },
-    sync: function () {
-      var _this = this;
-      _this.progress = {
-        finished: 0,
-        total: 0,
+    });
+  },
+  sync: function () {
+    var _this = this;
+    _this.progress = {
+      finished: 0,
+      total: 0,
+    };
+    _this.syncState.set('syncing');
+    return _this.getMeta()
+    .then(function (meta) {
+      return Promise.all([
+        meta,
+        _this.list(),
+        app.vmdb.getScriptsByIndex('position'),
+      ]);
+    }).then(function (res) {
+      var remote = {
+        meta: res[0],
+        data: res[1],
       };
-      _this.syncState.set('syncing');
-      return _this.getMeta()
-      .then(function (meta) {
-        return Promise.all([
-          meta,
-          _this.list(),
-          app.vmdb.getScriptsByIndex('position'),
-        ]);
-      }).then(function (res) {
-        var remote = {
-          meta: res[0],
-          data: res[1],
-        };
-        var local = {
-          meta: _this.config.get('meta', {}),
-          data: res[2],
-        };
-        var firstSync = !local.meta.timestamp;
-        var outdated = !local.meta.timestamp || remote.meta.timestamp > local.meta.timestamp;
-        console.log('First sync:', firstSync);
-        console.log('Outdated:', outdated, '(', 'local:', local.meta.timestamp, 'remote:', remote.meta.timestamp, ')');
-        var map = {};
-        var getRemote = [];
-        var putRemote = [];
-        var delRemote = [];
-        var delLocal = [];
-        remote.data.forEach(function (item) {
-          map[item.uri] = item;
-        });
-        local.data.forEach(function (item) {
-          var remoteItem = map[item.uri];
-          if (remoteItem) {
-            if (firstSync || !item.custom.modified || remoteItem.modified > item.custom.modified) {
-              getRemote.push(remoteItem);
-            } else if (remoteItem.modified < item.custom.modified) {
-              putRemote.push(item);
-            }
-            delete map[item.uri];
-          } else if (firstSync || !outdated) {
+      var local = {
+        meta: _this.config.get('meta', {}),
+        data: res[2],
+      };
+      var firstSync = !local.meta.timestamp;
+      var outdated = !local.meta.timestamp || remote.meta.timestamp > local.meta.timestamp;
+      console.log('First sync:', firstSync);
+      console.log('Outdated:', outdated, '(', 'local:', local.meta.timestamp, 'remote:', remote.meta.timestamp, ')');
+      var map = {};
+      var getRemote = [];
+      var putRemote = [];
+      var delRemote = [];
+      var delLocal = [];
+      remote.data.forEach(function (item) {
+        map[item.uri] = item;
+      });
+      local.data.forEach(function (item) {
+        var remoteItem = map[item.uri];
+        if (remoteItem) {
+          if (firstSync || !item.custom.modified || remoteItem.modified > item.custom.modified) {
+            getRemote.push(remoteItem);
+          } else if (remoteItem.modified < item.custom.modified) {
             putRemote.push(item);
-          } else {
-            delLocal.push(item);
-          }
-        });
-        for (var uri in map) {
-          var item = map[uri];
-          if (outdated) {
-            getRemote.push(item);
-          } else {
-            delRemote.push(item);
           }
+          delete map[item.uri];
+        } else if (firstSync || !outdated) {
+          putRemote.push(item);
+        } else {
+          delLocal.push(item);
         }
-        var promises = [].concat(
-          getRemote.map(function (item) {
-            console.log('Download script:', item.uri);
-            return _this.get(getFilename(item.uri)).then(function (raw) {
-              var data = {};
-              try {
-                var obj = JSON.parse(raw);
-                if (obj.version === 1) {
-                  data.code = obj.code;
-                  data.more = obj.more;
-                }
-              } catch (e) {
-                data.code = raw;
-              }
-              data.modified = item.modified;
-              return app.vmdb.parseScript(data)
-              .then(function (res) {
-                _.messenger.post(res);
-              });
-            });
-          }),
-          putRemote.map(function (item) {
-            console.log('Upload script:', item.uri);
-            var data = JSON.stringify({
-              version: 1,
-              code: item.code,
-              more: {
-                custom: item.custom,
-                enabled: item.enabled,
-                update: item.update,
-              },
-            });
-            return _this.put(getFilename(item.uri), data)
-            .then(function (data) {
-              if (item.custom.modified !== data.modified) {
-                item.custom.modified = data.modified;
-                return app.vmdb.saveScript(item);
+      });
+      for (var uri in map) {
+        var item = map[uri];
+        if (outdated) {
+          getRemote.push(item);
+        } else {
+          delRemote.push(item);
+        }
+      }
+      var promises = [].concat(
+        getRemote.map(function (item) {
+          console.log('Download script:', item.uri);
+          return _this.get(getFilename(item.uri)).then(function (raw) {
+            var data = {};
+            try {
+              var obj = JSON.parse(raw);
+              if (obj.version === 1) {
+                data.code = obj.code;
+                data.more = obj.more;
               }
+            } catch (e) {
+              data.code = raw;
+            }
+            data.modified = item.modified;
+            return app.vmdb.parseScript(data)
+            .then(function (res) {
+              _.messenger.post(res);
             });
-          }),
-          delRemote.map(function (item) {
-            console.log('Remove remote script:', item.uri);
-            return _this.remove(getFilename(item.uri));
-          }),
-          delLocal.map(function (item) {
-            console.log('Remove local script:', item.uri);
-            return app.vmdb.removeScript(item.id)
-            .then(function () {
-              _.messenger.post({
-                cmd: 'del',
-                data: item.id,
-              });
+          });
+        }),
+        putRemote.map(function (item) {
+          console.log('Upload script:', item.uri);
+          var data = JSON.stringify({
+            version: 1,
+            code: item.code,
+            more: {
+              custom: item.custom,
+              enabled: item.enabled,
+              update: item.update,
+            },
+          });
+          return _this.put(getFilename(item.uri), data)
+          .then(function (data) {
+            if (item.custom.modified !== data.modified) {
+              item.custom.modified = data.modified;
+              return app.vmdb.saveScript(item);
+            }
+          });
+        }),
+        delRemote.map(function (item) {
+          console.log('Remove remote script:', item.uri);
+          return _this.remove(getFilename(item.uri));
+        }),
+        delLocal.map(function (item) {
+          console.log('Remove local script:', item.uri);
+          return app.vmdb.removeScript(item.id)
+          .then(function () {
+            _.messenger.post({
+              cmd: 'del',
+              data: item.id,
             });
-          })
-        );
-        promises.push(Promise.all(promises).then(function () {
-          var promises = [];
-          var remoteChanged;
-          if (!remote.meta.timestamp || putRemote.length || delRemote.length) {
-            remoteChanged = true;
-            remote.meta.timestamp = Date.now();
-            promises.push(_this.put(_this.metaFile, JSON.stringify(remote.meta)));
-          }
-          if (!local.meta.timestamp || getRemote.length || delLocal.length || remoteChanged || outdated) {
-            local.meta.timestamp = remote.meta.timestamp;
-          }
-          local.meta.lastSync = Date.now();
-          _this.config.set('meta', local.meta);
-          return Promise.all(promises);
-        }));
-        return Promise.all(promises.map(function (promise) {
-          // ignore errors to ensure all promises are fulfilled
-          return promise.then(_.noop, function (err) {
-            return err || true;
           });
-        }))
-        .then(function (errors) {
-          errors = errors.filter(function (err) {return err;});
-          if (errors.length) throw errors;
+        })
+      );
+      promises.push(Promise.all(promises).then(function () {
+        var promises = [];
+        var remoteChanged;
+        if (!remote.meta.timestamp || putRemote.length || delRemote.length) {
+          remoteChanged = true;
+          remote.meta.timestamp = Date.now();
+          promises.push(_this.put(_this.metaFile, JSON.stringify(remote.meta)));
+        }
+        if (!local.meta.timestamp || getRemote.length || delLocal.length || remoteChanged || outdated) {
+          local.meta.timestamp = remote.meta.timestamp;
+        }
+        local.meta.lastSync = Date.now();
+        _this.config.set('meta', local.meta);
+        return Promise.all(promises);
+      }));
+      return Promise.all(promises.map(function (promise) {
+        // ignore errors to ensure all promises are fulfilled
+        return promise.then(_.noop, function (err) {
+          return err || true;
         });
-      })
-      .then(function () {
-        _this.syncState.set('idle');
-      }, function (err) {
-        _this.syncState.set('error');
-        console.log('Failed syncing:', _this.name);
-        console.log(err);
+      }))
+      .then(function (errors) {
+        errors = errors.filter(function (err) {return err;});
+        if (errors.length) throw errors;
       });
-    },
-  });
-
-  tabs.update(function (tab) {
-    tab.url && services.some(function (service) {
-      return service.checkAuthenticate && service.checkAuthenticate(tab.url);
-    }) && tabs.remove(tab.id);
-  });
+    })
+    .then(function () {
+      _this.syncState.set('idle');
+    }, function (err) {
+      _this.syncState.set('error');
+      console.log('Failed syncing:', _this.name);
+      console.log(err);
+    });
+  },
+});
 
-  module.exports = {
-    init: init,
-    sync: sync,
-    service: service,
-    states: getStates,
-    utils: {
-      getFilename: getFilename,
-      isScriptFile: isScriptFile,
-      getURI: getURI,
-    },
-    BaseService: BaseService,
-  };
+tabs.update(function (tab) {
+  tab.url && services.some(function (service) {
+    return service.checkAuthenticate && service.checkAuthenticate(tab.url);
+  }) && tabs.remove(tab.id);
 });
+
+module.exports = {
+  init: init,
+  sync: sync,
+  service: service,
+  states: getStates,
+  utils: {
+    getFilename: getFilename,
+    isScriptFile: isScriptFile,
+    getURI: getURI,
+  },
+  BaseService: BaseService,
+};

+ 165 - 166
src/background/sync/onedrive.js

@@ -1,178 +1,177 @@
-define('sync_onedrive', function (require, _exports, _module) {
-  var sync = require('sync');
-  var tabsUtils = require('utils/tabs');
-  var searchUtils = require('utils/search');
-  var _ = require('utils/common');
-  var config = Object.assign({
-    client_id: '000000004418358A',
-    redirect_uri: 'https://violentmonkey.github.io/auth_onedrive.html',
-  }, JSON.parse(
-    // assume this is secret
-    window.atob('eyJjbGllbnRfc2VjcmV0Ijoiajl4M09WRXRIdmhpSEtEV09HcXV5TWZaS2s5NjA0MEgifQ==')
-  ));
+var sync = require('.');
+var tabsUtils = require('../utils/tabs');
+var searchUtils = require('../utils/search');
+var _ = require('../../common');
 
-  function authenticate() {
-    var params = {
-      response_type: 'code',
+var config = Object.assign({
+  client_id: '000000004418358A',
+  redirect_uri: 'https://violentmonkey.github.io/auth_onedrive.html',
+}, JSON.parse(
+  // assume this is secret
+  window.atob('eyJjbGllbnRfc2VjcmV0Ijoiajl4M09WRXRIdmhpSEtEV09HcXV5TWZaS2s5NjA0MEgifQ==')
+));
+
+function authenticate() {
+  var params = {
+    response_type: 'code',
+    client_id: config.client_id,
+    redirect_uri: config.redirect_uri,
+    scope: 'onedrive.appfolder wl.offline_access',
+  };
+  var url = 'https://login.live.com/oauth20_authorize.srf';
+  var qs = searchUtils.dump(params);
+  url += '?' + qs;
+  tabsUtils.create(url);
+}
+function checkAuthenticate(url) {
+  var redirect_uri = config.redirect_uri + '?code=';
+  if (url.slice(0, redirect_uri.length) === redirect_uri) {
+    onedrive.authState.set('authorizing');
+    authorized({
+      code: url.slice(redirect_uri.length),
+    }).then(function () {
+      onedrive.checkSync();
+    });
+    return true;
+  }
+}
+function authorized(params) {
+  return onedrive.request({
+    method: 'POST',
+    url: 'https://login.live.com/oauth20_token.srf',
+    prefix: '',
+    headers: {
+      'Content-Type': 'application/x-www-form-urlencoded',
+    },
+    body: searchUtils.dump(Object.assign({}, {
       client_id: config.client_id,
+      client_secret: config.client_secret,
       redirect_uri: config.redirect_uri,
-      scope: 'onedrive.appfolder wl.offline_access',
-    };
-    var url = 'https://login.live.com/oauth20_authorize.srf';
-    var qs = searchUtils.dump(params);
-    url += '?' + qs;
-    tabsUtils.create(url);
-  }
-  function checkAuthenticate(url) {
-    var redirect_uri = config.redirect_uri + '?code=';
-    if (url.slice(0, redirect_uri.length) === redirect_uri) {
-      onedrive.authState.set('authorizing');
-      authorized({
-        code: url.slice(redirect_uri.length),
-      }).then(function () {
-        onedrive.checkSync();
+      grant_type: 'authorization_code',
+    }, params)),
+  }).then(function (text) {
+    var data = JSON.parse(text);
+    if (data.access_token) {
+      onedrive.config.set({
+        uid: data.user_id,
+        token: data.access_token,
+        refresh_token: data.refresh_token,
       });
-      return true;
+    } else {
+      throw data;
     }
-  }
-  function authorized(params) {
-    return onedrive.request({
-      method: 'POST',
-      url: 'https://login.live.com/oauth20_token.srf',
-      prefix: '',
-      headers: {
-        'Content-Type': 'application/x-www-form-urlencoded',
-      },
-      body: searchUtils.dump(Object.assign({}, {
-        client_id: config.client_id,
-        client_secret: config.client_secret,
-        redirect_uri: config.redirect_uri,
-        grant_type: 'authorization_code',
-      }, params)),
-    }).then(function (text) {
-      var data = JSON.parse(text);
-      if (data.access_token) {
-        onedrive.config.set({
-          uid: data.user_id,
-          token: data.access_token,
-          refresh_token: data.refresh_token,
-        });
-      } else {
-        throw data;
+  });
+}
+function normalize(item) {
+  return {
+    size: item.size,
+    uri: sync.utils.getURI(item.name),
+    modified: new Date(item.lastModifiedDateTime).getTime(),
+  };
+}
+
+var OneDrive = sync.BaseService.extend({
+  name: 'onedrive',
+  displayName: 'OneDrive',
+  urlPrefix: 'https://api.onedrive.com/v1.0',
+  refreshToken: function () {
+    var _this = this;
+    var refresh_token = _this.config.get('refresh_token');
+    return authorized({
+      refresh_token: refresh_token,
+      grant_type: 'refresh_token',
+    }).then(function () {
+      return _this.prepare();
+    });
+  },
+  user: function () {
+    var _this = this;
+    return requestUser()
+    .catch(function (res) {
+      if (res.status === 401) {
+        return _this.refreshToken().then(requestUser);
       }
+      throw res;
     });
-  }
-  function normalize(item) {
-    return {
-      size: item.size,
-      uri: sync.utils.getURI(item.name),
-      modified: new Date(item.lastModifiedDateTime).getTime(),
-    };
-  }
-
-  var OneDrive = sync.BaseService.extend({
-    name: 'onedrive',
-    displayName: 'OneDrive',
-    urlPrefix: 'https://api.onedrive.com/v1.0',
-    refreshToken: function () {
-      var _this = this;
-      var refresh_token = _this.config.get('refresh_token');
-      return authorized({
-        refresh_token: refresh_token,
-        grant_type: 'refresh_token',
-      }).then(function () {
-        return _this.prepare();
+    function requestUser() {
+      return _this.request({
+        url: '/drive',
       });
-    },
-    user: function () {
-      var _this = this;
-      return requestUser()
-      .catch(function (res) {
-        if (res.status === 401) {
-          return _this.refreshToken().then(requestUser);
+    }
+  },
+  getMeta: function () {
+    function getMeta() {
+      return sync.BaseService.prototype.getMeta.call(_this);
+    }
+    var _this = this;
+    return getMeta()
+    .catch(function (res) {
+      if (res.status === 404) {
+        var header = res.xhr.getResponseHeader('WWW-Authenticate') || '';
+        if (/^Bearer realm="OneDriveAPI"/.test(header)) {
+          return _this.refreshToken().then(getMeta);
+        } else {
+          return {};
         }
-        throw res;
-      });
-      function requestUser() {
-        return _this.request({
-          url: '/drive',
-        });
       }
-    },
-    getMeta: function () {
-      function getMeta() {
-        return sync.BaseService.prototype.getMeta.call(_this);
-      }
-      var _this = this;
-      return getMeta()
-      .catch(function (res) {
-        if (res.status === 404) {
-          var header = res.xhr.getResponseHeader('WWW-Authenticate') || '';
-          if (/^Bearer realm="OneDriveAPI"/.test(header)) {
-            return _this.refreshToken().then(getMeta);
-          } else {
-            return {};
-          }
-        }
-        throw res;
-      });
-    },
-    list: function () {
-      var _this = this;
-      return _this.request({
-        url: '/drive/special/approot/children',
-      }).then(function (text) {
-        return JSON.parse(text);
-      }).then(function (data) {
-        return data.value.filter(function (item) {
-          return item.file && sync.utils.isScriptFile(item.name);
-        }).map(normalize);
-      });
-    },
-    get: function (path) {
-      return this.request({
-        url: '/drive/special/approot:/' + encodeURIComponent(path),
-      }).then(function (text) {
-        return JSON.parse(text);
-      }).then(function (data) {
-        var url = data['@content.downloadUrl'];
-        return new Promise(function (resolve, reject) {
-          var xhr = new XMLHttpRequest;
-          xhr.open('GET', url, true);
-          xhr.onload = function () {
-            resolve(xhr.responseText);
-          };
-          xhr.onerror = function () {
-            reject();
-          };
-          xhr.ontimeout = function () {
-            reject();
-          };
-          xhr.send();
-        });
+      throw res;
+    });
+  },
+  list: function () {
+    var _this = this;
+    return _this.request({
+      url: '/drive/special/approot/children',
+    }).then(function (text) {
+      return JSON.parse(text);
+    }).then(function (data) {
+      return data.value.filter(function (item) {
+        return item.file && sync.utils.isScriptFile(item.name);
+      }).map(normalize);
+    });
+  },
+  get: function (path) {
+    return this.request({
+      url: '/drive/special/approot:/' + encodeURIComponent(path),
+    }).then(function (text) {
+      return JSON.parse(text);
+    }).then(function (data) {
+      var url = data['@content.downloadUrl'];
+      return new Promise(function (resolve, reject) {
+        var xhr = new XMLHttpRequest;
+        xhr.open('GET', url, true);
+        xhr.onload = function () {
+          resolve(xhr.responseText);
+        };
+        xhr.onerror = function () {
+          reject();
+        };
+        xhr.ontimeout = function () {
+          reject();
+        };
+        xhr.send();
       });
-    },
-    put: function (path, data) {
-      return this.request({
-        method: 'PUT',
-        url: '/drive/special/approot:/' + encodeURIComponent(path) + ':/content',
-        headers: {
-          'Content-Type': 'application/octet-stream',
-        },
-        body: data,
-      }).then(function (text) {
-        return JSON.parse(text);
-      }).then(normalize);
-    },
-    remove: function (path) {
-      // return 204
-      return this.request({
-        method: 'DELETE',
-        url: '/drive/special/approot:/' + encodeURIComponent(path),
-      }).catch(_.noop);
-    },
-    authenticate: authenticate,
-    checkAuthenticate: checkAuthenticate,
-  });
-  var onedrive = sync.service('onedrive', OneDrive);
+    });
+  },
+  put: function (path, data) {
+    return this.request({
+      method: 'PUT',
+      url: '/drive/special/approot:/' + encodeURIComponent(path) + ':/content',
+      headers: {
+        'Content-Type': 'application/octet-stream',
+      },
+      body: data,
+    }).then(function (text) {
+      return JSON.parse(text);
+    }).then(normalize);
+  },
+  remove: function (path) {
+    // return 204
+    return this.request({
+      method: 'DELETE',
+      url: '/drive/special/approot:/' + encodeURIComponent(path),
+    }).catch(_.noop);
+  },
+  authenticate: authenticate,
+  checkAuthenticate: checkAuthenticate,
 });
+var onedrive = sync.service('onedrive', OneDrive);

+ 22 - 24
src/background/utils/cache.js

@@ -1,26 +1,24 @@
-define('utils/cache', function (_require, _exports, module) {
-  function get(key) {
+function get(key) {
+  var obj = cache[key];
+  return obj && obj.value;
+}
+function set(key, value) {
+  if (value) {
     var obj = cache[key];
-    return obj && obj.value;
+    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];
   }
-  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,
-  };
-});
+}
+var cache = {};
+module.exports = {
+  get: get,
+  set: set,
+};

+ 28 - 0
src/background/utils/events.js

@@ -0,0 +1,28 @@
+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);
+    });
+  }
+}
+
+exports.getEventEmitter = getEventEmitter;

+ 92 - 94
src/background/utils/script.js

@@ -1,96 +1,94 @@
-define('utils/script', function (_require, _exports, module) {
-  module.exports = {
-    isRemote: function (url) {
-      return url && !(/^(file|data):/.test(url));
-    },
-    fetch: function (url, type, headers) {
-      return new Promise(function (resolve, reject) {
-        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]);
-        }
-        xhr.onloadend = function () {
-          (xhr.status > 300 ? reject : resolve)(xhr);
-        };
-        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,
+module.exports = {
+  isRemote: function (url) {
+    return url && !(/^(file|data):/.test(url));
+  },
+  fetch: function (url, type, headers) {
+    return new Promise(function (resolve, reject) {
+      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]);
+      }
+      xhr.onloadend = function () {
+        (xhr.status > 300 ? reject : resolve)(xhr);
       };
-    },
-    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;
+      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;
       }
-      return 0;
-    },
-  };
-});
+    });
+    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;
+  },
+};

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

@@ -1,18 +1,16 @@
-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('&');
-    },
-  };
-});
+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('&');
+  },
+};

+ 27 - 29
src/background/utils/tabs.js

@@ -1,32 +1,30 @@
-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,
-        });
+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);
-        });
+    });
+  },
+  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) {
-        tabs.forEach(function (tab) {
-          chrome.tabs.sendMessage(tab.id, data);
-        });
+    });
+  },
+  broadcast: function (data) {
+    chrome.tabs.query({}, function (tabs) {
+      tabs.forEach(function (tab) {
+        chrome.tabs.sendMessage(tab.id, data);
       });
-    },
-  };
-});
+    });
+  },
+};

+ 69 - 71
src/background/utils/tester.js

@@ -1,77 +1,75 @@
-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 (test) {
-      return mat.some(test);
-    }(matchTester(url));
-    // @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 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 (test) {
+    return mat.some(test);
+  }(matchTester(url));
+  // @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 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
-  }
+function autoReg(str) {
+  if (/^\/.*\/$/.test(str))
+    return RegExp(str.slice(1, -1));  // Regular-expression
+  else
+    return str2RE(str); // String with wildcards
+}
 
-  function matchTester(url) {
-    function matchScheme(rule, data) {
-      // exact match
-      if (rule == data) return 1;
-      // * = http | https
-      if (rule == '*' && /^https?$/i.test(data)) return 1;
-      return 0;
-    }
-    function matchHost(rule, data) {
-      // * matches all
-      if (rule == '*') return 1;
-      // exact match
-      if (rule == data) return 1;
-      // *.example.com
-      if (/^\*\.[^*]*$/.test(rule)) {
-        // matches the specified domain
-        if (rule.slice(2) == data) return 1;
-        // matches subdomains
-        if (str2RE(rule).test(data)) return 1;
-      }
-      return 0;
-    }
-    function matchPath(rule, data) {
-      return str2RE(rule).test(data);
+function matchTester(url) {
+  function matchScheme(rule, data) {
+    // exact match
+    if (rule == data) return 1;
+    // * = http | https
+    if (rule == '*' && /^https?$/i.test(data)) return 1;
+    return 0;
+  }
+  function matchHost(rule, data) {
+    // * matches all
+    if (rule == '*') return 1;
+    // exact match
+    if (rule == data) return 1;
+    // *.example.com
+    if (/^\*\.[^*]*$/.test(rule)) {
+      // matches the specified domain
+      if (rule.slice(2) == data) return 1;
+      // matches subdomains
+      if (str2RE(rule).test(data)) return 1;
     }
-    var RE = /(.*?):\/\/([^\/]*)\/(.*)/;
-    var urlParts = url.match(RE);
-    return function (str) {
-      if (str == '<all_urls>') return true;
-      var parts = str.match(RE);
-      return !!parts
-        && matchScheme(parts[1], urlParts[1])
-        && matchHost(parts[2], urlParts[2])
-        && matchPath(parts[3], urlParts[3]);
-    };
+    return 0;
+  }
+  function matchPath(rule, data) {
+    return str2RE(rule).test(data);
   }
+  var RE = /(.*?):\/\/([^\/]*)\/(.*)/;
+  var urlParts = url.match(RE);
+  return function (str) {
+    if (str == '<all_urls>') return true;
+    var parts = str.match(RE);
+    return !!parts
+      && matchScheme(parts[1], urlParts[1])
+      && matchHost(parts[2], urlParts[2])
+      && matchPath(parts[3], urlParts[3]);
+  };
+}
 
-  exports.testURL = testURL;
-});
+exports.testURL = testURL;

+ 30 - 31
src/cache.js

@@ -1,33 +1,32 @@
-define('cache', function (require, _exports, module) {
-  function Cache(allowOverride) {
-    this.data = {};
-    this.allowOverride = allowOverride;
-  }
-  Cache.prototype.put = function (key, fn) {
-    if (key in this.data && !this.allowOverride)
-      throw 'Key {' + key + '} already exists!';
-    this.data[key] = fn;
-  };
-  Cache.prototype.get = function (key) {
-    var data = this.data;
-    if (key in data) return data[key];
-    throw 'Cache not found: ' + key;
-  };
+function Cache(allowOverride) {
+  this.data = {};
+  this.allowOverride = allowOverride;
+}
+Cache.prototype.put = function (key, fn) {
+  if (key in this.data && !this.allowOverride)
+    throw 'Key {' + key + '} already exists!';
+  this.data[key] = fn;
+};
+Cache.prototype.get = function (key) {
+  var data = this.data;
+  if (key in data) return data[key];
+  throw 'Cache not found: ' + key;
+};
 
-  var _ = require('utils/common');
-  Vue.filter('i18n', _.i18n);
-  module.exports = new Cache();
-  require('templates');
+var _ = require('./common');
+Vue.prototype.i18n = _.i18n;
+
+!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();
-  }();
-});
+/* eslint-disable no-unused-vars */
+var cache = module.exports = new Cache();

+ 117 - 119
src/common.js

@@ -1,138 +1,136 @@
-define('utils/common', function (_require, _exports, module) {
-  var _ = module.exports = {};
-  _.i18n = chrome.i18n.getMessage;
+var _ = module.exports = {};
+_.i18n = chrome.i18n.getMessage;
 
-  _.options = function () {
-    function getOption(key, def) {
-      var value = localStorage.getItem(key), obj;
-      if (value)
-        try {
-          obj = JSON.parse(value);
-        } catch (e) {
-          obj = def;
-        }
-        else obj = def;
-        if (obj == null) obj = defaults[key];
-        return obj;
-    }
+_.options = function () {
+  function getOption(key, def) {
+    var value = localStorage.getItem(key), obj;
+    if (value)
+      try {
+        obj = JSON.parse(value);
+      } catch (e) {
+        obj = def;
+      }
+    else obj = def;
+    if (obj == null) obj = defaults[key];
+    return obj;
+  }
 
-    function setOption(key, value) {
-      if (key in defaults) {
-        localStorage.setItem(key, JSON.stringify(value));
-        [hooks[key], hooks['']].forEach(function (group) {
-          group && group.forEach(function (cb) {
-            cb(value, key);
-          });
+  function setOption(key, value) {
+    if (key in defaults) {
+      localStorage.setItem(key, JSON.stringify(value));
+      [hooks[key], hooks['']].forEach(function (group) {
+        group && group.forEach(function (cb) {
+          cb(value, key);
         });
-      }
+      });
     }
+  }
 
-    function getAllOptions() {
-      var options = {};
-      for (var i in defaults) options[i] = getOption(i);
-      return options;
-    }
+  function getAllOptions() {
+    var options = {};
+    for (var i in defaults) options[i] = getOption(i);
+    return options;
+  }
 
-    function parseArgs(args) {
-      return args.length === 1 ? {
-        key: '',
-        cb: args[0],
-      } : {
-        key: args[0] || '',
-        cb: args[1],
-      };
-    }
+  function parseArgs(args) {
+    return args.length === 1 ? {
+      key: '',
+      cb: args[0],
+    } : {
+      key: args[0] || '',
+      cb: args[1],
+    };
+  }
 
-    function hook() {
-      var arg = parseArgs(arguments);
-      var list = hooks[arg.key];
-      if (!list) list = hooks[arg.key] = [];
-      list.push(arg.cb);
-      return function () {
-        unhook(arg.key, arg.cb);
-      };
-    }
-    function unhook() {
-      var arg = parseArgs(arguments);
-      var list = hooks[arg.key];
-      if (list) {
-        var i = list.indexOf(arg.cb);
-        ~i && list.splice(i, 1);
-      }
+  function hook() {
+    var arg = parseArgs(arguments);
+    var list = hooks[arg.key];
+    if (!list) list = hooks[arg.key] = [];
+    list.push(arg.cb);
+    return function () {
+      unhook(arg.key, arg.cb);
+    };
+  }
+  function unhook() {
+    var arg = parseArgs(arguments);
+    var list = hooks[arg.key];
+    if (list) {
+      var i = list.indexOf(arg.cb);
+      ~i && list.splice(i, 1);
     }
+  }
 
-    var defaults = {
-      isApplied: true,
-      autoUpdate: true,
-      ignoreGrant: false,
-      lastUpdate: 0,
-      exportValues: true,
-      closeAfterInstall: false,
-      trackLocalFile: false,
-      autoReload: false,
-      dropbox: {},
-      dropboxEnabled: false,
-      onedrive: {},
-      onedriveEnabled: false,
-      features: null,
-    };
-    var hooks = {};
+  var defaults = {
+    isApplied: true,
+    autoUpdate: true,
+    ignoreGrant: false,
+    lastUpdate: 0,
+    exportValues: true,
+    closeAfterInstall: false,
+    trackLocalFile: false,
+    autoReload: false,
+    dropbox: {},
+    dropboxEnabled: false,
+    onedrive: {},
+    onedriveEnabled: false,
+    features: null,
+  };
+  var hooks = {};
 
-    return {
-      get: getOption,
-      set: setOption,
-      getAll: getAllOptions,
-      hook: hook,
-      unhook: unhook,
-    };
-  }();
+  return {
+    get: getOption,
+    set: setOption,
+    getAll: getAllOptions,
+    hook: hook,
+    unhook: unhook,
+  };
+}();
 
-  _.sendMessage = function (data) {
-    return new Promise(function (resolve, reject) {
-      chrome.runtime.sendMessage(data, function (res) {
-        res && res.error ? reject(res.error) : resolve(res && res.data);
-      });
+_.sendMessage = function (data) {
+  return new Promise(function (resolve, reject) {
+    chrome.runtime.sendMessage(data, function (res) {
+      res && res.error ? reject(res.error) : resolve(res && res.data);
     });
-  };
+  });
+};
 
-  _.debounce = function (func, time) {
-    function run() {
-      cancel();
-      func();
-    }
-    function cancel() {
-      if (timer) {
-        clearTimeout(timer);
-        timer = null;
-      }
+_.debounce = function (func, time) {
+  function run() {
+    cancel();
+    func();
+  }
+  function cancel() {
+    if (timer) {
+      clearTimeout(timer);
+      timer = null;
     }
-    var timer;
-    return function () {
-      cancel();
-      timer = setTimeout(run, time);
-    };
+  }
+  var timer;
+  return function () {
+    cancel();
+    timer = setTimeout(run, time);
   };
+};
 
-  _.noop = function () {};
+_.noop = function () {};
 
-  _.zfill = function (num, length) {
-    num = num.toString();
-    while (num.length < length) num = '0' + num;
-    return num;
-  };
+_.zfill = function (num, length) {
+  num = num.toString();
+  while (num.length < length) num = '0' + num;
+  return num;
+};
 
-  _.getUniqId = function () {
-    return Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
-  };
+_.getUniqId = function () {
+  return Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
+};
 
-  /**
-   * Get locale attributes such as `@name:zh-CN`
-   */
-  _.getLocaleString = function (meta, key) {
-    var lang = navigator.languages.find(function (lang) {
-      return (key + ':' + lang) in meta;
-    });
-    if (lang) key += ':' + lang;
-    return meta[key] || '';
-  };
-});
+/**
+ * Get locale attributes such as `@name:zh-CN`
+ */
+_.getLocaleString = function (meta, key) {
+  var lang = navigator.languages.find(function (lang) {
+    return (key + ':' + lang) in meta;
+  });
+  if (lang) key += ':' + lang;
+  return meta[key] || '';
+};

+ 99 - 100
src/options/app.js

@@ -1,110 +1,109 @@
-define('app', function (require, _exports, _module) {
-  function initMain() {
-    store.loading = true;
-    _.sendMessage({cmd: 'GetData'})
-    .then(function (data) {
-      [
-        'cache',
-        'scripts',
-        'sync',
-      ].forEach(function (key) {
-        store[key] = data[key];
-      });
-      store.loading = false;
-      // features.reset(data.version);
-      features.reset('sync');
+function initMain() {
+  store.loading = true;
+  _.sendMessage({cmd: 'GetData'})
+  .then(function (data) {
+    [
+      'cache',
+      'scripts',
+      'sync',
+    ].forEach(function (key) {
+      store[key] = data[key];
     });
-    var port = chrome.runtime.connect({name: 'Options'});
-    port.onMessage.addListener(function (res) {
-      switch (res.cmd) {
-      case 'sync':
-        store.sync = res.data;
-        break;
-      case 'add':
-        res.data.message = '';
-        store.scripts.push(res.data);
-        break;
-      case 'update':
-        if (res.data) {
-          var script = store.scripts.find(function (script) {
-            return script.id === res.data.id;
-          });
-          if (script) for (var k in res.data) {
-            Vue.set(script, k, res.data[k]);
-          }
-        }
-        break;
-      case 'del':
-        var i = store.scripts.findIndex(function (script) {
-          return script.id === res.data;
+    store.loading = false;
+    // utils.features.reset(data.version);
+    utils.features.reset('sync');
+  });
+  var port = chrome.runtime.connect({name: 'Options'});
+  port.onMessage.addListener(function (res) {
+    switch (res.cmd) {
+    case 'sync':
+      store.sync = res.data;
+      break;
+    case 'add':
+      res.data.message = '';
+      store.scripts.push(res.data);
+      break;
+    case 'update':
+      if (res.data) {
+        var script = store.scripts.find(function (script) {
+          return script.id === res.data.id;
         });
-        ~i && store.scripts.splice(i, 1);
+        if (script) for (var k in res.data) {
+          Vue.set(script, k, res.data[k]);
+        }
       }
-    });
-  }
-
-  var _ = require('utils/common');
-  var utils = require('utils');
-  var Main = require('views/Main');
-  var Confirm = require('views/Confirm');
-  var features = require('utils/features');
-  var store = Object.assign(utils.store, {
-    loading: false,
-    cache: {},
-    scripts: [],
-    sync: [],
+      break;
+    case 'del':
+      var i = store.scripts.findIndex(function (script) {
+        return script.id === res.data;
+      });
+      ~i && store.scripts.splice(i, 1);
+    }
   });
-  var init = {
-    Main: initMain,
-  };
-  zip.workerScriptsPath = '/lib/zip.js/';
-  document.title = _.i18n('extName');
+}
 
-  new Vue({
-    el: document.body,
-    components: {
-      Main: Main,
-      Confirm: Confirm,
-    },
-    data: function () {
-      return {
-        type: 'main',
-        params: {},
-      };
-    },
-    ready: function () {
+var _ = require('../common');
+var utils = require('./utils');
+var Main = require('./views/main');
+var Confirm = require('./views/confirm');
+
+var store = Object.assign(utils.store, {
+  loading: false,
+  cache: {},
+  scripts: [],
+  sync: [],
+});
+var init = {
+  Main: initMain,
+};
+zip.workerScriptsPath = '/lib/zip.js/';
+document.title = _.i18n('extName');
+
+new Vue({
+  el: '#app',
+  template: '<component :is=type :params=params></component>',
+  components: {
+    Main: Main,
+    Confirm: Confirm,
+  },
+  data: function () {
+    return {
+      type: 'Main',
+      params: {},
+    };
+  },
+  mounted: function () {
+    var _this = this;
+    _this.routes = {
+      Main: utils.routeTester([
+        '',
+        'main/:tab',
+      ]),
+      Confirm: utils.routeTester([
+        'confirm/:url',
+        'confirm/:url/:referer',
+      ]),
+    };
+    window.addEventListener('hashchange', _this.loadHash.bind(_this));
+    _this.loadHash();
+  },
+  methods: {
+    loadHash: function () {
       var _this = this;
-      _this.routes = {
-        Main: utils.routeTester([
-          '',
-          'main/:tab',
-        ]),
-        Confirm: utils.routeTester([
-          'confirm/:url',
-          'confirm/:url/:referer',
-        ]),
-      };
-      window.addEventListener('hashchange', _this.loadHash.bind(_this));
-      _this.loadHash();
-    },
-    methods: {
-      loadHash: function () {
-        var _this = this;
-        var hash = location.hash.slice(1);
-        for (var k in _this.routes) {
-          var test = _this.routes[k];
-          var params = test(hash);
-          if (params) {
-            _this.type = k;
-            _this.params = params;
-            if (init[k]) {
-              init[k]();
-              init[k] = null;
-            }
-            break;
+      var hash = location.hash.slice(1);
+      for (var k in _this.routes) {
+        var test = _this.routes[k];
+        var params = test(hash);
+        if (params) {
+          _this.type = k;
+          _this.params = params;
+          if (init[k]) {
+            init[k]();
+            init[k] = null;
           }
+          break;
         }
-      },
+      }
     },
-  });
+  },
 });

+ 0 - 189
src/options/components/confirm.js

@@ -1,189 +0,0 @@
-define('views/Confirm', function (require, _exports, module) {
-  var Editor = require('views/Editor');
-  var cache = require('cache');
-  var _ = require('utils/common');
-
-  module.exports = {
-    props: ['params'],
-    components: {
-      Editor: Editor,
-    },
-    template: cache.get('/options/components/confirm.html'),
-    data: function () {
-      return {
-        installable: false,
-        dependencyOK: false,
-        message: '',
-        code: '',
-        require: {},
-        resources: {},
-      };
-    },
-    computed: {
-      isLocal: function () {
-        return /^file:\/\/\//.test(this.params.url);
-      },
-    },
-    ready: function () {
-      var _this = this;
-      _this.message = _.i18n('msgLoadingData');
-      _this.revoke = function (key) {
-        _this.$set(key, _.options.get(key));
-        return _.options.hook(key, function (value) {
-          _this.$set(key, value);
-          value && _.options.set('trackLocalFile', false);
-        });
-      }('closeAfterInstall');
-      _this.loadData().then(function () {
-        _this.parseMeta();
-      });
-    },
-    beforeDestroy: function () {
-      this.revoke();
-    },
-    methods: {
-      loadData: function (changedOnly) {
-        var _this = this;
-        _this.installable = false;
-        var oldCode = _this.code;
-        return _this.getScript(_this.params.url)
-        .then(function (code) {
-          if (changedOnly && oldCode === code) return Promise.reject();
-          _this.code = code;
-        })
-      },
-      parseMeta: function () {
-        var _this = this;
-        return _.sendMessage({
-          cmd: 'ParseMeta',
-          data: _this.code,
-        })
-        .then(function (script) {
-          var urls = Object.keys(script.resources).map(function (key) {
-            return script.resources[key];
-          });
-          var length = script.require.length + urls.length;
-          if (!length) return;
-          var finished = 0;
-          var error = [];
-          var updateStatus = function () {
-            _this.message = _.i18n('msgLoadingDependency', [finished, length]);
-          };
-          updateStatus();
-          var promises = script.require.map(function (url) {
-            return _this.getFile(url).then(function (res) {
-              _this.require[url] = res;
-            });
-          });
-          promises = promises.concat(urls.map(function (url) {
-            return _this.getFile(url, true).then(function (res) {
-              _this.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.dependencyOK = true;
-          });
-        })
-        .then(function () {
-          _this.message = _.i18n('msgLoadedData');
-          _this.installable = true;
-        }, function (err) {
-          _this.message = _.i18n('msgErrorLoadingDependency', [err]);
-          return Promise.reject();
-        });
-      },
-      close: function () {
-        window.close();
-      },
-      getFile: function (url, isBlob) {
-        return new Promise(function (resolve, reject) {
-          var xhr = new XMLHttpRequest;
-          xhr.open('GET', url, true);
-          if (isBlob) xhr.responseType = 'blob';
-          xhr.onloadend = function () {
-            if (xhr.status > 300) return reject(url);
-            if (isBlob) {
-              var reader = new FileReader;
-              reader.onload = function () {
-                resolve(window.btoa(this.result));
-              };
-              reader.readAsBinaryString(xhr.response);
-            } else {
-              resolve(xhr.responseText);
-            }
-          };
-          xhr.send();
-        });
-      },
-      getScript: function (url) {
-        var _this = this;
-        return _.sendMessage({
-          cmd: 'GetFromCache',
-          data: url,
-        })
-        .then(function (text) {
-          return text || Promise.reject();
-        })
-        .catch(function () {
-          return _this.getFile(url);
-        })
-        .catch(function (url) {
-          _this.message = _.i18n('msgErrorLoadingData');
-          throw url;
-        });
-      },
-      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.installable = false;
-        _.sendMessage({
-          cmd:'ParseScript',
-          data:{
-            url: _this.params.url,
-            from: _this.params.referer,
-            code: _this.code,
-            require: _this.require,
-            resources: _this.resources,
-          },
-        })
-        .then(function (res) {
-          _this.message = res.message + '[' + _this.getTimeString() + ']';
-          if (res.code < 0) return;
-          if (_.options.get('closeAfterInstall')) _this.close();
-          else if (_this.isLocal && _.options.get('trackLocalFile')) _this.trackLocalFile();
-        });
-      },
-      trackLocalFile: function () {
-        var _this = this;
-        new Promise(function (resolve) {
-          setTimeout(resolve, 2000);
-        })
-        .then(function () {
-          return _this.loadData(true).then(function () {
-            return _this.parseMeta();
-          });
-        })
-        .then(function () {
-          var track = _.options.get('trackLocalFile');
-          track && _this.installScript();
-        }, function () {
-          _this.trackLocalFile();
-        });
-      },
-    },
-  };
-});

+ 0 - 149
src/options/components/edit.js

@@ -1,149 +0,0 @@
-define('views/Edit', function (require, _exports, module) {
-  function fromList(list) {
-    return (list || []).join('\n');
-  }
-  function toList(text) {
-    return text.split('\n')
-    .map(function (line) {
-      return line.trim();
-    })
-    .filter(function (item) {
-      return item;
-    });
-  }
-
-  var Message = require('views/Message');
-  var Editor = require('views/Editor');
-  var cache = require('cache');
-  var _ = require('utils/common');
-
-  module.exports = {
-    props: {
-      script: {
-        twoWay: true,
-      },
-    },
-    template: cache.get('/options/components/edit.html'),
-    components: {
-      Editor: Editor,
-    },
-    data: function () {
-      return {
-        canSave: false,
-        update: false,
-        code: '',
-        custom: {},
-      };
-    },
-    computed: {
-      placeholders: function () {
-        var script = this.script;
-        return {
-          name: script.meta.name,
-          homepageURL: script.meta.homepageURL,
-          updateURL: script.meta.updateURL || _.i18n('hintUseDownloadURL'),
-          downloadURL: script.meta.downloadURL || script.lastInstallURL,
-        };
-      },
-    },
-    watch: {
-      code: function () {
-        this.canSave = true;
-      },
-      custom: {
-        deep: true,
-        handler: function () {
-          this.canSave = true;
-        },
-      },
-    },
-    ready: function () {
-      var _this = this;
-      (_this.script.id ? _.sendMessage({
-        cmd: 'GetScript',
-        data: _this.script.id,
-      }) : Promise.resolve(_this.script))
-      .then(function (script) {
-        _this.update = script.update;
-        _this.code = script.code;
-        var custom = script.custom;
-        _this.custom = [
-          'name',
-          'homepageURL',
-          'updateURL',
-          'downloadURL',
-        ].reduce(function (value, key) {
-          value[key] = custom[key];
-          return value;
-        }, {
-          keepInclude: custom._include !== false,
-          keepMatch: custom._match !== false,
-          keepExclude: custom._exclude !== false,
-          include: fromList(custom.include),
-          match: fromList(custom.match),
-          exclude: fromList(custom.exclude),
-          'run-at': custom['run-at'] || '',
-        });
-        _this.$nextTick(function () {
-          _this.canSave = false;
-        });
-      });
-    },
-    methods: {
-      save: function () {
-        var _this = this;
-        var custom = _this.custom;
-        var value = [
-          'name',
-          'run-at',
-          'homepageURL',
-          'updateURL',
-          'downloadURL',
-        ].reduce(function (value, key) {
-          value[key] = custom[key];
-          return value;
-        }, {
-          _include: custom.keepInclude,
-          _match: custom.keepMatch,
-          _exclude: custom.keepExclude,
-          include: toList(custom.include),
-          match: toList(custom.match),
-          exclude: toList(custom.exclude),
-        });
-        return _.sendMessage({
-          cmd: 'ParseScript',
-          data: {
-            id: _this.script.id,
-            code: _this.code,
-            // User created scripts MUST be marked `isNew` so that
-            // the backend is able to check namespace conflicts
-            isNew: !_this.script.id,
-            message: '',
-            more: {
-              custom: value,
-              update: _this.update,
-            },
-          },
-        })
-        .then(function (script) {
-          _this.script = script;
-          _this.canSave = false;
-        }, function (err) {
-          new Message({text: err});
-        });
-      },
-      close: function () {
-        var _this = this;
-        if (!_this.canSave || confirm(_.i18n('confirmNotSaved'))) {
-          _this.script = null;
-        }
-      },
-      saveClose: function () {
-        var _this = this;
-        _this.save().then(function () {
-          _this.close();
-        });
-      },
-    },
-  };
-});

+ 0 - 127
src/options/components/editor.js

@@ -1,127 +0,0 @@
-define('views/Editor', function (require, _exports, module) {
-  function addScripts(data) {
-    function add(data) {
-      var s = document.createElement('script');
-      if (data.innerHTML) s.innerHTML = data.innerHTML;
-      else if (data.src) s.src = data.src;
-      return new Promise(function (resolve, reject) {
-        s.onload = function () {
-          resolve();
-        };
-        s.onerror = function () {
-          reject();
-        };
-        document.body.appendChild(s);
-      });
-    }
-    if (!data.map) data = [data];
-    return Promise.all(data.map(add));
-  }
-
-  function addCSS(data) {
-    function add(data) {
-      var s;
-      if (data.html) {
-        s = document.createElement('style');
-        s.innerHTML = data.html;
-      } else if (data.href) {
-        s = document.createElement('link');
-        s.rel = 'stylesheet';
-        s.href = data.href;
-      }
-      if (s) document.body.appendChild(s);
-    }
-    if (!data.forEach) data = [data];
-    data.forEach(add);
-  }
-
-  function initCodeMirror() {
-    addCSS([
-      {href: '/lib/CodeMirror/lib/codemirror.css'},
-      {href: '/mylib/CodeMirror/fold.css'},
-      {href: '/mylib/CodeMirror/search.css'},
-    ]);
-    return addScripts(
-      {src: '/lib/CodeMirror/lib/codemirror.js'}
-    ).then(function () {
-      return addScripts([
-        {src: '/lib/CodeMirror/mode/javascript/javascript.js'},
-        {src: '/lib/CodeMirror/addon/comment/continuecomment.js'},
-        {src: '/lib/CodeMirror/addon/edit/matchbrackets.js'},
-        {src: '/lib/CodeMirror/addon/edit/closebrackets.js'},
-        {src: '/lib/CodeMirror/addon/fold/foldcode.js'},
-        {src: '/lib/CodeMirror/addon/fold/foldgutter.js'},
-        {src: '/lib/CodeMirror/addon/fold/brace-fold.js'},
-        {src: '/lib/CodeMirror/addon/fold/comment-fold.js'},
-        {src: '/lib/CodeMirror/addon/search/match-highlighter.js'},
-        {src: '/lib/CodeMirror/addon/search/searchcursor.js'},
-        {src: '/lib/CodeMirror/addon/selection/active-line.js'},
-        {src: '/mylib/CodeMirror/search.js'},
-      ]);
-    });
-  }
-
-  var cache = require('cache');
-  var readyCodeMirror = initCodeMirror();
-
-  module.exports = {
-    props: {
-      readonly: null,
-      onExit: null,
-      onSave: null,
-      content: {
-        twoWay: true,
-      },
-    },
-    template: cache.get('/options/components/editor.html'),
-    ready: function () {
-      var _this = this;
-      readyCodeMirror.then(function () {
-        var editor = _this.editor = CodeMirror(_this.$el, {
-          continueComments: true,
-          matchBrackets: true,
-          autoCloseBrackets: true,
-          highlightSelectionMatches: true,
-          lineNumbers: true,
-          mode: 'javascript',
-          lineWrapping: true,
-          styleActiveLine: true,
-          foldGutter: true,
-          gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
-        });
-        _this.readonly && editor.setOption('readOnly', _this.readonly);
-        editor.on('change', function () {
-          _this.cachedContent = editor.getValue();
-          _this.content = _this.cachedContent;
-        });
-        var extraKeys = {};
-        if (_this.onExit) {
-          extraKeys.Esc = _this.onExit;
-        }
-        if (_this.onSave) {
-          extraKeys['Ctrl-S'] = extraKeys['Cmd-S'] = _this.onSave;
-        }
-        editor.setOption('extraKeys', extraKeys);
-        _this.update();
-      });
-    },
-    watch: {
-      content: function (content) {
-        var _this = this;
-        if (content !== _this.cachedContent) {
-          _this.cachedContent = content;
-          _this.update();
-        }
-      },
-    },
-    methods: {
-      update: function () {
-        var _this = this;
-        if (!_this.editor || _this.cachedContent == null) return;
-        _this.editor.setValue(_this.cachedContent);
-        _this.editor.getDoc().clearHistory();
-        _this.editor.focus();
-      },
-    },
-  };
-});

+ 0 - 16
src/options/components/main.html

@@ -1,16 +0,0 @@
-<div class="main">
-  <aside>
-    <img src="/images/icon128.png">
-    <h1 v-text="'extName'|i18n"></h1>
-    <div class="line">2013-2016</div>
-    <hr>
-    <div class=sidemenu>
-      <a href="#main/installed" :class="{active:tab==='main'}" v-text="'sideMenuInstalled'|i18n"></a>
-      <a href="#main/settings" :class="{active:tab==='settings'}" v-feature.literal="settings">
-        <span v-text="'sideMenuSettings'|i18n" class="feature-text"></span>
-      </a>
-      <a href="#main/about" :class="{active:tab==='about'}" v-text="'sideMenuAbout'|i18n"></a>
-    </div>
-  </aside>
-  <component :is="tab"></component>
-</div>

+ 0 - 30
src/options/components/main.js

@@ -1,30 +0,0 @@
-define('views/Main', function (require, _exports, module) {
-  var MainTab = require('views/TabInstalled');
-  var SettingsTab = require('views/TabSettings');
-  var AboutTab = require('views/TabAbout');
-  var cache = require('cache');
-
-  var components = {
-    main: MainTab,
-    settings: SettingsTab,
-    about: AboutTab,
-  };
-
-  module.exports = {
-    props: {
-      params: {
-        coerce: function (params) {
-          params.tab = components[params.tab] ? params.tab : 'main';
-          return params;
-        },
-      },
-    },
-    template: cache.get('/options/components/main.html'),
-    components: components,
-    computed: {
-      tab: function () {
-        return this.params.tab;
-      },
-    },
-  };
-});

+ 0 - 1
src/options/components/message.html

@@ -1 +0,0 @@
-<div class="message" transition="message">{{$options.text}}</div>

+ 0 - 21
src/options/components/message.js

@@ -1,21 +0,0 @@
-define('views/Message', function (require, _exports, module) {
-  var cache = require('cache');
-
-  module.exports = Vue.extend({
-    template: cache.get('/options/components/message.html'),
-    el: function () {
-      var el = document.createElement('div');
-      document.body.appendChild(el);
-      return el;
-    },
-    ready: function () {
-      var _this = this;
-      new Promise(function (resolve) {
-        setTimeout(resolve, 2000);
-      })
-      .then(function () {
-        _this.$destroy(true);
-      });
-    },
-  });
-});

+ 0 - 254
src/options/components/script.js

@@ -1,254 +0,0 @@
-define('views/Script', function (require, _exports, module) {
-  var store = require('utils').store;
-  var cache = require('cache');
-  var _ = require('utils/common');
-  var DEFAULT_ICON = '/images/icon48.png';
-
-  module.exports = {
-    props: ['script'],
-    template: cache.get('/options/components/script.html'),
-    data: function () {
-      return {
-        safeIcon: DEFAULT_ICON,
-      };
-    },
-    computed: {
-      canUpdate: function () {
-        var script = this.script;
-        return script.update && (
-          script.custom.updateURL ||
-          script.meta.updateURL ||
-          script.custom.downloadURL ||
-          script.meta.downloadURL ||
-          script.custom.lastInstallURL
-        );
-      },
-      homepageURL: function () {
-        var script = this.script;
-        return script.custom.homepageURL || script.meta.homepageURL || script.meta.homepage;
-      },
-      author: function () {
-        var text = this.script.meta.author;
-        if (!text) return;
-        var matches = text.match(/^(.*?)\s<(\S*?@\S*?)>$/);
-        return {
-          email: matches && matches[2],
-          name: matches ? matches[1] : text,
-        };
-      },
-      labelEnable: function () {
-        return this.script.enabled ? _.i18n('buttonDisable') : _.i18n('buttonEnable');
-      },
-    },
-    ready: function () {
-      var _this = this;
-      _this.$el.addEventListener('dragstart', _this.onDragStart.bind(_this), false);
-      var icon = _this.script.meta.icon;
-      if (icon && icon !== _this.safeIcon) {
-        _this.loadImage(icon)
-        .then(function (url) {
-          _this.safeIcon = url;
-        }, function () {
-          _this.safeIcon = DEFAULT_ICON;
-        });
-      }
-    },
-    methods: {
-      getLocaleString: function (key) {
-        return _.getLocaleString(this.script.meta, key);
-      },
-      loadImage: function () {
-        var images = {};
-        return function (url) {
-          if (!url) return Promise.reject();
-          var promise = images[url];
-          if (!promise) {
-            var cache = store.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[url] = promise;
-          }
-          return promise;
-        };
-      }(),
-      onEdit: function () {
-        var _this = this;
-        _this.$dispatch('EditScript', _this.script.id);
-      },
-      onRemove: function () {
-        var _this = this;
-        _.sendMessage({
-          cmd: 'RemoveScript',
-          data: _this.script.id,
-        });
-      },
-      onEnable: function () {
-        var _this = this;
-        _.sendMessage({
-          cmd: 'UpdateScriptInfo',
-          data: {
-            id: _this.script.id,
-            enabled: _this.script.enabled ? 0 : 1,
-          },
-        });
-      },
-      onUpdate: function () {
-        _.sendMessage({
-          cmd: 'CheckUpdate',
-          data: this.script.id,
-        });
-      },
-      onDragStart: function (e) {
-        var _this = this;
-        new DND(e, function (data) {
-          _this.$dispatch('MoveScript', data);
-        });
-      },
-    },
-  };
-
-  function DND(e, cb) {
-    var _this = this;
-    _this.mousemove = _this.mousemove.bind(_this);
-    _this.mouseup = _this.mouseup.bind(_this);
-    if (e) {
-      e.preventDefault();
-      _this.start(e);
-    }
-    _this.onDrop = cb;
-  }
-  DND.prototype.start = function (e) {
-    var _this = this;
-    var dragging = _this.dragging = {};
-    var el = dragging.el = e.currentTarget;
-    var parent = el.parentNode;
-    var rect = el.getBoundingClientRect();
-    dragging.offset = {
-      x: e.clientX - rect.left,
-      y: e.clientY - rect.top,
-    };
-    var next = el.nextElementSibling;
-    dragging.delta = (next ? next.getBoundingClientRect().top : parent.offsetHeight) - rect.top;
-    dragging.lastIndex = dragging.index = [].indexOf.call(parent.children, el);
-    dragging.elements = [].filter.call(parent.children, function (el) {
-      return el !== dragging.el;
-    });
-    var dragged = dragging.dragged = el.cloneNode(true);
-    dragged.classList.add('dragging');
-    dragged.style.left = rect.left + 'px';
-    dragged.style.top = rect.top + 'px';
-    dragged.style.width = rect.width + 'px';
-    parent.appendChild(dragged);
-    el.classList.add('dragging-placeholder');
-    document.addEventListener('mousemove', _this.mousemove, false);
-    document.addEventListener('mouseup', _this.mouseup, false);
-  };
-  DND.prototype.mousemove = function (e) {
-    var _this = this;
-    var dragging = _this.dragging;
-    var dragged = dragging.dragged;
-    dragged.style.left = e.clientX - dragging.offset.x + 'px';
-    dragged.style.top = e.clientY - dragging.offset.y + 'px';
-    var hoveredIndex = dragging.elements.findIndex(function (el) {
-      if (!el) return;
-      if (el.classList.contains('dragging-moving')) return;
-      var rect = el.getBoundingClientRect();
-      var pad = 10;
-      return (
-        e.clientX >= rect.left + pad
-        && e.clientX <= rect.left + rect.width - pad
-        && e.clientY >= rect.top + pad
-        && e.clientY <= rect.top + rect.height - pad
-      );
-    });
-    if (~hoveredIndex) {
-      var hoveredEl = dragging.elements[hoveredIndex];
-      var lastIndex = dragging.lastIndex;
-      var isDown = hoveredIndex >= lastIndex;
-      var el = dragging.el;
-      var delta = dragging.delta;
-      if (isDown) {
-        hoveredIndex ++;
-        hoveredEl.parentNode.insertBefore(el, hoveredEl.nextElementSibling);
-      } else {
-        delta = -delta;
-        hoveredEl.parentNode.insertBefore(el, hoveredEl);
-      }
-      dragging.lastIndex = hoveredIndex;
-      _this.animate(dragging.elements.slice(
-        isDown ? lastIndex : hoveredIndex,
-        isDown ? hoveredIndex : lastIndex
-      ), delta);
-    }
-    _this.checkScroll(e.clientY);
-  };
-  DND.prototype.animate = function (elements, delta) {
-    function endAnimation(e) {
-      e.target.classList.remove('dragging-moving');
-      e.target.removeEventListener('transitionend', endAnimation, false);
-    }
-    elements.forEach(function (el) {
-      if (!el) return;
-      el.classList.add('dragging-moving');
-      el.style.transition = 'none';
-      el.style.transform = 'translateY(' + delta + 'px)';
-      el.addEventListener('transitionend', endAnimation, false);
-      setTimeout(function () {
-        el.style.transition = '';
-        el.style.transform = '';
-      });
-    });
-  };
-  DND.prototype.mouseup = function () {
-    var _this = this;
-    document.removeEventListener('mousemove', _this.mousemove, false);
-    document.removeEventListener('mouseup', _this.mouseup, false);
-    var dragging = _this.dragging;
-    dragging.dragged.remove();
-    dragging.el.classList.remove('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, 32);
-        } else dragging.scrolling = false;
-      }
-    }
-    var _this = this;
-    if (!_this.dragging.scrolling) {
-      _this.dragging.scrolling = true;
-      scroll();
-    }
-  };
-});

+ 0 - 14
src/options/components/sync-service.html

@@ -1,14 +0,0 @@
-<div class="line">
-  <label>
-    <input type=checkbox v-setting="service.name+'Enabled'"
-      v-el:enabled @change="update">
-    <span>{{labelText}}</span>
-  </label>
-  <button @click="authenticate" :disabled="service.authState!=='unauthorized'">
-    {{labelAuthenticate}}
-  </button>
-  <button :disabled="disableSync" class="sync-start" @click="retry">
-    <svg class="icon"><use xlink:href="#refresh"/></svg>
-  </button>
-  <span>{{message}}</span>
-</div>

+ 0 - 61
src/options/components/sync-service.js

@@ -1,61 +0,0 @@
-define('views/SyncService', function (require, _exports, module) {
-  var cache = require('cache');
-  var _ = require('utils/common');
-
-  module.exports = {
-    props: ['service'],
-    template: cache.get('/options/components/sync-service.html'),
-    computed: {
-      labelText: function () {
-        var service = this.service;
-        return _.i18n('labelSyncTo', service.displayName || service.name);
-      },
-      labelAuthenticate: function () {
-        return {
-          authorized: _.i18n('buttonAuthorized'),
-          authorizing: _.i18n('buttonAuthorizing'),
-        }[this.service.authState] || _.i18n('buttonAuthorize');
-      },
-      disableSync: function () {
-        var service = this.service;
-        return !!(
-          ['authorized', 'error'].indexOf(service.authState) < 0 ||
-          ~['ready', 'syncing'].indexOf(service.syncState)
-        );
-      },
-      message: function () {
-        var service = this.service;
-        if (service.authState === 'initializing') return _.i18n('msgSyncInit');
-        if (service.authState === 'error') return _.i18n('msgSyncInitError');
-        if (service.syncState === 'error') return _.i18n('msgSyncError');
-        if (service.syncState === 'ready') return _.i18n('msgSyncReady');
-        if (service.syncState === 'syncing') {
-          var progress = '';
-          if (service.progress && service.progress.total) {
-            progress = ' (' + service.progress.finished + '/' + service.progress.total + ')';
-          }
-          return _.i18n('msgSyncing') + progress;
-        }
-        if (service.lastSync) {
-          var lastSync = new Date(service.lastSync).toLocaleString();
-          return _.i18n('lastSync', lastSync);
-        }
-      },
-    },
-    methods: {
-      retry: function () {
-        _.sendMessage({
-          cmd: 'SyncStart',
-          data: this.service.name,
-        });
-      },
-      authenticate: function () {
-        _.sendMessage({cmd: 'Authenticate', data: this.service.name});
-      },
-      update: function () {
-        var _this = this;
-        _this.$els.enabled.checked && _this.$dispatch('EnableService', _this.service.name);
-      },
-    },
-  };
-});

+ 0 - 28
src/options/components/tab-about.html

@@ -1,28 +0,0 @@
-<div class="content">
-  <h1>
-    <span v-text="'labelAbout'|i18n"></span>
-    <small>v{{version}}</small>
-  </h1>
-  <div class=line v-text="'extDescription'|i18n"></div>
-  <div class=line>
-    <label v-text="'labelRelated'|i18n"></label>
-    <span v-html="'anchorSupportPage'|i18n"></span> |
-    <a href=https://gerald.top/~donate target=_blank v-text="'labelDonate'|i18n"></a> |
-    <a href=https://github.com/violentmonkey/violentmonkey/issues target=_blank v-text="'labelFeedback'|i18n"></a>
-  </div>
-  <div class=line>
-    <label v-text="'labelAuthor'|i18n"></label>
-    <span v-html="'anchorAuthor'|i18n"></span>
-  </div>
-  <div class=line>
-    <label v-text="'labelTranslator'|i18n"></label>
-    <span v-html="'anchorTranslator'|i18n"></span>
-  </div>
-  <div class=line>
-    <label v-text="'labelCurrentLang'|i18n"></label>
-    <span id="currentLang">{{language}}</span> |
-    <a href=https://violentmonkey.github.io/localization.html#nex target=_blank>
-      Help with translation
-    </a>
-  </div>
-</div>

+ 0 - 14
src/options/components/tab-about.js

@@ -1,14 +0,0 @@
-define('views/TabAbout', function (require, _exports, module) {
-  var cache = require('cache');
-  var data = {
-    version: chrome.app.getDetails().version,
-    language: navigator.language,
-  };
-
-  module.exports = {
-    template: cache.get('/options/components/tab-about.html'),
-    data: function () {
-      return data;
-    },
-  };
-});

+ 0 - 18
src/options/components/tab-installed.html

@@ -1,18 +0,0 @@
-<div class="content no-pad">
-  <header>
-    <button v-text="'buttonNew'|i18n" @click="newScript"></button>
-    <button v-text="'buttonUpdateAll'|i18n" @click="updateAll"></button>
-    <button v-text="'buttonInstallFromURL'|i18n" @click="installFromURL"></button>
-    <div class="pull-right">
-      <a href=https://greasyfork.org/scripts target=_blank v-text="'anchorGetMoreScripts'|i18n"></a>
-    </div>
-  </header>
-  <div class="backdrop" :class="{mask:loading}" v-show="message">
-    <div>{{{message}}}</div>
-  </div>
-  <div class="scripts">
-    <script-item v-for="script in store.scripts"
-      :script="script"></script-item>
-  </div>
-  <edit v-if="script" :script.sync="script"></edit>
-</div>

+ 0 - 85
src/options/components/tab-installed.js

@@ -1,85 +0,0 @@
-define('views/TabInstalled', function (require, _exports, module) {
-  var ScriptItem = require('views/Script');
-  var Edit = require('views/Edit');
-  var cache = require('cache');
-  var _ = require('utils/common');
-  var store = require('utils').store;
-
-  module.exports = {
-    template: cache.get('/options/components/tab-installed.html'),
-    components: {
-      ScriptItem: ScriptItem,
-      Edit: Edit,
-    },
-    events: {
-      EditScript: function (id) {
-        var _this = this;
-        _this.script = _this.store.scripts.find(function (script) {
-          return script.id === id;
-        });
-      },
-      MoveScript: function (data) {
-        var _this = this;
-        if (data.from === data.to) return;
-        _.sendMessage({
-          cmd: 'Move',
-          data: {
-            id: _this.store.scripts[data.from].id,
-            offset: data.to - data.from,
-          },
-        })
-        .then(function () {
-          var scripts = _this.store.scripts;
-          var i = Math.min(data.from, data.to);
-          var j = Math.max(data.from, data.to);
-          var seq = [
-            scripts.slice(0, i),
-            scripts.slice(i, j + 1),
-            scripts.slice(j + 1),
-          ];
-          i === data.to
-          ? seq[1].unshift(seq[1].pop())
-          : seq[1].push(seq[1].shift());
-          _this.store.scripts = seq.concat.apply([], seq);
-        });
-      },
-    },
-    data: function () {
-      return {
-        script: null,
-        store: store,
-      };
-    },
-    computed: {
-      message: function () {
-        var _this = this;
-        if (_this.store.loading) {
-          return _.i18n('msgLoading');
-        }
-        if (!_this.store.scripts.length) {
-          return _.i18n('labelNoScripts');
-        }
-      },
-    },
-    methods: {
-      newScript: function () {
-        var _this = this;
-        _.sendMessage({cmd: 'NewScript'})
-        .then(function (script) {
-          _this.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),
-          });
-        }
-      },
-    },
-  };
-});

+ 0 - 40
src/options/components/tab-settings.html

@@ -1,40 +0,0 @@
-<div class="content">
-<h1 v-text="'labelSettings'|i18n"></h1>
-<label class="line">
-  <input type=checkbox v-setting="'autoUpdate'" @change="updateAutoUpdate">
-  <span v-text="'labelAutoUpdate'|i18n"></span>
-</label>
-<label class="line">
-  <input type=checkbox v-setting="'ignoreGrant'">
-  <span v-text="'labelIgnoreGrant'|i18n"></span>
-</label>
-<label class="line">
-  <input type=checkbox v-setting="'autoReload'">
-  <span v-text="'labelAutoReloadCurrentTab'|i18n"></span>
-</label>
-<fieldset class=title>
-  <legend v-text="'labelDataImport'|i18n"></legend>
-  <button v-text="'buttonImportData'|i18n" @click="importFile"></button>
-  <button :title="'hintVacuum'|i18n" @click="vacuum" :disabled="vacuuming" v-text="labelVacuum"></button>
-</fieldset>
-<fieldset class=title>
-  <legend v-text="'labelDataExport'|i18n"></legend>
-  <b v-text="'labelScriptsToExport'|i18n"></b>
-  <label>
-    <input type=checkbox v-setting="'exportValues'">
-    <span v-text="'labelExportScriptData'|i18n"></span>
-  </label>
-  <select class=export-list multiple v-model="selectedIds">
-    <option class="ellipsis" v-for="script in store.scripts"
-      :value="script.id" v-text="script.custom.name||script.meta.name"></option>
-  </select>
-  <button v-text="'buttonAllNone'|i18n" @click="updateSelection()"></button>
-  <button v-text="'buttonExportData'|i18n" @click="exportData" :disabled="exporting"></button>
-</fieldset>
-<fieldset class=title v-feature.literal="sync">
-  <legend v-text="'labelSync'|i18n" class="feature-text"></legend>
-  <div class="sync-services">
-    <sync-service v-for="service in store.sync" :service="service"></sync-service>
-  </div>
-</fieldset>
-</div>

+ 0 - 258
src/options/components/tab-settings.js

@@ -1,258 +0,0 @@
-define('views/TabSettings', function (require, _exports, module) {
-  function importData(file) {
-    function forEach(obj, cb) {
-      obj && Object.keys(obj).forEach(function (key) {
-        var value = obj[key];
-        cb(value, key);
-      });
-    }
-    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) {
-        value && _.sendMessage({
-          cmd: 'SetValue',
-          data: {
-            uri: key,
-            values: value,
-          }
-        });
-      });
-      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) {
-              delete more.id;
-              script.more = more;
-            }
-          }
-          _.sendMessage({
-            cmd: 'ParseScript',
-            data: script,
-          }).then(function () {
-            resolve(true);
-          });
-        });
-      });
-    }
-    function getVMFiles(entries) {
-      var i = entries.findIndex(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,
-          });
-        });
-      });
-      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 res.filter(function (item) {return item;}).length;
-      });
-    }).then(function (count) {
-      new Message({text: _.i18n('msgImported', [count])});
-    });
-  }
-  function exportData(selectedIds) {
-    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);
-        });
-      });
-    }
-    function download(writer) {
-      return new Promise(function (resolve, _reject) {
-        writer.close(function (blob) {
-          resolve(blob);
-        });
-      }).then(function (blob) {
-        var url = URL.createObjectURL(blob);
-        var a = document.createElement('a');
-        a.href = url;
-        a.download = 'scripts.zip';
-        a.click();
-        setTimeout(function () {
-          URL.revokeObjectURL(url);
-        });
-      });
-    }
-    if (!selectedIds.length) return;
-    var withValues = _.options.get('exportValues');
-    _.sendMessage({
-      cmd: 'ExportZip',
-      data: {
-        values: withValues,
-        ids: selectedIds,
-      }
-    }).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] = ['id', 'custom', 'enabled', 'update'].reduce(function (res, key) {
-          res[key] = script[key];
-          return res;
-        }, {});
-        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);
-    });
-  }
-
-  var Message = require('views/Message');
-  var SyncService = require('views/SyncService');
-  var store = require('utils').store;
-  var cache = require('cache');
-  var _ = require('utils/common');
-
-  module.exports = {
-    template: cache.get('/options/components/tab-settings.html'),
-    components: {
-      SyncService: SyncService,
-    },
-    data: function () {
-      return {
-        store: store,
-        selectedIds: [],
-        exporting: false,
-        vacuuming: false,
-        labelVacuum: _.i18n('buttonVacuum'),
-      };
-    },
-    watch: {
-      'store.scripts': function () {
-        this.updateSelection(true);
-      },
-    },
-    events: {
-      EnableService: function (name) {
-        store.sync.forEach(function (service) {
-          if (service.name !== name) {
-            var key = service.name + 'Enabled';
-            var enabled = _.options.get(key);
-            if (enabled) {
-              _.options.set(key, false);
-            }
-          }
-        });
-        _.sendMessage({cmd: 'SyncStart'});
-      },
-    },
-    created: function () {
-      this.updateSelection(true);
-    },
-    methods: {
-      updateAutoUpdate: function () {
-        _.sendMessage({cmd: 'AutoUpdate'});
-      },
-      updateSelection: function (select) {
-        var _this = this;
-        if (!store.scripts.length) return;
-        if (select == null) select = _this.selectedIds.length < store.scripts.length;
-        if (select) {
-          _this.selectedIds = store.scripts.map(function (script) {
-            return script.id;
-          });
-        } else {
-          _this.selectedIds = [];
-        }
-      },
-      importFile: function () {
-        var input = document.createElement('input');
-        input.type = 'file';
-        input.accept = '.zip';
-        input.onchange = function () {
-          input.files && input.files.length && importData(input.files[0]);
-        };
-        input.click();
-      },
-      exportData: function () {
-        var _this = this;
-        _this.exporting = true;
-        exportData(_this.selectedIds)
-        .catch(_.noop)
-        .then(function () {
-          _this.exporting = false;
-        });
-      },
-      vacuum: function () {
-        var _this = this;
-        _this.vacuuming = true;
-        _this.labelVacuum = _.i18n('buttonVacuuming');
-        _.sendMessage({cmd: 'Vacuum'})
-        .then(function () {
-          _this.vacuuming = false;
-          _this.labelVacuum = _.i18n('buttonVacuumed');
-        });
-      },
-    },
-  };
-});

+ 2 - 2
src/options/index.html

@@ -7,12 +7,12 @@
   <link rel="stylesheet" href="style.css">
   <script src="/lib/vue.min.js"></script>
   <script src="/lib/zip.js/zip.js"></script>
-  <script src="/lib/require-lite.js"></script>
+  <script src="/lib/define.js"></script>
   <script src="/common.js"></script>
   <script src="/cache.js"></script>
 </head>
 <body>
-  <component :is="type" :params="params"></component>
+  <div id="app"></div>
   <script src="app.js"></script>
 </body>
 </html>

+ 3 - 2
src/options/style.css

@@ -348,11 +348,12 @@ fieldset.title {
   border-bottom-right-radius: .2rem;
   box-shadow: 0 0 .2rem rgba(0,0,0,.2);
 }
-.message-transition {
+.message-enter-active,
+.message-leave-active {
   transition: transform .5s;
 }
 .message-enter,
-.message-leave {
+.message-leave-active {
   transform: translateY(-120%);
 }
 .icon {

+ 21 - 31
src/options/utils/dropdown.js

@@ -1,32 +1,22 @@
-define('utils/dropdown', function () {
-  Vue.directive('dropdown', {
-    bind: function () {
-      var _this = this;
-      var dropdown = _this.data = {
-        toggle: _this.el.querySelector('[dropdown-toggle]'),
-        data: {
-          isOpen: false,
-        },
-      };
-      var methods = dropdown.methods = {
-        onClose: function (e) {
-          if (e && _this.el && _this.el.contains(e.target)) return;
-          dropdown.data.isOpen = false;
-          _this.el.classList.remove('open');
-          document.removeEventListener('mousedown', methods.onClose, false);
-        },
-        onOpen: function (_e) {
-          dropdown.data.isOpen = true;
-          _this.el.classList.add('open');
-          document.addEventListener('mousedown', methods.onClose, false);
-        },
-        onToggle: function (_e) {
-          if (dropdown.data.isOpen) methods.onClose();
-          else methods.onOpen();
-        },
-      };
-      dropdown.toggle.addEventListener('click', methods.onToggle, false);
-      _this.el.classList.add('dropdown');
-    },
-  });
+Vue.directive('dropdown', {
+  bind: function (el) {
+    function onClose(e) {
+      if (e && el.contains(e.target)) return;
+      isOpen = false;
+      el.classList.remove('open');
+      document.removeEventListener('mousedown', onClose, false);
+    }
+    function onOpen(_e) {
+      isOpen = true;
+      el.classList.add('open');
+      document.addEventListener('mousedown', onClose, false);
+    }
+    function onToggle(_e) {
+      isOpen ? onClose() : onOpen();
+    }
+    var toggle = el.querySelector('[dropdown-toggle]');
+    var isOpen = false;
+    toggle.addEventListener('click', onToggle, false);
+    el.classList.add('dropdown');
+  },
 });

+ 27 - 51
src/options/utils/features.js

@@ -1,55 +1,31 @@
-define('utils/features', function (require, exports, _module) {
-  var _ = require('utils/common');
+var _ = require('../../common');
 
-  var key = 'features';
-  var features = _.options.get(key);
-  if (!features || !features.data) features = {
-    data: {},
-  };
+var key = 'features';
+var features = _.options.get(key);
+if (!features || !features.data) features = {
+  data: {},
+};
 
-  exports.reset = function (version) {
-    if (features.version !== version) {
-      _.options.set(key, features = {
-        version: version,
-        data: {},
-      });
-    }
-  };
-
-  Vue.directive('feature', {
-    bind: function () {
-      function onClick() {
-        features.data[_this.value] = 1;
-        _.options.set(key, features);
-        _this.hideFeature();
-      }
+exports.reset = function (version) {
+  if (features.version !== version) {
+    _.options.set(key, features = {
+      version: version,
+      data: {},
+    });
+  }
+};
 
-      var _this = this;
-      var el = _this.el;
-      var bound = false;
-      _this.showFeature = function () {
-        if (bound) return;
-        bound = true;
-        el.classList.add('feature');
-        el.addEventListener('click', onClick, false);
-      };
-      _this.hideFeature = function () {
-        if (!bound) return;
-        bound = false;
-        el.classList.remove('feature');
-        el.removeEventListener('click', onClick, false);
-      };
-    },
-    update: function (value) {
-      var _this = this;
-      if (features.data[_this.value = value]) {
-        _this.hideFeature();
-      } else {
-        _this.showFeature();
-      }
-    },
-    unbind: function () {
-      this.hideFeature();
-    },
-  });
+Vue.directive('feature', {
+  bind: function (el, binding) {
+    function onFeatureClick(_e) {
+      features.data[value] = 1;
+      _.options.set(key, features);
+      el.classList.remove('feature');
+      el.removeEventListener('click', onFeatureClick, false);
+    }
+    var value = binding.value;
+    if (features.data[value]) return;
+    el.classList.add('feature');
+    el.addEventListener('click', onFeatureClick, false);
+  },
 });

+ 30 - 31
src/options/utils/index.js

@@ -1,35 +1,34 @@
-define('utils', function (require, exports, _module) {
-  function routeTester(paths) {
-    var routes = paths.map(function (path) {
-      var names = [];
-      path = path.replace(/:(\w+)/g, function (_param, name) {
-        names.push(name);
-        return '([^/]+)';
-      });
-      return {
-        re: new RegExp('^' + path + '$'),
-        names: names,
-      };
+function routeTester(paths) {
+  var routes = paths.map(function (path) {
+    var names = [];
+    path = path.replace(/:(\w+)/g, function (_param, name) {
+      names.push(name);
+      return '([^/]+)';
     });
-    return function (url) {
-      var length = routes.length;
-      for (var i = 0; i < length; i ++) {
-        var route = routes[i];
-        var matches = url.match(route.re);
-        if (matches) {
-          return route.names.reduce(function (params, name, i) {
-            params[name] = decodeURIComponent(matches[i + 1]);
-            return params;
-          }, {});
-        }
-      }
+    return {
+      re: new RegExp('^' + path + '$'),
+      names: names,
     };
-  }
+  });
+  return function (url) {
+    var length = routes.length;
+    for (var i = 0; i < length; i ++) {
+      var route = routes[i];
+      var matches = url.match(route.re);
+      if (matches) {
+        return route.names.reduce(function (params, name, i) {
+          params[name] = decodeURIComponent(matches[i + 1]);
+          return params;
+        }, {});
+      }
+    }
+  };
+}
 
-  exports.routeTester = routeTester;
-  exports.store = {};
+exports.routeTester = routeTester;
+exports.store = {};
+exports.events = new Vue;
+exports.features = require('./features');
 
-  require('utils/dropdown');
-  require('utils/features');
-  require('utils/settings');
-});
+require('./dropdown');
+require('./settings');

+ 35 - 24
src/options/utils/settings.js

@@ -1,27 +1,38 @@
-define('utils/settings', function (require, _exports, _module) {
-  var _ = require('utils/common');
+var _ = require('../../common');
 
-  Vue.directive('setting', {
-    bind: function () {
-      var _this = this;
-      _this.onChange = function () {
-        _.options.set(_this.value, _this.el.checked);
-      };
-      _this.el.addEventListener('change', _this.onChange, false);
-      _this.revoke = _.options.hook(function (value, key) {
-        if (key === _this.value) {
-          _this.el.checked = value;
-        }
-      });
-    },
-    update: function (value) {
-      var _this = this;
-      _this.el.checked = _.options.get(_this.value = value);
-    },
-    unbind: function () {
-      var _this = this;
-      _this.el.removeEventListener('change', _this.onChange, false);
-      _this.revoke();
-    },
+var hooks = {};
+_.options.hook(function (value, key) {
+  var list = hooks[key];
+  list && list.forEach(function (el) {
+    el.checked = value;
   });
+  setTimeout(onChanged, 0, value, key);
+});
+
+function onSettingChange(e) {
+  var target = e.target;
+  _.options.set(target.dataset.setting, target.checked);
+}
+function onChanged(value, key) {
+  if (value && key === 'closeAfterInstall') {
+    _.options.set('trackLocalFile', false);
+  }
+}
+
+Vue.directive('setting', {
+  bind: function (el, binding) {
+    var value = binding.value;
+    el.dataset.setting = value;
+    el.addEventListener('change', onSettingChange, false);
+    var list = hooks[value] = hooks[value] || [];
+    list.push(el);
+    el.checked = _.options.get(value);
+  },
+  unbind: function (el, binding) {
+    var value = binding.value;
+    el.removeEventListener('change', onSettingChange, false);
+    var list = hooks[value] || [];
+    var i = list.indexOf(el);
+    ~i && list.splice(i, 1);
+  },
 });

+ 6 - 6
src/options/components/confirm.html → src/options/views/confirm.html

@@ -2,23 +2,23 @@
   <div class=frame-header>
     <div class=buttons>
       <div v-dropdown>
-        <button dropdown-toggle v-text="'buttonInstallOptions'|i18n"></button>
+        <button dropdown-toggle v-text="i18n('buttonInstallOptions')"></button>
         <div class="dropdown-menu options-panel" @mousedown.stop>
           <label>
             <input type=checkbox v-setting="'closeAfterInstall'">
-            <span v-text="'installOptionClose'|i18n"></span>
+            <span v-text="i18n('installOptionClose')"></span>
           </label>
           <label>
             <input type=checkbox v-setting="'trackLocalFile'" :disabled="closeAfterInstall">
-            <span v-text="'installOptionTrack'|i18n"></span>
+            <span v-text="i18n('installOptionTrack')"></span>
           </label>
         </div>
       </div>
-      <button v-text="'buttonConfirmInstallation'|i18n"
+      <button v-text="i18n('buttonConfirmInstallation')"
         :disabled="!installable" @click="installScript"></button>
-      <button v-text="'buttonClose'|i18n" @click="close"></button>
+      <button v-text="i18n('buttonClose')" @click="close"></button>
     </div>
-    <h1><span v-text="'labelInstall'|i18n"></span> - <span v-text="'extName'|i18n"></span></h1>
+    <h1><span v-text="i18n('labelInstall')"></span> - <span v-text="i18n('extName')"></span></h1>
     <div class="ellipsis confirm-url" :title="params.url">{{params.url}}</div>
     <div class="ellipsis confirm-msg">{{message}}</div>
   </div>

+ 184 - 0
src/options/views/confirm.js

@@ -0,0 +1,184 @@
+var Editor = require('./editor');
+var cache = require('../../cache');
+var _ = require('../../common');
+
+module.exports = {
+  props: ['params'],
+  components: {
+    Editor: Editor,
+  },
+  template: cache.get('./confirm.html'),
+  data: function () {
+    return {
+      installable: false,
+      dependencyOK: false,
+      message: '',
+      code: '',
+      require: {},
+      resources: {},
+      closeAfterInstall: _.options.get('closeAfterInstall'),
+    };
+  },
+  computed: {
+    isLocal: function () {
+      return /^file:\/\/\//.test(this.params.url);
+    },
+  },
+  mounted: function () {
+    var _this = this;
+    _this.message = _.i18n('msgLoadingData');
+    _this.loadData().then(function () {
+      _this.parseMeta();
+    });
+    _this.revoke = _.options.hook('closeAfterInstall', function (value) {
+      _this.closeAfterInstall = value;
+    });
+  },
+  beforeDestroy: function () {
+    this.revoke();
+  },
+  methods: {
+    loadData: function (changedOnly) {
+      var _this = this;
+      _this.installable = false;
+      var oldCode = _this.code;
+      return _this.getScript(_this.params.url)
+      .then(function (code) {
+        if (changedOnly && oldCode === code) return Promise.reject();
+        _this.code = code;
+      })
+    },
+    parseMeta: function () {
+      var _this = this;
+      return _.sendMessage({
+        cmd: 'ParseMeta',
+        data: _this.code,
+      })
+      .then(function (script) {
+        var urls = Object.keys(script.resources).map(function (key) {
+          return script.resources[key];
+        });
+        var length = script.require.length + urls.length;
+        if (!length) return;
+        var finished = 0;
+        var error = [];
+        var updateStatus = function () {
+          _this.message = _.i18n('msgLoadingDependency', [finished, length]);
+        };
+        updateStatus();
+        var promises = script.require.map(function (url) {
+          return _this.getFile(url).then(function (res) {
+            _this.require[url] = res;
+          });
+        });
+        promises = promises.concat(urls.map(function (url) {
+          return _this.getFile(url, true).then(function (res) {
+            _this.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.dependencyOK = true;
+        });
+      })
+      .then(function () {
+        _this.message = _.i18n('msgLoadedData');
+        _this.installable = true;
+      }, function (err) {
+        _this.message = _.i18n('msgErrorLoadingDependency', [err]);
+        return Promise.reject();
+      });
+    },
+    close: function () {
+      window.close();
+    },
+    getFile: function (url, isBlob) {
+      return new Promise(function (resolve, reject) {
+        var xhr = new XMLHttpRequest;
+        xhr.open('GET', url, true);
+        if (isBlob) xhr.responseType = 'blob';
+        xhr.onloadend = function () {
+          if (xhr.status > 300) return reject(url);
+          if (isBlob) {
+            var reader = new FileReader;
+            reader.onload = function () {
+              resolve(window.btoa(this.result));
+            };
+            reader.readAsBinaryString(xhr.response);
+          } else {
+            resolve(xhr.responseText);
+          }
+        };
+        xhr.send();
+      });
+    },
+    getScript: function (url) {
+      var _this = this;
+      return _.sendMessage({
+        cmd: 'GetFromCache',
+        data: url,
+      })
+      .then(function (text) {
+        return text || Promise.reject();
+      })
+      .catch(function () {
+        return _this.getFile(url);
+      })
+      .catch(function (url) {
+        _this.message = _.i18n('msgErrorLoadingData');
+        throw url;
+      });
+    },
+    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.installable = false;
+      _.sendMessage({
+        cmd:'ParseScript',
+        data:{
+          url: _this.params.url,
+          from: _this.params.referer,
+          code: _this.code,
+          require: _this.require,
+          resources: _this.resources,
+        },
+      })
+      .then(function (res) {
+        _this.message = res.message + '[' + _this.getTimeString() + ']';
+        if (res.code < 0) return;
+        if (_.options.get('closeAfterInstall')) _this.close();
+        else if (_this.isLocal && _.options.get('trackLocalFile')) _this.trackLocalFile();
+      });
+    },
+    trackLocalFile: function () {
+      var _this = this;
+      new Promise(function (resolve) {
+        setTimeout(resolve, 2000);
+      })
+      .then(function () {
+        return _this.loadData(true).then(function () {
+          return _this.parseMeta();
+        });
+      })
+      .then(function () {
+        var track = _.options.get('trackLocalFile');
+        track && _this.installScript();
+      }, function () {
+        _this.trackLocalFile();
+      });
+    },
+  },
+};

+ 26 - 26
src/options/components/edit.html → src/options/views/edit.html

@@ -2,18 +2,18 @@
   <div class="frame-header">
     <div class="buttons">
       <div v-dropdown>
-        <button dropdown-toggle v-text="'buttonCustomMeta'|i18n"></button>
+        <button dropdown-toggle v-text="i18n('buttonCustomMeta')"></button>
         <div class="dropdown-menu">
           <table>
             <tr>
-              <td title="@name" v-text="'labelName'|i18n"></td>
+              <td title="@name" v-text="i18n('labelName')"></td>
               <td class=expand>
-                <input type="text" v-model="custom.name" placeholder="{{placeholders.name}}">
+                <input type="text" v-model="custom.name" :placeholder="placeholders.name">
               </td>
-              <td title="@run-at" v-text="'labelRunAt'|i18n"></td>
+              <td title="@run-at" v-text="i18n('labelRunAt')"></td>
               <td>
                 <select v-model="custom['run-at']">
-                  <option value="" v-text="'labelRunAtDefault'|i18n"></option>
+                  <option value="" v-text="i18n('labelRunAtDefault')"></option>
                   <option value=start>document-start</option>
                   <option value=idle>document-idle</option>
                   <option value=end>document-end</option>
@@ -21,76 +21,76 @@
               </td>
             </tr>
             <tr title="@homepageURL">
-              <td v-text="'labelHomepageURL'|i18n"></td>
+              <td v-text="i18n('labelHomepageURL')"></td>
               <td colspan=3 class=expand>
-                <input type="text" v-model="custom.homepageURL" placeholder="{{placeholders.homepageURL}}">
+                <input type="text" v-model="custom.homepageURL" :placeholder="placeholders.homepageURL">
               </td>
             </tr>
           </table>
           <table>
             <tr title="@updateURL">
-              <td v-text="'labelUpdateURL'|i18n"></td>
+              <td v-text="i18n('labelUpdateURL')"></td>
               <td class=expand>
-                <input type="text" v-model="custom.updateURL" placeholder="{{placeholders.updateURL}}">
+                <input type="text" v-model="custom.updateURL" :placeholder="placeholders.updateURL">
               </td>
             </tr>
             <tr title="@downloadURL">
-              <td v-text="'labelDownloadURL'|i18n"></td>
+              <td v-text="i18n('labelDownloadURL')"></td>
               <td class=expand>
-                <input type="text" v-model="custom.downloadURL" placeholder="{{placeholders.downloadURL}}">
+                <input type="text" v-model="custom.downloadURL" :placeholder="placeholders.downloadURL">
               </td>
             </tr>
           </table>
           <fieldset title="@include">
             <legend>
-              <span v-text="'labelInclude'|i18n"></span>
+              <span v-text="i18n('labelInclude')"></span>
               <label>
                 <input type=checkbox v-model="custom.keepInclude">
-                <span v-text="'labelKeepInclude'|i18n"></span>
+                <span v-text="i18n('labelKeepInclude')"></span>
               </label>
             </legend>
-            <div v-html="'labelCustomInclude'|i18n"></div>
+            <div v-html="i18n('labelCustomInclude')"></div>
             <textarea v-model="custom.include"></textarea>
           </fieldset>
           <fieldset title="@match">
             <legend>
-              <span v-text="'labelMatch'|i18n"></span>
+              <span v-text="i18n('labelMatch')"></span>
               <label>
                 <input type=checkbox v-model="custom.keepMatch">
-                <span v-text="'labelKeepMatch'|i18n"></span>
+                <span v-text="i18n('labelKeepMatch')"></span>
               </label>
             </legend>
-            <div v-html="'labelCustomMatch'|i18n"></div>
+            <div v-html="i18n('labelCustomMatch')"></div>
             <textarea v-model="custom.match"></textarea>
           </fieldset>
           <fieldset title="@exclude">
             <legend>
-              <span v-text="'labelExclude'|i18n"></span>
+              <span v-text="i18n('labelExclude')"></span>
               <label>
                 <input type=checkbox v-model="custom.keepExclude">
-                <span v-text="'labelKeepExclude'|i18n"></span>
+                <span v-text="i18n('labelKeepExclude')"></span>
               </label>
             </legend>
-            <div v-html="'labelCustomExclude'|i18n"></div>
+            <div v-html="i18n('labelCustomExclude')"></div>
             <textarea v-model="custom.exclude"></textarea>
           </fieldset>
         </div>
       </div>
     </div>
-    <h2 v-text="'labelScriptEditor'|i18n"></h2>
+    <h2 v-text="i18n('labelScriptEditor')"></h2>
   </div>
   <div class="frame-footer">
     <div class="pull-right">
-      <button v-text="'buttonSave'|i18n" @click="save" :disabled="!canSave"></button>
-      <button v-text="'buttonSaveClose'|i18n" @click="saveClose" :disabled="!canSave"></button>
-      <button v-text="'buttonClose'|i18n" @click="close"></button>
+      <button v-text="i18n('buttonSave')" @click="save" :disabled="!canSave"></button>
+      <button v-text="i18n('buttonSaveClose')" @click="saveClose" :disabled="!canSave"></button>
+      <button v-text="i18n('buttonClose')" @click="close"></button>
     </div>
     <label>
       <input type=checkbox v-model="update">
-      <span v-text="'labelAllowUpdate'|i18n"></span>
+      <span v-text="i18n('labelAllowUpdate')"></span>
     </label>
   </div>
   <div class=frame-body>
-    <editor :on-save="save" :on-exit="close" :content.sync="code"></editor>
+    <editor :on-save="save" :on-exit="close" :on-change="contentChange" :content="code"></editor>
   </div>
 </div>

+ 145 - 0
src/options/views/edit.js

@@ -0,0 +1,145 @@
+function fromList(list) {
+  return (list || []).join('\n');
+}
+function toList(text) {
+  return text.split('\n')
+  .map(function (line) {
+    return line.trim();
+  })
+  .filter(function (item) {
+    return item;
+  });
+}
+
+var Message = require('./message');
+var Editor = require('./editor');
+var cache = require('../../cache');
+var _ = require('../../common');
+
+module.exports = {
+  props: ['script', 'onClose'],
+  template: cache.get('./edit.html'),
+  components: {
+    Editor: Editor,
+  },
+  data: function () {
+    return {
+      canSave: false,
+      update: false,
+      code: '',
+      custom: {},
+    };
+  },
+  computed: {
+    placeholders: function () {
+      var script = this.script;
+      return {
+        name: script.meta.name,
+        homepageURL: script.meta.homepageURL,
+        updateURL: script.meta.updateURL || _.i18n('hintUseDownloadURL'),
+        downloadURL: script.meta.downloadURL || script.lastInstallURL,
+      };
+    },
+  },
+  watch: {
+    custom: {
+      deep: true,
+      handler: function () {
+        this.canSave = true;
+      },
+    },
+  },
+  mounted: function () {
+    var _this = this;
+    (_this.script.id ? _.sendMessage({
+      cmd: 'GetScript',
+      data: _this.script.id,
+    }) : Promise.resolve(_this.script))
+    .then(function (script) {
+      _this.update = script.update;
+      _this.code = script.code;
+      var custom = script.custom;
+      _this.custom = [
+        'name',
+        'homepageURL',
+        'updateURL',
+        'downloadURL',
+      ].reduce(function (value, key) {
+        value[key] = custom[key];
+        return value;
+      }, {
+        keepInclude: custom._include !== false,
+        keepMatch: custom._match !== false,
+        keepExclude: custom._exclude !== false,
+        include: fromList(custom.include),
+        match: fromList(custom.match),
+        exclude: fromList(custom.exclude),
+        'run-at': custom['run-at'] || '',
+      });
+      _this.$nextTick(function () {
+        _this.canSave = false;
+      });
+    });
+  },
+  methods: {
+    save: function () {
+      var _this = this;
+      var custom = _this.custom;
+      var value = [
+        'name',
+        'run-at',
+        'homepageURL',
+        'updateURL',
+        'downloadURL',
+      ].reduce(function (value, key) {
+        value[key] = custom[key];
+        return value;
+      }, {
+        _include: custom.keepInclude,
+        _match: custom.keepMatch,
+        _exclude: custom.keepExclude,
+        include: toList(custom.include),
+        match: toList(custom.match),
+        exclude: toList(custom.exclude),
+      });
+      return _.sendMessage({
+        cmd: 'ParseScript',
+        data: {
+          id: _this.script.id,
+          code: _this.code,
+          // User created scripts MUST be marked `isNew` so that
+          // the backend is able to check namespace conflicts
+          isNew: !_this.script.id,
+          message: '',
+          more: {
+            custom: value,
+            update: _this.update,
+          },
+        },
+      })
+      .then(function (script) {
+        _this.script = script;
+        _this.canSave = false;
+      }, function (err) {
+        Message.open({text: err});
+      });
+    },
+    close: function () {
+      var _this = this;
+      if (!_this.canSave || confirm(_.i18n('confirmNotSaved'))) {
+        _this.onClose();
+      }
+    },
+    saveClose: function () {
+      var _this = this;
+      _this.save().then(function () {
+        _this.close();
+      });
+    },
+    contentChange: function (code) {
+      var _this = this;
+      _this.code = code;
+      _this.canSave = true;
+    },
+  },
+};

+ 0 - 0
src/options/components/editor.html → src/options/views/editor.html


+ 124 - 0
src/options/views/editor.js

@@ -0,0 +1,124 @@
+function addScripts(data) {
+  function add(data) {
+    var s = document.createElement('script');
+    if (data.innerHTML) s.innerHTML = data.innerHTML;
+    else if (data.src) s.src = data.src;
+    return new Promise(function (resolve, reject) {
+      s.onload = function () {
+        resolve();
+      };
+      s.onerror = function () {
+        reject();
+      };
+      document.body.appendChild(s);
+    });
+  }
+  if (!data.map) data = [data];
+  return Promise.all(data.map(add));
+}
+
+function addCSS(data) {
+  function add(data) {
+    var s;
+    if (data.html) {
+      s = document.createElement('style');
+      s.innerHTML = data.html;
+    } else if (data.href) {
+      s = document.createElement('link');
+      s.rel = 'stylesheet';
+      s.href = data.href;
+    }
+    if (s) document.body.appendChild(s);
+  }
+  if (!data.forEach) data = [data];
+  data.forEach(add);
+}
+
+function initCodeMirror() {
+  addCSS([
+    {href: '/lib/CodeMirror/lib/codemirror.css'},
+    {href: '/mylib/CodeMirror/fold.css'},
+    {href: '/mylib/CodeMirror/search.css'},
+  ]);
+  return addScripts(
+    {src: '/lib/CodeMirror/lib/codemirror.js'}
+  ).then(function () {
+    return addScripts([
+      {src: '/lib/CodeMirror/mode/javascript/javascript.js'},
+      {src: '/lib/CodeMirror/addon/comment/continuecomment.js'},
+      {src: '/lib/CodeMirror/addon/edit/matchbrackets.js'},
+      {src: '/lib/CodeMirror/addon/edit/closebrackets.js'},
+      {src: '/lib/CodeMirror/addon/fold/foldcode.js'},
+      {src: '/lib/CodeMirror/addon/fold/foldgutter.js'},
+      {src: '/lib/CodeMirror/addon/fold/brace-fold.js'},
+      {src: '/lib/CodeMirror/addon/fold/comment-fold.js'},
+      {src: '/lib/CodeMirror/addon/search/match-highlighter.js'},
+      {src: '/lib/CodeMirror/addon/search/searchcursor.js'},
+      {src: '/lib/CodeMirror/addon/selection/active-line.js'},
+      {src: '/mylib/CodeMirror/search.js'},
+    ]);
+  });
+}
+
+var cache = require('../../cache');
+var readyCodeMirror = initCodeMirror();
+
+module.exports = {
+  props: [
+    'readonly',
+    'onExit',
+    'onSave',
+    'onChange',
+    'content',
+  ],
+  template: cache.get('./editor.html'),
+  mounted: function () {
+    var _this = this;
+    readyCodeMirror.then(function () {
+      var editor = _this.editor = CodeMirror(_this.$el, {
+        continueComments: true,
+        matchBrackets: true,
+        autoCloseBrackets: true,
+        highlightSelectionMatches: true,
+        lineNumbers: true,
+        mode: 'javascript',
+        lineWrapping: true,
+        styleActiveLine: true,
+        foldGutter: true,
+        gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
+      });
+      _this.readonly && editor.setOption('readOnly', _this.readonly);
+      editor.on('change', function () {
+        _this.cachedContent = editor.getValue();
+        _this.onChange && _this.onChange(_this.cachedContent);
+      });
+      var extraKeys = {};
+      if (_this.onExit) {
+        extraKeys.Esc = _this.onExit;
+      }
+      if (_this.onSave) {
+        extraKeys['Ctrl-S'] = extraKeys['Cmd-S'] = _this.onSave;
+      }
+      editor.setOption('extraKeys', extraKeys);
+      _this.update();
+    });
+  },
+  watch: {
+    content: function (content) {
+      var _this = this;
+      if (content !== _this.cachedContent) {
+        _this.cachedContent = content;
+        _this.update();
+      }
+    },
+  },
+  methods: {
+    update: function () {
+      var _this = this;
+      if (!_this.editor || _this.cachedContent == null) return;
+      _this.editor.setValue(_this.cachedContent);
+      _this.editor.getDoc().clearHistory();
+      _this.editor.focus();
+    },
+  },
+};

+ 16 - 0
src/options/views/main.html

@@ -0,0 +1,16 @@
+<div class="main">
+  <aside>
+    <img src="/images/icon128.png">
+    <h1 v-text="i18n('extName')"></h1>
+    <div class="line">2013-2016</div>
+    <hr>
+    <div class=sidemenu>
+      <a href="#main/Installed" :class="{active:tab==='Main'}" v-text="i18n('sideMenuInstalled')"></a>
+      <a href="#main/Settings" :class="{active:tab==='Settings'}" v-feature="'settings'">
+        <span v-text="i18n('sideMenuSettings')" class="feature-text"></span>
+      </a>
+      <a href="#main/About" :class="{active:tab==='About'}" v-text="i18n('sideMenuAbout')"></a>
+    </div>
+  </aside>
+  <component :is="tab"></component>
+</div>

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

@@ -0,0 +1,23 @@
+var MainTab = require('./tab-installed');
+var SettingsTab = require('./tab-settings');
+var AboutTab = require('./tab-about');
+var cache = require('../../cache');
+
+var components = {
+  Main: MainTab,
+  Settings: SettingsTab,
+  About: AboutTab,
+};
+
+module.exports = {
+  props: ['params'],
+  template: cache.get('./main.html'),
+  components: components,
+  computed: {
+    tab: function () {
+      var tab = this.params.tab;
+      if (!components[tab]) tab = 'Main';
+      return tab;
+    },
+  },
+};

+ 4 - 0
src/options/views/message.html

@@ -0,0 +1,4 @@
+<transition-group tag="div" name="message">
+  <div v-for="message in messages" class="message"
+  :key="message" v-text="message.text"></div>
+</transition>

+ 26 - 0
src/options/views/message.js

@@ -0,0 +1,26 @@
+var cache = require('../../cache');
+
+function init() {
+  if (div) return;
+  div = document.createElement('div');
+  document.body.appendChild(div);
+  new Vue({
+    el: div,
+    template: cache.get('./message.html'),
+    data: {
+      messages: messages,
+    },
+  });
+}
+
+var div;
+var messages = [];
+
+exports.open = function (options) {
+  init();
+  messages.push(options);
+  setTimeout(function () {
+    var i = messages.indexOf(options);
+    ~i && messages.splice(i, 1);
+  }, 2000);
+};

+ 6 - 6
src/options/components/script.html → src/options/views/script.html

@@ -2,7 +2,7 @@
   <img class=script-icon :src="safeIcon">
   <div class="script-version pull-right" v-text="script.meta.version?'v'+script.meta.version:''"></div>
   <div class="script-author ellipsis pull-right" :title="script.meta.author" v-if="author">
-    <span v-text="'labelAuthor'|i18n"></span>
+    <span v-text="i18n('labelAuthor')"></span>
     <a href="mailto:{{author.email}}" v-if="author.email" v-text="author.name"></a>
     <span v-if="!author.email" v-text="author.name"></a>
   </div>
@@ -16,11 +16,11 @@
   </div>
   <p class="script-desc ellipsis" v-text="script.custom.description||getLocaleString('description')"></p>
   <div class=buttons>
-    <button v-text="'buttonEdit'|i18n" @click="onEdit"></button>
-    <button @click="onEnable">{{labelEnable}}</button>
-    <button v-text="'buttonRemove'|i18n" @click="onRemove"></button>
+    <button v-text="i18n('buttonEdit')" @click="onEdit"></button>
+    <button @click="onEnable" v-text="labelEnable"></button>
+    <button v-text="i18n('buttonRemove')" @click="onRemove"></button>
     <button v-if="canUpdate" :disabled="script.checking"
-      v-text="'buttonUpdate'|i18n" @click="onUpdate"></button>
-    <span>{{script.message}}</span>
+      v-text="i18n('buttonUpdate')" @click="onUpdate"></button>
+    <span v-text="script.message"></span>
   </div>
 </div>

+ 254 - 0
src/options/views/script.js

@@ -0,0 +1,254 @@
+var utils = require('../utils');
+var cache = require('../../cache');
+var _ = require('../../common');
+var store = utils.store;
+var events = utils.events;
+
+var DEFAULT_ICON = '/images/icon48.png';
+
+module.exports = {
+  props: ['script'],
+  template: cache.get('./script.html'),
+  data: function () {
+    return {
+      safeIcon: DEFAULT_ICON,
+    };
+  },
+  computed: {
+    canUpdate: function () {
+      var script = this.script;
+      return script.update && (
+        script.custom.updateURL ||
+        script.meta.updateURL ||
+        script.custom.downloadURL ||
+        script.meta.downloadURL ||
+        script.custom.lastInstallURL
+      );
+    },
+    homepageURL: function () {
+      var script = this.script;
+      return script.custom.homepageURL || script.meta.homepageURL || script.meta.homepage;
+    },
+    author: function () {
+      var text = this.script.meta.author;
+      if (!text) return;
+      var matches = text.match(/^(.*?)\s<(\S*?@\S*?)>$/);
+      return {
+        email: matches && matches[2],
+        name: matches ? matches[1] : text,
+      };
+    },
+    labelEnable: function () {
+      return this.script.enabled ? _.i18n('buttonDisable') : _.i18n('buttonEnable');
+    },
+  },
+  mounted: function () {
+    var _this = this;
+    _this.$el.addEventListener('dragstart', _this.onDragStart.bind(_this), false);
+    var icon = _this.script.meta.icon;
+    if (icon && icon !== _this.safeIcon) {
+      _this.loadImage(icon)
+      .then(function (url) {
+        _this.safeIcon = url;
+      }, function () {
+        _this.safeIcon = DEFAULT_ICON;
+      });
+    }
+  },
+  methods: {
+    getLocaleString: function (key) {
+      return _.getLocaleString(this.script.meta, key);
+    },
+    loadImage: function () {
+      var images = {};
+      return function (url) {
+        if (!url) return Promise.reject();
+        var promise = images[url];
+        if (!promise) {
+          var cache = store.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[url] = promise;
+        }
+        return promise;
+      };
+    }(),
+    onEdit: function () {
+      var _this = this;
+      events.$emit('EditScript', _this.script.id);
+    },
+    onRemove: function () {
+      var _this = this;
+      _.sendMessage({
+        cmd: 'RemoveScript',
+        data: _this.script.id,
+      });
+    },
+    onEnable: function () {
+      var _this = this;
+      _.sendMessage({
+        cmd: 'UpdateScriptInfo',
+        data: {
+          id: _this.script.id,
+          enabled: _this.script.enabled ? 0 : 1,
+        },
+      });
+    },
+    onUpdate: function () {
+      _.sendMessage({
+        cmd: 'CheckUpdate',
+        data: this.script.id,
+      });
+    },
+    onDragStart: function (e) {
+      new DND(e, function (data) {
+        events.$emit('MoveScript', data);
+      });
+    },
+  },
+};
+
+function DND(e, cb) {
+  var _this = this;
+  _this.mousemove = _this.mousemove.bind(_this);
+  _this.mouseup = _this.mouseup.bind(_this);
+  if (e) {
+    e.preventDefault();
+    _this.start(e);
+  }
+  _this.onDrop = cb;
+}
+DND.prototype.start = function (e) {
+  var _this = this;
+  var dragging = _this.dragging = {};
+  var el = dragging.el = e.currentTarget;
+  var parent = el.parentNode;
+  var rect = el.getBoundingClientRect();
+  dragging.offset = {
+    x: e.clientX - rect.left,
+    y: e.clientY - rect.top,
+  };
+  var next = el.nextElementSibling;
+  dragging.delta = (next ? next.getBoundingClientRect().top : parent.offsetHeight) - rect.top;
+  dragging.lastIndex = dragging.index = [].indexOf.call(parent.children, el);
+  dragging.elements = [].filter.call(parent.children, function (el) {
+    return el !== dragging.el;
+  });
+  var dragged = dragging.dragged = el.cloneNode(true);
+  dragged.classList.add('dragging');
+  dragged.style.left = rect.left + 'px';
+  dragged.style.top = rect.top + 'px';
+  dragged.style.width = rect.width + 'px';
+  parent.appendChild(dragged);
+  el.classList.add('dragging-placeholder');
+  document.addEventListener('mousemove', _this.mousemove, false);
+  document.addEventListener('mouseup', _this.mouseup, false);
+};
+DND.prototype.mousemove = function (e) {
+  var _this = this;
+  var dragging = _this.dragging;
+  var dragged = dragging.dragged;
+  dragged.style.left = e.clientX - dragging.offset.x + 'px';
+  dragged.style.top = e.clientY - dragging.offset.y + 'px';
+  var hoveredIndex = dragging.elements.findIndex(function (el) {
+    if (!el) return;
+    if (el.classList.contains('dragging-moving')) return;
+    var rect = el.getBoundingClientRect();
+    var pad = 10;
+    return (
+      e.clientX >= rect.left + pad
+      && e.clientX <= rect.left + rect.width - pad
+      && e.clientY >= rect.top + pad
+      && e.clientY <= rect.top + rect.height - pad
+    );
+  });
+  if (~hoveredIndex) {
+    var hoveredEl = dragging.elements[hoveredIndex];
+    var lastIndex = dragging.lastIndex;
+    var isDown = hoveredIndex >= lastIndex;
+    var el = dragging.el;
+    var delta = dragging.delta;
+    if (isDown) {
+      hoveredIndex ++;
+      hoveredEl.parentNode.insertBefore(el, hoveredEl.nextElementSibling);
+    } else {
+      delta = -delta;
+      hoveredEl.parentNode.insertBefore(el, hoveredEl);
+    }
+    dragging.lastIndex = hoveredIndex;
+    _this.animate(dragging.elements.slice(
+      isDown ? lastIndex : hoveredIndex,
+      isDown ? hoveredIndex : lastIndex
+    ), delta);
+  }
+  _this.checkScroll(e.clientY);
+};
+DND.prototype.animate = function (elements, delta) {
+  function endAnimation(e) {
+    e.target.classList.remove('dragging-moving');
+    e.target.removeEventListener('transitionend', endAnimation, false);
+  }
+  elements.forEach(function (el) {
+    if (!el) return;
+    el.classList.add('dragging-moving');
+    el.style.transition = 'none';
+    el.style.transform = 'translateY(' + delta + 'px)';
+    el.addEventListener('transitionend', endAnimation, false);
+    setTimeout(function () {
+      el.style.transition = '';
+      el.style.transform = '';
+    });
+  });
+};
+DND.prototype.mouseup = function () {
+  var _this = this;
+  document.removeEventListener('mousemove', _this.mousemove, false);
+  document.removeEventListener('mouseup', _this.mouseup, false);
+  var dragging = _this.dragging;
+  dragging.dragged.remove();
+  dragging.el.classList.remove('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, 32);
+      } else dragging.scrolling = false;
+    }
+  }
+  var _this = this;
+  if (!_this.dragging.scrolling) {
+    _this.dragging.scrolling = true;
+    scroll();
+  }
+};

+ 12 - 0
src/options/views/sync-service.html

@@ -0,0 +1,12 @@
+<div class="line">
+  <label>
+    <input type=checkbox v-setting="service.name+'Enabled'" @change="update">
+    <span v-text="labelText"></span>
+  </label>
+  <button @click="authenticate" v-text="labelAuthenticate"
+  :disabled="service.authState!=='unauthorized'"></button>
+  <button :disabled="disableSync" class="sync-start" @click="retry">
+    <svg class="icon"><use xlink:href="#refresh"/></svg>
+  </button>
+  <span>{{message}}</span>
+</div>

+ 60 - 0
src/options/views/sync-service.js

@@ -0,0 +1,60 @@
+var cache = require('../../cache');
+var _ = require('../../common');
+var utils = require('../utils');
+var events = utils.events;
+
+module.exports = {
+  props: ['service'],
+  template: cache.get('./sync-service.html'),
+  computed: {
+    labelText: function () {
+      var service = this.service;
+      return _.i18n('labelSyncTo', service.displayName || service.name);
+    },
+    labelAuthenticate: function () {
+      return {
+        authorized: _.i18n('buttonAuthorized'),
+        authorizing: _.i18n('buttonAuthorizing'),
+      }[this.service.authState] || _.i18n('buttonAuthorize');
+    },
+    disableSync: function () {
+      var service = this.service;
+      return !!(
+        ['authorized', 'error'].indexOf(service.authState) < 0 ||
+        ~['ready', 'syncing'].indexOf(service.syncState)
+      );
+    },
+    message: function () {
+      var service = this.service;
+      if (service.authState === 'initializing') return _.i18n('msgSyncInit');
+      if (service.authState === 'error') return _.i18n('msgSyncInitError');
+      if (service.syncState === 'error') return _.i18n('msgSyncError');
+      if (service.syncState === 'ready') return _.i18n('msgSyncReady');
+      if (service.syncState === 'syncing') {
+        var progress = '';
+        if (service.progress && service.progress.total) {
+          progress = ' (' + service.progress.finished + '/' + service.progress.total + ')';
+        }
+        return _.i18n('msgSyncing') + progress;
+      }
+      if (service.lastSync) {
+        var lastSync = new Date(service.lastSync).toLocaleString();
+        return _.i18n('lastSync', lastSync);
+      }
+    },
+  },
+  methods: {
+    retry: function () {
+      _.sendMessage({
+        cmd: 'SyncStart',
+        data: this.service.name,
+      });
+    },
+    authenticate: function () {
+      _.sendMessage({cmd: 'Authenticate', data: this.service.name});
+    },
+    update: function (e) {
+      e.target.checked && events.$emit('EnableService', this.service.name);
+    },
+  },
+};

+ 28 - 0
src/options/views/tab-about.html

@@ -0,0 +1,28 @@
+<div class="content">
+  <h1>
+    <span v-text="i18n('labelAbout')"></span>
+    <small>v{{version}}</small>
+  </h1>
+  <div class=line v-text="i18n('extDescription')"></div>
+  <div class=line>
+    <label v-text="i18n('labelRelated')"></label>
+    <span v-html="i18n('anchorSupportPage')"></span> |
+    <a href=https://gerald.top/~donate target=_blank v-text="i18n('labelDonate')"></a> |
+    <a href=https://github.com/violentmonkey/violentmonkey/issues target=_blank v-text="i18n('labelFeedback')"></a>
+  </div>
+  <div class=line>
+    <label v-text="i18n('labelAuthor')"></label>
+    <span v-html="i18n('anchorAuthor')"></span>
+  </div>
+  <div class=line>
+    <label v-text="i18n('labelTranslator')"></label>
+    <span v-html="i18n('anchorTranslator')"></span>
+  </div>
+  <div class=line>
+    <label v-text="i18n('labelCurrentLang')"></label>
+    <span id="currentLang">{{language}}</span> |
+    <a href=https://violentmonkey.github.io/localization.html#nex target=_blank>
+      Help with translation
+    </a>
+  </div>
+</div>

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

@@ -0,0 +1,12 @@
+var cache = require('../../cache');
+var data = {
+  version: chrome.app.getDetails().version,
+  language: navigator.language,
+};
+
+module.exports = {
+  template: cache.get('./tab-about.html'),
+  data: function () {
+    return data;
+  },
+};

+ 18 - 0
src/options/views/tab-installed.html

@@ -0,0 +1,18 @@
+<div class="content no-pad">
+  <header>
+    <button v-text="i18n('buttonNew')" @click="newScript"></button>
+    <button v-text="i18n('buttonUpdateAll')" @click="updateAll"></button>
+    <button v-text="i18n('buttonInstallFromURL')" @click="installFromURL"></button>
+    <div class="pull-right">
+      <a href=https://greasyfork.org/scripts target=_blank v-text="i18n('anchorGetMoreScripts')"></a>
+    </div>
+  </header>
+  <div class="backdrop" :class="{mask:store.loading}" v-show="message">
+    <div v-html="message"></div>
+  </div>
+  <div class="scripts">
+    <script-item v-for="script in store.scripts"
+      :script="script"></script-item>
+  </div>
+  <edit v-if="script" :script="script" :on-close="endEditScript"></edit>
+</div>

+ 94 - 0
src/options/views/tab-installed.js

@@ -0,0 +1,94 @@
+var ScriptItem = require('./script');
+var Edit = require('./edit');
+var cache = require('../../cache');
+var _ = require('../../common');
+var utils = require('../utils');
+var events = utils.events;
+var store = utils.store;
+
+module.exports = {
+  template: cache.get('./tab-installed.html'),
+  components: {
+    ScriptItem: ScriptItem,
+    Edit: Edit,
+  },
+  data: function () {
+    return {
+      script: null,
+      store: store,
+    };
+  },
+  computed: {
+    message: function () {
+      var _this = this;
+      if (_this.store.loading) {
+        return _.i18n('msgLoading');
+      }
+      if (!_this.store.scripts.length) {
+        return _.i18n('labelNoScripts');
+      }
+    },
+  },
+  methods: {
+    newScript: function () {
+      var _this = this;
+      _.sendMessage({cmd: 'NewScript'})
+      .then(function (script) {
+        _this.script = script;
+      });
+    },
+    updateAll: function () {
+      _.sendMessage({cmd: 'CheckUpdateAll'});
+    },
+    installFromURL: function () {
+      var url = prompt(_.i18n('hintInputURL'));
+      if (url && ~url.indexOf('://')) {
+        chrome.tabs.create({
+          url: chrome.extension.getURL('/options/index.html') + '#confirm/' + encodeURIComponent(url),
+        });
+      }
+    },
+    editScript: function (id) {
+      var _this = this;
+      _this.script = _this.store.scripts.find(function (script) {
+        return script.id === id;
+      });
+    },
+    endEditScript: function () {
+      this.script = null;
+    },
+    moveScript: function (data) {
+      var _this = this;
+      if (data.from === data.to) return;
+      _.sendMessage({
+        cmd: 'Move',
+        data: {
+          id: _this.store.scripts[data.from].id,
+          offset: data.to - data.from,
+        },
+      })
+      .then(function () {
+        var scripts = _this.store.scripts;
+        var i = Math.min(data.from, data.to);
+        var j = Math.max(data.from, data.to);
+        var seq = [
+          scripts.slice(0, i),
+          scripts.slice(i, j + 1),
+          scripts.slice(j + 1),
+        ];
+        i === data.to
+          ? seq[1].unshift(seq[1].pop())
+          : seq[1].push(seq[1].shift());
+        _this.store.scripts = seq.concat.apply([], seq);
+      });
+    },
+  },
+  created: function () {
+    events.$on('EditScript', this.editScript);
+    events.$on('MoveScript', this.moveScript);
+  },
+  beforeDestroy: function () {
+    events.$off('EditScript', this.editScript);
+    events.$off('MoveScript', this.moveScript);
+  },
+};

+ 40 - 0
src/options/views/tab-settings.html

@@ -0,0 +1,40 @@
+<div class="content">
+<h1 v-text="i18n('labelSettings')"></h1>
+<label class="line">
+  <input type=checkbox v-setting="'autoUpdate'" @change="updateAutoUpdate">
+  <span v-text="i18n('labelAutoUpdate')"></span>
+</label>
+<label class="line">
+  <input type=checkbox v-setting="'ignoreGrant'">
+  <span v-text="i18n('labelIgnoreGrant')"></span>
+</label>
+<label class="line">
+  <input type=checkbox v-setting="'autoReload'">
+  <span v-text="i18n('labelAutoReloadCurrentTab')"></span>
+</label>
+<fieldset class=title>
+  <legend v-text="i18n('labelDataImport')"></legend>
+  <button v-text="i18n('buttonImportData')" @click="importFile"></button>
+  <button :title="i18n('hintVacuum')" @click="vacuum" :disabled="vacuuming" v-text="labelVacuum"></button>
+</fieldset>
+<fieldset class=title>
+  <legend v-text="i18n('labelDataExport')"></legend>
+  <b v-text="i18n('labelScriptsToExport')"></b>
+  <label>
+    <input type=checkbox v-setting="'exportValues'">
+    <span v-text="i18n('labelExportScriptData')"></span>
+  </label>
+  <select class=export-list multiple v-model="selectedIds">
+    <option class="ellipsis" v-for="script in store.scripts"
+      :value="script.id" v-text="script.custom.name||script.meta.name"></option>
+  </select>
+  <button v-text="i18n('buttonAllNone')" @click="updateSelection()"></button>
+  <button v-text="i18n('buttonExportData')" @click="exportData" :disabled="exporting"></button>
+</fieldset>
+<fieldset class=title v-feature="'sync'">
+  <legend v-text="i18n('labelSync')" class="feature-text"></legend>
+  <div class="sync-services">
+    <sync-service v-for="service in store.sync" :service="service"></sync-service>
+  </div>
+</fieldset>
+</div>

+ 260 - 0
src/options/views/tab-settings.js

@@ -0,0 +1,260 @@
+function importData(file) {
+  function forEach(obj, cb) {
+    obj && Object.keys(obj).forEach(function (key) {
+      var value = obj[key];
+      cb(value, key);
+    });
+  }
+  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) {
+      value && _.sendMessage({
+        cmd: 'SetValue',
+        data: {
+          uri: key,
+          values: value,
+        }
+      });
+    });
+    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) {
+            delete more.id;
+            script.more = more;
+          }
+        }
+        _.sendMessage({
+          cmd: 'ParseScript',
+          data: script,
+        }).then(function () {
+          resolve(true);
+        });
+      });
+    });
+  }
+  function getVMFiles(entries) {
+    var i = entries.findIndex(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,
+        });
+      });
+    });
+    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 res.filter(function (item) {return item;}).length;
+    });
+  }).then(function (count) {
+    Message.open({text: _.i18n('msgImported', [count])});
+  });
+}
+function exportData(selectedIds) {
+  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);
+      });
+    });
+  }
+  function download(writer) {
+    return new Promise(function (resolve, _reject) {
+      writer.close(function (blob) {
+        resolve(blob);
+      });
+    }).then(function (blob) {
+      var url = URL.createObjectURL(blob);
+      var a = document.createElement('a');
+      a.href = url;
+      a.download = 'scripts.zip';
+      a.click();
+      setTimeout(function () {
+        URL.revokeObjectURL(url);
+      });
+    });
+  }
+  if (!selectedIds.length) return;
+  var withValues = _.options.get('exportValues');
+  _.sendMessage({
+    cmd: 'ExportZip',
+    data: {
+      values: withValues,
+      ids: selectedIds,
+    }
+  }).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] = ['id', 'custom', 'enabled', 'update'].reduce(function (res, key) {
+        res[key] = script[key];
+        return res;
+      }, {});
+      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);
+  });
+}
+
+var Message = require('./message');
+var SyncService = require('./sync-service');
+var utils = require('../utils');
+var store = utils.store;
+var events = utils.events;
+var cache = require('../../cache');
+var _ = require('../../common');
+
+module.exports = {
+  template: cache.get('./tab-settings.html'),
+  components: {
+    SyncService: SyncService,
+  },
+  data: function () {
+    return {
+      store: store,
+      selectedIds: [],
+      exporting: false,
+      vacuuming: false,
+      labelVacuum: _.i18n('buttonVacuum'),
+    };
+  },
+  watch: {
+    'store.scripts': function () {
+      this.updateSelection(true);
+    },
+  },
+  created: function () {
+    this.updateSelection(true);
+    events.$on('EnableService', this.onEnableService);
+  },
+  beforeDestroy: function () {
+    events.$off('EnableService', this.onEnableService);
+  },
+  methods: {
+    updateAutoUpdate: function () {
+      _.sendMessage({cmd: 'AutoUpdate'});
+    },
+    updateSelection: function (select) {
+      var _this = this;
+      if (!store.scripts.length) return;
+      if (select == null) select = _this.selectedIds.length < store.scripts.length;
+      if (select) {
+        _this.selectedIds = store.scripts.map(function (script) {
+          return script.id;
+        });
+      } else {
+        _this.selectedIds = [];
+      }
+    },
+    importFile: function () {
+      var input = document.createElement('input');
+      input.type = 'file';
+      input.accept = '.zip';
+      input.onchange = function () {
+        input.files && input.files.length && importData(input.files[0]);
+      };
+      input.click();
+    },
+    exportData: function () {
+      var _this = this;
+      _this.exporting = true;
+      exportData(_this.selectedIds)
+      .catch(_.noop)
+      .then(function () {
+        _this.exporting = false;
+      });
+    },
+    vacuum: function () {
+      var _this = this;
+      _this.vacuuming = true;
+      _this.labelVacuum = _.i18n('buttonVacuuming');
+      _.sendMessage({cmd: 'Vacuum'})
+      .then(function () {
+        _this.vacuuming = false;
+        _this.labelVacuum = _.i18n('buttonVacuumed');
+      });
+    },
+    onEnableService: function (name) {
+      store.sync.forEach(function (service) {
+        if (service.name !== name) {
+          var key = service.name + 'Enabled';
+          var enabled = _.options.get(key);
+          if (enabled) {
+            _.options.set(key, false);
+          }
+        }
+      });
+      _.sendMessage({cmd: 'SyncStart'});
+    },
+  },
+};

+ 64 - 65
src/popup/app.js

@@ -1,72 +1,71 @@
-define('app', function (require, exports, _module) {
-  var Menu = require('views/Menu');
-  var Commands = require('views/Command');
-  var Domains = require('views/Domain');
-  var utils = require('utils');
-  var _ = require('utils/common');
+var Menu = require('./views/menu');
+var Commands = require('./views/command');
+var Domains = require('./views/domain');
+var utils = require('./utils');
+var _ = require('../common');
 
-  var app = new Vue({
-    el: document.body,
-    components: {
-      Menu: Menu,
-      Commands: Commands,
-      Domains: Domains,
-    },
-    data: function () {
-      return {
-        type: 'menu',
-      };
-    },
-    methods: {
-      navigate: function (type) {
-        this.type = type || 'menu';
-      },
+var app = new Vue({
+  el: '#app',
+  template: '<component :is=type></component>',
+  components: {
+    Menu: Menu,
+    Commands: Commands,
+    Domains: Domains,
+  },
+  data: function () {
+    return {
+      type: 'Menu',
+    };
+  },
+  methods: {
+    navigate: function (type) {
+      this.type = type || 'Menu';
     },
-  });
+  },
+});
 
-  exports.navigate = app.navigate.bind(app);
+exports.navigate = app.navigate.bind(app);
 
-  !function () {
-    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 domains = domain.split('.').reduceRight(function (res, part) {
-          var last = res[0];
-          if (last) part += '.' + last;
-          res.unshift(part);
-          return res;
-        }, []);
-        domains.length > 1 && domains.pop();
-        utils.store.domains = domains;
-      }
+!function () {
+  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 domains = domain.split('.').reduceRight(function (res, part) {
+        var last = res[0];
+        if (last) part += '.' + last;
+        res.unshift(part);
+        return res;
+      }, []);
+      domains.length > 1 && domains.pop();
+      utils.store.domains = domains;
     }
+  }
 
-    var currentTab;
-    var commands = {
-      SetPopup: function (data, src, _callback) {
-        if (utils.store.currentTab.id !== src.tab.id) return;
-        utils.store.commands = data.menus;
-        _.sendMessage({
-          cmd: 'GetMetas',
-          data: data.ids,
-        }).then(function (scripts) {
-          utils.store.scripts = scripts;
-        });
-      },
-    };
-    chrome.runtime.onMessage.addListener(function (req, src, callback) {
-      var func = commands[req.cmd];
-      if (func) func(req.data, src, callback);
-    });
+  var currentTab;
+  var commands = {
+    SetPopup: function (data, src, _callback) {
+      if (utils.store.currentTab.id !== src.tab.id) return;
+      utils.store.commands = data.menus;
+      _.sendMessage({
+        cmd: 'GetMetas',
+        data: data.ids,
+      }).then(function (scripts) {
+        utils.store.scripts = scripts;
+      });
+    },
+  };
+  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) {
-      utils.store.currentTab = currentTab = {
-        id: tabs[0].id,
-        url: tabs[0].url,
-      };
-      init();
-    });
-  }();
-});
+  chrome.tabs.query({currentWindow: true, active: true}, function (tabs) {
+    utils.store.currentTab = currentTab = {
+      id: tabs[0].id,
+      url: tabs[0].url,
+    };
+    init();
+  });
+}();

+ 0 - 39
src/popup/components/command.js

@@ -1,39 +0,0 @@
-define('views/Command', function (require, _exports, module) {
-  var app = require('app');
-  var MixIn = require('views/MixIn');
-  var _ = require('utils/common');
-
-  module.exports = {
-    mixins: [MixIn],
-    ready: function () {
-      this.items.top.push({
-        name: _.i18n('menuBack'),
-        symbol: 'arrow-left',
-        onClick: function () {
-          app.navigate('');
-        },
-      });
-    },
-    watch: {
-      'store.commands': 'update',
-    },
-    methods: {
-      updateView: function () {
-        var _this = this;
-        _this.items.bot = _this.store.commands.map(function (item) {
-          return {
-            name: item[0],
-            symbol: 'right-hand',
-            className: 'ellipsis',
-            onClick: function (options) {
-              chrome.tabs.sendMessage(_this.store.currentTab.id, {
-                cmd: 'Command',
-                data: options.name,
-              });
-            },
-          };
-        });
-      },
-    },
-  };
-});

+ 0 - 37
src/popup/components/domain.js

@@ -1,37 +0,0 @@
-define('views/Domain', function (require, _exports, module) {
-  var app = require('app');
-  var MixIn = require('views/MixIn');
-  var _ = require('utils/common');
-
-  module.exports = {
-    mixins: [MixIn],
-    ready: function () {
-      this.items.top.push({
-        name: _.i18n('menuBack'),
-        symbol: 'arrow-left',
-        onClick: function () {
-          app.navigate('');
-        },
-      });
-    },
-    watch: {
-      'store.domains': 'update',
-    },
-    methods: {
-      updateView: function () {
-        var _this = this;
-        _this.items.bot = _this.store.domains.map(function (domain) {
-          return {
-            name: domain,
-            className: 'ellipsis',
-            onClick: function () {
-              chrome.tabs.create({
-                url: 'https://greasyfork.org/scripts/search?q=' + encodeURIComponent(domain),
-              });
-            },
-          };
-        });
-      },
-    },
-  };
-});

+ 0 - 22
src/popup/components/item.js

@@ -1,22 +0,0 @@
-define('views/MenuItem', function (require, _exports, module) {
-  function wrapHandler(name) {
-    return function () {
-      var _this = this;
-      var options = _this.options;
-      var handler = options[name];
-      handler && handler.call(_this, options);
-    };
-  }
-
-  var cache = require('cache');
-
-  module.exports = {
-    props: ['options'],
-    template: cache.get('/popup/components/menuitem.html'),
-    methods: {
-      onClick: wrapHandler('onClick'),
-      detailClick: wrapHandler('detailClick'),
-    },
-    ready: wrapHandler('init'),
-  };
-});

+ 0 - 117
src/popup/components/menu.js

@@ -1,117 +0,0 @@
-define('views/Menu', function (require, _exports, module) {
-  var app = require('app');
-  var MixIn = require('views/MixIn');
-  var _ = require('utils/common');
-
-  module.exports = {
-    mixins: [MixIn],
-    ready: function () {
-      var _this = this;
-      _this.items.top.push({
-        name: _.i18n('menuManageScripts'),
-        symbol: 'cog',
-        onClick: function () {
-          var url = chrome.extension.getURL(chrome.app.getDetails().options_page);
-          chrome.tabs.query({
-            currentWindow: true,
-            url: url,
-          }, function (tabs) {
-            var tab = tabs.find(function (tab) {
-              var hash = tab.url.match(/#(\w+)/);
-              return !hash || hash[1] !== 'confirm';
-            });
-            if (tab) chrome.tabs.update(tab.id, {active: true});
-            else chrome.tabs.create({url: url});
-          });
-        },
-      }, _this.menuFindScripts = {
-        name: _.i18n('menuFindScripts'),
-        symbol: 'search',
-        hide: false,
-        onClick: function () {
-          var matches = _this.store.currentTab.url.match(/:\/\/(?:www\.)?([^\/]*)/);
-          chrome.tabs.create({
-            url: 'https://greasyfork.org/scripts/search?q=' + matches[1],
-          });
-        },
-        detailClick: function () {
-          app.navigate('domains');
-        },
-      }, _this.menuCommands = {
-        name: _.i18n('menuCommands'),
-        symbol: 'arrow-right',
-        hide: false,
-        onClick: function () {
-          app.navigate('commands');
-        },
-      }, {
-        name: null,
-        symbol: null,
-        disabled: null,
-        init: function (options) {
-          options.disabled = !_.options.get('isApplied');
-          options.name = options.disabled ? _.i18n('menuScriptDisabled') : _.i18n('menuScriptEnabled');
-          options.symbol = options.disabled ? 'remove' : 'check';
-        },
-        onClick: function (options) {
-          _.options.set('isApplied', options.disabled);
-          options.init.call(this, options);
-          chrome.browserAction.setIcon({
-            path: {
-              19: '/images/icon19' + (options.disabled ? 'w' : '') + '.png',
-              38: '/images/icon38' + (options.disabled ? 'w' : '') + '.png',
-            },
-          });
-        },
-      });
-      _this.updateDomains();
-      _this.updateCommands();
-    },
-    watch: {
-      'store.scripts': 'update',
-      'store.commands': 'updateCommands',
-      'store.domains': 'updateDomains',
-    },
-    methods: {
-      updateView: function () {
-        var _this = this;
-        _this.items.bot = _this.store.scripts.map(function (script) {
-          return {
-            name: script.custom.name || _.getLocaleString(script.meta, 'name'),
-            className: 'ellipsis',
-            symbol: null,
-            disabled: null,
-            init: function (options) {
-              options.disabled = !script.enabled;
-              options.symbol = options.disabled ? 'remove' : 'check';
-            },
-            onClick: function (options) {
-              var vm = this;
-              _.sendMessage({
-                cmd: 'UpdateScriptInfo',
-                data: {
-                  id: script.id,
-                  enabled: !script.enabled,
-                },
-              }).then(function () {
-                script.enabled = !script.enabled;
-                options.init.call(vm, options);
-                _.options.get('autoReload') && chrome.tabs.reload(_this.store.currentTab.id);
-              });
-            },
-          };
-        });
-      },
-      updateCommands: function () {
-        var _this = this;
-        var commands = _this.store.commands;
-        _this.menuCommands.hide = !commands || !commands.length;
-      },
-      updateDomains: function () {
-        var _this = this;
-        var domains = _this.store.domains;
-        _this.menuFindScripts.hide = !domains || !domains.length;
-      },
-    },
-  };
-});

+ 0 - 41
src/popup/components/mixin.js

@@ -1,41 +0,0 @@
-define('views/MixIn', function (require, _exports, module) {
-  var MenuItem = require('views/MenuItem');
-  var cache = require('cache');
-  var utils = require('utils');
-
-  module.exports = {
-    template: cache.get('/popup/components/menu.html'),
-    data: function () {
-      return {
-        items: {
-          top: [],
-          bot: [],
-        },
-        store: utils.store,
-      };
-    },
-    components: {
-      MenuItem: MenuItem,
-    },
-    ready: function () {
-      this.update();
-    },
-    methods: {
-      update: function () {
-        var _this = this;
-        _this.updateView();
-        _this.fixStyles();
-      },
-      fixStyles: function () {
-        var _this = this;
-        _this.$nextTick(function () {
-          var placeholder = _this.$els.placeholder;
-          var bot = _this.$els.bot;
-          placeholder.innerHTML = bot.innerHTML;
-          var pad = bot.offsetWidth - bot.clientWidth + 2;
-          placeholder.style.paddingRight = pad + 'px';
-        });
-      },
-    },
-  };
-});

+ 2 - 2
src/popup/index.html

@@ -5,12 +5,12 @@
 		<title>Popup Menu - Violentmonkey</title>
     <link rel="stylesheet" href="style.css">
     <script src="/lib/vue.min.js"></script>
-		<script src="/lib/require-lite.js"></script>
+		<script src="/lib/define.js"></script>
 		<script src="/common.js"></script>
     <script src="/cache.js"></script>
 	</head>
   <body>
-    <component :is="type"></component>
+    <div id="app"></div>
 		<script src="app.js"></script>
 	</body>
 </html>

+ 5 - 7
src/popup/utils/index.js

@@ -1,7 +1,5 @@
-define('utils', function (_require, exports, _module) {
-  exports.store = {
-    scripts: [],
-    commands: [],
-    domains: [],
-  };
-});
+exports.store = {
+  scripts: [],
+  commands: [],
+  domains: [],
+};

+ 37 - 0
src/popup/views/command.js

@@ -0,0 +1,37 @@
+var app = require('../app');
+var MixIn = require('./mixin');
+var _ = require('../../common');
+
+module.exports = {
+  mixins: [MixIn],
+  mounted: function () {
+    this.items.top.push({
+      name: _.i18n('menuBack'),
+      symbol: 'arrow-left',
+      onClick: function () {
+        app.navigate();
+      },
+    });
+  },
+  watch: {
+    'store.commands': 'update',
+  },
+  methods: {
+    updateView: function () {
+      var _this = this;
+      _this.items.bot = _this.store.commands.map(function (item) {
+        return {
+          name: item[0],
+          symbol: 'right-hand',
+          className: 'ellipsis',
+          onClick: function (options) {
+            chrome.tabs.sendMessage(_this.store.currentTab.id, {
+              cmd: 'Command',
+              data: options.name,
+            });
+          },
+        };
+      });
+    },
+  },
+};

+ 35 - 0
src/popup/views/domain.js

@@ -0,0 +1,35 @@
+var app = require('../app');
+var MixIn = require('./mixin');
+var _ = require('../../common');
+
+module.exports = {
+  mixins: [MixIn],
+  mounted: function () {
+    this.items.top.push({
+      name: _.i18n('menuBack'),
+      symbol: 'arrow-left',
+      onClick: function () {
+        app.navigate();
+      },
+    });
+  },
+  watch: {
+    'store.domains': 'update',
+  },
+  methods: {
+    updateView: function () {
+      var _this = this;
+      _this.items.bot = _this.store.domains.map(function (domain) {
+        return {
+          name: domain,
+          className: 'ellipsis',
+          onClick: function () {
+            chrome.tabs.create({
+              url: 'https://greasyfork.org/scripts/search?q=' + encodeURIComponent(domain),
+            });
+          },
+        };
+      });
+    },
+  },
+};

+ 1 - 1
src/popup/components/menuitem.html → src/popup/views/item.html

@@ -1,7 +1,7 @@
 <div class="menu-item" :class="[options.className,{disabled:options.disabled}]" :title="options.title||options.name">
   <div class="menu-item-detail" v-if="options.detailClick" @click="detailClick">...</div>
   <div class="menu-item-label" @click="onClick">
-    <svg class="icon"><use xlink:href="#{{options.symbol}}"/></svg>
+    <svg class="icon"><use :xlink:href="'#'+options.symbol"/></svg>
     {{options.name}}
   </div>
 </div>

+ 20 - 0
src/popup/views/item.js

@@ -0,0 +1,20 @@
+function wrapHandler(name) {
+  return function () {
+    var _this = this;
+    var options = _this.options;
+    var handler = options[name];
+    handler && handler.call(_this, options);
+  };
+}
+
+var cache = require('../../cache');
+
+module.exports = {
+  props: ['options'],
+  template: cache.get('./item.html'),
+  methods: {
+    onClick: wrapHandler('onClick'),
+    detailClick: wrapHandler('detailClick'),
+  },
+  mounted: wrapHandler('init'),
+};

+ 2 - 2
src/popup/components/menu.html → src/popup/views/menu.html

@@ -3,8 +3,8 @@
     <menu-item v-for="item in items.top" v-show="!item.hide" :options="item"></menu-item>
   </div>
   <hr v-if="items.bot.length">
-  <div class="menu placeholder" v-el:placeholder></div>
-  <div class="menu" v-el:bot>
+  <div class="menu placeholder" ref="placeholder"></div>
+  <div class="menu" ref="bot">
     <menu-item v-for="item in items.bot" :options="item"></menu-item>
   </div>
 </div>

+ 115 - 0
src/popup/views/menu.js

@@ -0,0 +1,115 @@
+var app = require('../app');
+var MixIn = require('./mixin');
+var _ = require('../../common');
+
+module.exports = {
+  mixins: [MixIn],
+  mounted: function () {
+    var _this = this;
+    _this.items.top.push({
+      name: _.i18n('menuManageScripts'),
+      symbol: 'cog',
+      onClick: function () {
+        var url = chrome.extension.getURL(chrome.app.getDetails().options_page);
+        chrome.tabs.query({
+          currentWindow: true,
+          url: url,
+        }, function (tabs) {
+          var tab = tabs.find(function (tab) {
+            var hash = tab.url.match(/#(\w+)/);
+            return !hash || hash[1] !== 'confirm';
+          });
+          if (tab) chrome.tabs.update(tab.id, {active: true});
+          else chrome.tabs.create({url: url});
+        });
+      },
+    }, _this.menuFindScripts = {
+      name: _.i18n('menuFindScripts'),
+      symbol: 'search',
+      hide: false,
+      onClick: function () {
+        var matches = _this.store.currentTab.url.match(/:\/\/(?:www\.)?([^\/]*)/);
+        chrome.tabs.create({
+          url: 'https://greasyfork.org/scripts/search?q=' + matches[1],
+        });
+      },
+      detailClick: function () {
+        app.navigate('Domains');
+      },
+    }, _this.menuCommands = {
+      name: _.i18n('menuCommands'),
+      symbol: 'arrow-right',
+      hide: false,
+      onClick: function () {
+        app.navigate('Commands');
+      },
+    }, {
+      name: null,
+      symbol: null,
+      disabled: null,
+      init: function (options) {
+        options.disabled = !_.options.get('isApplied');
+        options.name = options.disabled ? _.i18n('menuScriptDisabled') : _.i18n('menuScriptEnabled');
+        options.symbol = options.disabled ? 'remove' : 'check';
+      },
+      onClick: function (options) {
+        _.options.set('isApplied', options.disabled);
+        options.init.call(this, options);
+        chrome.browserAction.setIcon({
+          path: {
+            19: '/images/icon19' + (options.disabled ? 'w' : '') + '.png',
+            38: '/images/icon38' + (options.disabled ? 'w' : '') + '.png',
+          },
+        });
+      },
+    });
+    _this.updateDomains();
+    _this.updateCommands();
+  },
+  watch: {
+    'store.scripts': 'update',
+    'store.commands': 'updateCommands',
+    'store.domains': 'updateDomains',
+  },
+  methods: {
+    updateView: function () {
+      var _this = this;
+      _this.items.bot = _this.store.scripts.map(function (script) {
+        return {
+          name: script.custom.name || _.getLocaleString(script.meta, 'name'),
+          className: 'ellipsis',
+          symbol: null,
+          disabled: null,
+          init: function (options) {
+            options.disabled = !script.enabled;
+            options.symbol = options.disabled ? 'remove' : 'check';
+          },
+          onClick: function (options) {
+            var vm = this;
+            _.sendMessage({
+              cmd: 'UpdateScriptInfo',
+              data: {
+                id: script.id,
+                enabled: !script.enabled,
+              },
+            }).then(function () {
+              script.enabled = !script.enabled;
+              options.init.call(vm, options);
+              _.options.get('autoReload') && chrome.tabs.reload(_this.store.currentTab.id);
+            });
+          },
+        };
+      });
+    },
+    updateCommands: function () {
+      var _this = this;
+      var commands = _this.store.commands;
+      _this.menuCommands.hide = !commands || !commands.length;
+    },
+    updateDomains: function () {
+      var _this = this;
+      var domains = _this.store.domains;
+      _this.menuFindScripts.hide = !domains || !domains.length;
+    },
+  },
+};

+ 39 - 0
src/popup/views/mixin.js

@@ -0,0 +1,39 @@
+var MenuItem = require('./item');
+var cache = require('../../cache');
+var utils = require('../utils');
+
+module.exports = {
+  template: cache.get('./menu.html'),
+  data: function () {
+    return {
+      items: {
+        top: [],
+        bot: [],
+      },
+      store: utils.store,
+    };
+  },
+  components: {
+    MenuItem: MenuItem,
+  },
+  mounted: function () {
+    this.update();
+  },
+  methods: {
+    update: function () {
+      var _this = this;
+      _this.updateView();
+      _this.fixStyles();
+    },
+    fixStyles: function () {
+      var _this = this;
+      _this.$nextTick(function () {
+        var placeholder = _this.$refs.placeholder;
+        var bot = _this.$refs.bot;
+        placeholder.innerHTML = bot.innerHTML;
+        var pad = bot.offsetWidth - bot.clientWidth + 2;
+        placeholder.style.paddingRight = pad + 'px';
+      });
+    },
+  },
+};

+ 0 - 0
src/public/lib/require-lite.js → src/public/lib/define.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 2 - 2
src/public/lib/vue.min.js


برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است