Quellcode durchsuchen

refactor options with vue.js

Gerald vor 9 Jahren
Ursprung
Commit
a7d3e80a33
59 geänderte Dateien mit 1604 neuen und 1751 gelöschten Zeilen
  1. 1 2
      .eslintrc.yml
  2. 4 1
      gulpfile.js
  3. 2 2
      scripts/i18n.js
  4. 8 8
      scripts/templateCache.js
  5. 7 10
      scripts/updateLib.js
  6. 3 128
      src/cache.js
  7. 2 42
      src/common.js
  8. 89 42
      src/options/app.js
  9. 28 0
      src/options/components/confirm.html
  10. 176 0
      src/options/components/confirm.js
  11. 96 0
      src/options/components/edit.html
  12. 132 0
      src/options/components/edit.js
  13. 1 0
      src/options/components/editor.html
  14. 38 20
      src/options/components/editor.js
  15. 16 0
      src/options/components/main.html
  16. 30 0
      src/options/components/main.js
  17. 1 0
      src/options/components/message.html
  18. 21 0
      src/options/components/message.js
  19. 26 0
      src/options/components/script.html
  20. 253 0
      src/options/components/script.js
  21. 14 0
      src/options/components/sync-service.html
  22. 58 0
      src/options/components/sync-service.js
  23. 28 0
      src/options/components/tab-about.html
  24. 14 0
      src/options/components/tab-about.js
  25. 18 0
      src/options/components/tab-installed.html
  26. 85 0
      src/options/components/tab-installed.js
  27. 40 0
      src/options/components/tab-settings.html
  28. 234 0
      src/options/components/tab-settings.js
  29. 2 4
      src/options/index.html
  30. 0 55
      src/options/model.js
  31. 32 36
      src/options/style.css
  32. 0 11
      src/options/templates/confirm-options.html
  33. 0 16
      src/options/templates/confirm.html
  34. 0 70
      src/options/templates/edit-meta.html
  35. 0 22
      src/options/templates/edit.html
  36. 0 14
      src/options/templates/main.html
  37. 0 1
      src/options/templates/message.html
  38. 0 1
      src/options/templates/option.html
  39. 0 33
      src/options/templates/script.html
  40. 0 29
      src/options/templates/sync-service.html
  41. 0 26
      src/options/templates/tab-about.html
  42. 0 10
      src/options/templates/tab-installed.html
  43. 0 43
      src/options/templates/tab-settings.html
  44. 32 0
      src/options/utils/dropdown.js
  45. 53 0
      src/options/utils/features.js
  46. 36 0
      src/options/utils/index.js
  47. 19 0
      src/options/utils/settings.js
  48. 0 20
      src/options/views/confirm-options.js
  49. 0 201
      src/options/views/confirm.js
  50. 0 28
      src/options/views/edit-meta.js
  51. 0 117
      src/options/views/edit.js
  52. 0 36
      src/options/views/main.js
  53. 0 38
      src/options/views/message.js
  54. 0 274
      src/options/views/script.js
  55. 0 27
      src/options/views/sync-service.js
  56. 0 13
      src/options/views/tab-about.js
  57. 0 89
      src/options/views/tab-installed.js
  58. 0 282
      src/options/views/tab-settings.js
  59. 5 0
      src/public/lib/vue.min.js

+ 1 - 2
.eslintrc.yml

@@ -20,10 +20,9 @@ env:
 globals:
   define: true
   _: true
-  $: true
   chrome: true
   zip: true
-  Backbone: true
+  Vue: true
   CodeMirror: true
 
 extends: 'eslint:recommended'

+ 4 - 1
gulpfile.js

@@ -16,7 +16,10 @@ const isProd = process.env.NODE_ENV === 'production';
 const paths = {
   cache: 'src/cache.js',
   manifest: 'src/manifest.json',
-  templates: 'src/**/templates/*.html',
+  templates: [
+    'src/**/*.html',
+    '!src/**/index.html',
+  ],
   jsBg: 'src/background/**/*.js',
   jsOptions: 'src/options/**/*.js',
   jsPopup: 'src/popup/**/*.js',

+ 2 - 2
scripts/i18n.js

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

+ 8 - 8
scripts/templateCache.js

@@ -3,16 +3,11 @@
 const gutil = require('gulp-util');
 const through = require('through2');
 const _ = require('underscore');
-const minified = require('./minifyHtml');
-
-/*function minified(data) {
-  data = String(data);
-  return data.replace(/\s+/g, ' ');
-}*/
+const minify = require('html-minifier').minify;
 
 module.exports = function templateCache() {
   const contentTpl = 'cache.put(<%= name %>, <%= content %>);\n';
-  const header = `/* Templates cached from \`_.template\` with love :) */
+  const header = `/* Templates cached with love :) */
 define('templates', function (require, exports, module) {
   var cache = require('cache');
 `;
@@ -27,7 +22,12 @@ define('templates', function (require, exports, module) {
       return this.emit('error', new gutil.PluginError('VM-cache', 'Stream is not supported.'));
     contents.push(gutil.template(contentTpl, {
       name: JSON.stringify(('/' + file.relative).replace(/\\/g, '/')),
-      content: _.template(minified(file.contents), {variable: 'it'}).source,
+      content: JSON.stringify(minify(String(file.contents), {
+        removeComments: true,
+        collapseWhitespace: true,
+        conservativeCollapse: true,
+        removeAttributeQuotes: true,
+      })),
       file: '',
     }));
     cb();

+ 7 - 10
scripts/updateLib.js

@@ -28,7 +28,7 @@ function readdir(dir) {
 
 function copyFile(src, dest) {
   return new Promise((resolve, reject) => {
-    ncp(src, dest, (err) => err ? reject(err) : resolve());
+    ncp(src, dest, err => err ? reject(err) : resolve());
   }).then(() => {
     console.log(src + ' => ' + dest);
   });
@@ -42,7 +42,7 @@ function update(lib, files) {
   alias.lib = alias.lib || lib;
   const libdir = `node_modules/${alias.lib}`;
   const srcdir = `${SRC_DIR}/${lib}`
-  return Promise.all(files.map((file) => {
+  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);
@@ -52,11 +52,8 @@ function update(lib, files) {
   });
 }
 
-readdir(SRC_DIR).then((data) => {
-  data.forEach(function (name) {
-    if (!aliases[name]) return;
-    getFiles('**', `${SRC_DIR}/${name}`).then((files) => {
-      update(name, files);
-    });
-  });
-});
+readdir(SRC_DIR).then(data => data.forEach(name => {
+  if (!aliases[name]) return;
+  getFiles('**', `${SRC_DIR}/${name}`)
+  .then(files => update(name, files));
+}));

+ 3 - 128
src/cache.js

@@ -10,138 +10,13 @@ define('cache', function (require, _exports, module) {
   };
   Cache.prototype.get = function (key) {
     var data = this.data;
-    return new Promise(function (resolve, reject) {
-      if (key in data) return resolve(data[key]);
-      var xhr = new XMLHttpRequest;
-      xhr.open('GET', key, true);
-      xhr.onload = function () {
-        resolve(data[key] = _.template(this.responseText, {variable: 'it'}));
-      };
-      xhr.onerror = function () {
-        reject(this);
-      };
-      xhr.send();
-    });
+    if (key in data) return data[key];
+    throw 'Cache not found: ' + key;
   };
 
-  var cache = module.exports = new Cache();
+  module.exports = new Cache();
   require('templates');
 
-  var BaseView = cache.BaseView = Backbone.View.extend({
-    initialize: function () {
-      var _this = this;
-      _this.subviews = {data: {}};
-      _this.childViews = [];
-      if (_this.templateUrl) {
-        _this.__gotTemplate = cache.get(_this.templateUrl)
-        .then(function (fn) {
-          _this.templateFn = fn;
-        });
-      }
-      _.bindAll(_this, 'render', 'postrender');
-      _this.render();
-    },
-    clear: function () {
-      var _this = this;
-      if (_this.childViews.length) {
-        _this.childViews.forEach(function (view) {
-          view.remove();
-        });
-        _this.childViews = [];
-      }
-    },
-    remove: function () {
-      var _this = this;
-      _this.clear();
-      _this.undelegateEvents();
-      Backbone.View.prototype.remove.call(_this);
-    },
-    _render: function () {
-      this.$el.html(this.templateFn());
-    },
-    render: function () {
-      var render = this._render.bind(this);
-      (this.__gotTemplate || Promise.resolve()).then(render)
-      .then(this.postrender);
-      return this;
-    },
-    postrender: function () {
-      _.forEach(this.$('[data-i18n]'), function (node) {
-        node.innerHTML = _.i18n(node.dataset.i18n);
-      });
-      _.forEach(this.$('[data-feature]'), function (node) {
-        _.features.isHit(node.dataset.feature) || node.classList.add('feature');
-      });
-    },
-    loadSubview: function (name, factory, selector) {
-      var _this = this;
-      var view;
-      var subviews = _this.subviews;
-      view = subviews.data[name];
-      if (!view) {
-        view = factory();
-      }
-      var current = subviews.current;
-      if (name !== current) {
-        var currentView = subviews.data[current];
-        if (currentView) {
-          currentView.remove();
-          subviews.data[current] = null;
-        }
-      }
-      subviews.data[subviews.current = name] = view;
-      var $el = selector ? _this.$(selector) : _this.$el;
-      $el.html(view.render().el);
-      return view;
-    },
-    getValue: function (target) {
-      var key = target.dataset.id;
-      var value;
-      switch (key[0]) {
-      case '!':
-        key = key.slice(1);
-        value = target.checked;
-        break;
-      case '[':
-        key = key.slice(1);
-        value = _.filter(target.value.split('\n').map(function (s) {return s.trim();}));
-        break;
-      default:
-        value = target.value;
-      }
-      return {
-        key: key,
-        value: value,
-      };
-    },
-  });
-
-  BaseView.prototype.postrender.call(window);
-
-  cache.BaseRouter = Backbone.Router.extend({
-    initialize: function (selector) {
-      var _this = this;
-      _this.views = {data: {}};
-      _this.$root = $(selector);
-    },
-    loadView: function (name, factory) {
-      var _this = this;
-      var views = _this.views;
-      var view = views.data[name];
-      if (!view) view = views.data[name] = factory();
-      if (name !== views.current) {
-        var currentView = views.data[views.current];
-        if (currentView) {
-          currentView.remove();
-          views.data[views.current] = null;
-        }
-        views.data[views.current = name] = view;
-      }
-      _this.$root.html(view.el);
-      return view;
-    },
-  });
-
   !function () {
     var xhr = new XMLHttpRequest;
     xhr.open('GET', '/images/sprite.svg', true);

+ 2 - 42
src/common.js

@@ -70,56 +70,16 @@ _.zfill = function (num, length) {
 };
 
 _.getUniqId = function () {
-	return Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
+  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 = _.find(navigator.languages, function (lang) {
+  var lang = navigator.languages.find(function (lang) {
     return (key + ':' + lang) in meta;
   });
   if (lang) key += ':' + lang;
   return meta[key] || '';
 };
-
-/*
-function format() {
-  var args = arguments;
-  if (args[0]) return args[0].replace(/\$(?:\{(\d+)\}|(\d+))/g, function(value, group1, group2) {
-		var index = typeof group1 != 'undefined' ? group1 : group2;
-		return index >= args.length ? value : (args[index] || '');
-  });
-}
-*/
-
-_.features = function () {
-  var FEATURES = 'features';
-  var features = _.options.get(FEATURES);
-  if (!features || !features.data) features = {
-    data: {},
-  };
-
-  return {
-    init: init,
-    hit: hit,
-    isHit: isHit,
-  };
-
-  function init(version) {
-    if (features.version !== version) {
-      _.options.set(FEATURES, features = {
-        version: version,
-        data: {},
-      });
-    }
-  }
-  function hit(key) {
-    features.data[key] = 1;
-    _.options.set(FEATURES, features);
-  }
-  function isHit(key) {
-    return features.data[key];
-  }
-}();

+ 89 - 42
src/options/app.js

@@ -1,60 +1,107 @@
-define('app', function (require, exports, _module) {
-  var MainView = require('views/Main');
-  var ConfirmView = require('views/Confirm');
-  var models = require('models');
-  var cache = require('cache');
-  zip.workerScriptsPath = '/lib/zip.js/';
-
-  var App = cache.BaseRouter.extend({
-    routes: {
-      '': 'renderMain',
-      'main/:tab': 'renderMain',
-      'confirm/:url': 'renderConfirm',
-      'confirm/:url/:from': 'renderConfirm',
-    },
-    renderMain: function (tab) {
-      this.loadView('main', function () {
-        initMain();
-        return new MainView;
-      }).loadTab(tab);
-    },
-    renderConfirm: function (url, referer) {
-      this.loadView('confirm', function () {
-        return new ConfirmView;
-      }).initData(url, referer);
-    },
-  });
-  var app = new App('#app');
-  Backbone.history.start() || app.navigate('', {trigger: true, replace: true});
-
-  $(document).on('click', '[data-feature]', function (e) {
-    var target = e.currentTarget;
-    _.features.hit(target.dataset.feature);
-    target.classList.remove('feature');
-  });
-
+define('app', function (require, _exports, _module) {
   function initMain() {
-    var scriptList = exports.scriptList = new models.ScriptList;
-    var syncData = exports.syncData = new models.SyncList;
+    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);
+    });
     var port = chrome.runtime.connect({name: 'Options'});
     port.onMessage.addListener(function (res) {
       switch (res.cmd) {
       case 'sync':
-        syncData.set(res.data);
+        store.sync = res.data;
         break;
       case 'add':
         res.data.message = '';
-        scriptList.push(res.data);
+        store.scripts.push(res.data);
         break;
       case 'update':
         if (res.data) {
-          var model = scriptList.get(res.data.id);
-          if (model) model.set(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':
-        scriptList.remove(res.data);
+        var i = store.scripts.findIndex(function (script) {
+          return script.id === res.data;
+        });
+        ~i && store.scripts.splice(i, 1);
       }
     });
   }
+
+  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: [],
+  });
+  var init = {
+    Main: initMain,
+  };
+  zip.workerScriptsPath = '/lib/zip.js/';
+
+  new Vue({
+    el: document.body,
+    components: {
+      Main: Main,
+      Confirm: Confirm,
+    },
+    data: function () {
+      return {
+        type: 'main',
+        params: {},
+      };
+    },
+    ready: 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;
+          }
+        }
+      },
+    },
+  });
 });

+ 28 - 0
src/options/components/confirm.html

@@ -0,0 +1,28 @@
+<div>
+  <div class=frame-header>
+    <div class=buttons>
+      <div v-dropdown>
+        <button dropdown-toggle v-text="'buttonInstallOptions'|i18n" @click="showOptions=!showOptions"></button>
+        <div class="dropdown-menu options-panel" @mousedown.stop v-show="showOptions">
+          <label>
+            <input type=checkbox v-setting="'closeAfterInstall'">
+            <span v-text="'installOptionClose'|i18n"></span>
+          </label>
+          <label>
+            <input type=checkbox v-setting="'trackLocalFile'" :disabled="closeAfterInstall">
+            <span v-text="'installOptionTrack'|i18n"></span>
+          </label>
+        </div>
+      </div>
+      <button v-text="'buttonConfirmInstallation'|i18n"
+        :disabled="!installable" @click="installScript"></button>
+      <button v-text="'buttonClose'|i18n" @click="close"></button>
+    </div>
+    <h1><span v-text="'labelInstall'|i18n"></span> - <span v-text="'extName'|i18n"></span></h1>
+    <div class=ellipsis :title="url">{{url}}</div>
+    <div class=ellipsis>{{message}}</div>
+  </div>
+  <div class=frame-body>
+    <editor readonly :on-exit="close" :content="code"></editor>
+  </div>
+</div>

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

@@ -0,0 +1,176 @@
+define('views/Confirm', function (require, _exports, module) {
+  var Editor = require('views/Editor');
+  var cache = require('cache');
+
+  module.exports = {
+    props: ['params'],
+    components: {
+      Editor: Editor,
+    },
+    template: cache.get('/options/components/confirm.html'),
+    data: function () {
+      return {
+        installable: false,
+        message: '',
+        code: '',
+        require: {},
+        resources: {},
+        dependencyOK: false,
+      };
+    },
+    computed: {
+      isLocal: function () {
+        return /^file:\/\/\//.test(this.params.url);
+      },
+    },
+    ready: function () {
+      var _this = this;
+      _this.message = _.i18n('msgLoadingData');
+      _this.loadData().then(function () {
+        _this.parseMeta();
+      });
+    },
+    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 = _.values(script.resources);
+          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();
+        });
+      },
+    },
+  };
+});

+ 96 - 0
src/options/components/edit.html

@@ -0,0 +1,96 @@
+<div class="frame edit">
+  <div class="frame-header">
+    <div class="buttons">
+      <div v-dropdown>
+        <button dropdown-toggle v-text="'buttonCustomMeta'|i18n"></button>
+        <div class="dropdown-menu">
+          <table>
+            <tr>
+              <td title="@name" v-text="'labelName'|i18n"></td>
+              <td class=expand>
+                <input type="text" v-model="custom.name" placeholder="{{placeholders.name}}">
+              </td>
+              <td title="@run-at" v-text="'labelRunAt'|i18n"></td>
+              <td>
+                <select v-model="custom['run-at']">
+                  <option value=default v-text="'labelRunAtDefault'|i18n"></option>
+                  <option value=start>document-start</option>
+                  <option value=idle>document-idle</option>
+                  <option value=end>document-end</option>
+                </select>
+              </td>
+            </tr>
+            <tr title="@homepageURL">
+              <td v-text="'labelHomepageURL'|i18n"></td>
+              <td colspan=3 class=expand>
+                <input type="text" v-model="custom.homepageURL" placeholder="{{placeholders.homepageURL}}">
+              </td>
+            </tr>
+          </table>
+          <table>
+            <tr title="@updateURL">
+              <td v-text="'labelUpdateURL'|i18n"></td>
+              <td class=expand>
+                <input type="text" v-model="custom.updateURL" placeholder="{{placeholders.updateURL}}">
+              </td>
+            </tr>
+            <tr title="@downloadURL">
+              <td v-text="'labelDownloadURL'|i18n"></td>
+              <td class=expand>
+                <input type="text" v-model="custom.downloadURL" placeholder="{{placeholders.downloadURL}}">
+              </td>
+            </tr>
+          </table>
+          <fieldset title="@include">
+            <legend>
+              <span v-text="'labelInclude'|i18n"></span>
+              <label>
+                <input type=checkbox v-model="custom.keepInclude">
+                <span v-text="'labelKeepInclude'|i18n"></span>
+              </label>
+            </legend>
+            <div v-text="'labelCustomInclude'|i18n"></div>
+            <textarea v-model="custom.include"></textarea>
+          </fieldset>
+          <fieldset title="@match">
+            <legend>
+              <span v-text="'labelMatch'|i18n"></span>
+              <label>
+                <input type=checkbox v-model="custom.keepMatch">
+                <span v-text="'labelKeepMatch'|i18n"></span>
+              </label>
+            </legend>
+            <div v-text="'labelCustomMatch'|i18n"></div>
+            <textarea v-model="custom.match"></textarea>
+          </fieldset>
+          <fieldset title="@exclude">
+            <legend>
+              <span v-text="'labelExclude'|i18n"></span>
+              <label>
+                <input type=checkbox v-model="custom.keepExclude">
+                <span v-text="'labelKeepExclude'|i18n"></span>
+              </label>
+            </legend>
+            <div v-text="'labelCustomExclude'|i18n"></div>
+            <textarea v-model="custom.exclude"></textarea>
+          </fieldset>
+        </div>
+      </div>
+    </div>
+    <h2 v-text="'labelScriptEditor'|i18n"></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>
+    </div>
+    <label>
+      <input type=checkbox v-model="update">
+      <span v-text="'labelAllowUpdate'|i18n"></span>
+    </label>
+  </div>
+  <div class=frame-body>
+    <editor :on-save="save" :on-exit="close" :content.sync="code"></editor>
+  </div>
+</div>

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

@@ -0,0 +1,132 @@
+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');
+
+  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;
+      },
+    },
+    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 = Object.assign({}, script.custom);
+        custom.keepInclude = custom._include;
+        custom.keepMatch = custom._match;
+        custom.keepExclude = custom._exclude;
+        custom.include = fromList(custom.include);
+        custom.match = fromList(custom.match);
+        custom.exclude = fromList(custom.exclude);
+        _this.custom = custom;
+        _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();
+        });
+      },
+    },
+  };
+});

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

@@ -0,0 +1 @@
+<div class="editor-code"></div>

+ 38 - 20
src/options/editor.js → src/options/components/editor.js

@@ -1,4 +1,4 @@
-define('editor', function (_require, exports, _module) {
+define('views/Editor', function (require, _exports, module) {
   function addScripts(data) {
     function add(data) {
       var s = document.createElement('script');
@@ -61,13 +61,22 @@ define('editor', function (_require, exports, _module) {
     });
   }
 
-  var readyCodeMirror;
+  var cache = require('cache');
+  initCodeMirror();
 
-  exports.init = function (options) {
-    options = options || {};
-    readyCodeMirror = readyCodeMirror || initCodeMirror();
-    return readyCodeMirror.then(function(){
-      var editor = CodeMirror(options.container, {
+  module.exports = {
+    props: {
+      readonly: null,
+      onExit: null,
+      onSave: null,
+      content: {
+        twoWay: true,
+      },
+    },
+    template: cache.get('/options/components/editor.html'),
+    ready: function () {
+      var _this = this;
+      var editor = _this.editor = CodeMirror(_this.$el, {
         continueComments: true,
         matchBrackets: true,
         autoCloseBrackets: true,
@@ -79,20 +88,29 @@ define('editor', function (_require, exports, _module) {
         foldGutter: true,
         gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
       });
-      editor.clearHistory = function() {
-        this.getDoc().clearHistory();
-      };
-      editor.setValueAndFocus = function(value) {
-        this.setValue(value);
-        this.focus();
-      };
-      options.readonly && editor.setOption('readOnly', options.readonly);
-      options.onchange && editor.on('change', options.onchange);
+      _this.readonly && editor.setOption('readOnly', _this.readonly);
+      editor.on('change', function () {
+        _this.cachedContent = editor.getValue();
+        _this.content = _this.cachedContent;
+      });
       var extraKeys = {};
-      options.onexit && (extraKeys.Esc = options.onexit);
-      options.onsave && (extraKeys['Ctrl-S'] = extraKeys['Cmd-S'] = options.onsave);
+      if (_this.onExit) {
+        extraKeys.Esc = _this.onExit;
+      }
+      if (_this.onSave) {
+        extraKeys['Ctrl-S'] = extraKeys['Cmd-S'] = _this.onSave;
+      }
       editor.setOption('extraKeys', extraKeys);
-      return editor;
-    });
+    },
+    watch: {
+      content: function (content) {
+        var _this = this;
+        if (content !== _this.cachedContent) {
+          _this.editor.setValue(_this.cachedContent = content);
+          _this.editor.getDoc().clearHistory();
+          _this.editor.focus();
+        }
+      },
+    },
   };
 });

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

@@ -0,0 +1,16 @@
+<div class="main">
+  <aside>
+    <img src="/images/icon128.png">
+    <h2 v-text="'extName'|i18n"></h2>
+    <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>

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

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

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

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

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

@@ -0,0 +1,21 @@
+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);
+      });
+    },
+  });
+});

+ 26 - 0
src/options/components/script.html

@@ -0,0 +1,26 @@
+<div class="script" :class="{disabled:!script.enabled}" draggable="true">
+  <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>
+    <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>
+  <div class=script-info>
+    <a class="script-name ellipsis" target=_blank :href="homepageURL"
+      v-text="script.custom.name||getLocaleString('name')"></a>
+    <a class="script-support" :class="{hide:!script.meta.supportURL}"
+      target=_blank :href="script.meta.supportURL">
+      <svg class="icon"><use xlink:href="#question"/></svg>
+    </a>
+  </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-if="canUpdate" :disabled="script.checking"
+      v-text="'buttonUpdate'|i18n" @click="onUpdate"></button>
+    <span>{{script.message}}</span>
+  </div>
+</div>

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

@@ -0,0 +1,253 @@
+define('views/Script', function (require, _exports, module) {
+  var store = require('utils').store;
+  var cache = require('cache');
+  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();
+    }
+  };
+});

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

@@ -0,0 +1,14 @@
+<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="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>

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

@@ -0,0 +1,58 @@
+define('views/SyncService', function (require, _exports, module) {
+  var cache = require('cache');
+
+  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);
+      },
+    },
+  };
+});

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

@@ -0,0 +1,28 @@
+<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>

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

@@ -0,0 +1,14 @@
+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;
+    },
+  };
+});

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

@@ -0,0 +1,18 @@
+<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>

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

@@ -0,0 +1,85 @@
+define('views/TabInstalled', function (require, _exports, module) {
+  var ScriptItem = require('views/Script');
+  var Edit = require('views/Edit');
+  var cache = require('cache');
+  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 collection = model.collection;
+          var models = collection.models;
+          var i = Math.min(data.from, data.to);
+          var j = Math.max(data.from, data.to);
+          var seq = [
+            models.slice(0, i),
+            models.slice(i, j + 1),
+            models.slice(j + 1),
+          ];
+          i === data.to
+          ? seq[1].unshift(seq[1].pop())
+          : seq[1].push(seq[1].shift());
+          collection.models = seq.concat.apply([], seq);
+        });
+      },
+    },
+    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),
+          });
+        }
+      },
+    },
+  };
+});

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

@@ -0,0 +1,40 @@
+<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>

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

@@ -0,0 +1,234 @@
+define('views/TabSettings', function (require, _exports, module) {
+  function importData(file) {
+    function getVMConfig(text) {
+      var vm;
+      try {
+        vm = JSON.parse(text);
+      } catch (e) {
+        console.warn('Error parsing ViolentMonkey configuration.');
+      }
+      vm = vm || {};
+      _.forEach(vm.values, function (value, key) {
+        _.sendMessage({
+          cmd: 'SetValue',
+          data: {
+            uri: key,
+            values: value,
+          }
+        });
+      });
+      _.forEach(vm.settings, function (value, key) {
+        _.options.set(key, value);
+      });
+      return vm;
+    }
+    function getVMFile(entry, vm) {
+      if (!entry.filename.endsWith('.user.js')) return;
+      vm = vm || {};
+      return new Promise(function (resolve, _reject) {
+        var writer = new zip.TextWriter;
+        entry.getData(writer, function (text) {
+          var script = {code: text};
+          if (vm.scripts) {
+            var more = vm.scripts[entry.filename.slice(0, -8)];
+            if (more) script.more = _.omit(more, ['id']);
+          }
+          _.sendMessage({
+            cmd: 'ParseScript',
+            data: script,
+          }).then(function () {
+            resolve(true);
+          });
+        });
+      });
+    }
+    function getVMFiles(entries) {
+      var i = _.findIndex(entries, function (entry) {
+        return entry.filename === 'ViolentMonkey';
+      });
+      if (~i) return new Promise(function (resolve, _reject) {
+        var writer = new zip.TextWriter;
+        entries[i].getData(writer, function (text) {
+          entries.splice(i, 1);
+          resolve({
+            vm: getVMConfig(text),
+            entries: entries,
+          });
+        });
+      });
+      return {
+        entries: entries,
+      };
+    }
+    function readZip(file) {
+      return new Promise(function (resolve, reject) {
+        zip.createReader(new zip.BlobReader(file), function (res) {
+          res.getEntries(function (entries) {
+            resolve(entries);
+          });
+        }, function (err) {reject(err);});
+      });
+    }
+    readZip(file).then(getVMFiles).then(function (data) {
+      var vm = data.vm;
+      var entries = data.entries;
+      return Promise.all(entries.map(function (entry) {
+        return getVMFile(entry, vm);
+      })).then(function (res) {
+        return _.filter(res).length;
+      });
+    }).then(function (count) {
+      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] = _.pick(script, ['id', 'custom', 'enabled', 'update']);
+        if (withValues) {
+          var values = data.values[script.uri];
+          if (values) vm.values[script.uri] = values;
+        }
+        return {
+          name: name + '.user.js',
+          content: script.code,
+        };
+      });
+      files.push({
+        name: 'ViolentMonkey',
+        content: JSON.stringify(vm),
+      });
+      return files;
+    }).then(function (files) {
+      return files.reduce(function (result, file) {
+        return result.then(function (writer) {
+          return addFile(writer, file);
+        });
+      }, getWriter()).then(download);
+    });
+  }
+
+  var Message = require('views/Message');
+  var SyncService = require('views/SyncService');
+  var store = require('utils').store;
+  var cache = require('cache');
+
+  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) {
+        // TODO disable other services
+        _.sendMessage({cmd: 'SyncStart'});
+      },
+    },
+    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(function () {})
+        .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 - 4
src/options/index.html

@@ -5,16 +5,14 @@
     <title data-i18n="extName"></title>
     <link rel="shortcut icon" type="image/png" href="/images/icon16.png">
     <link rel="stylesheet" href="style.css">
-    <script src="/lib/zepto.min.js"></script>
-    <script src="/lib/underscore-min.js"></script>
-    <script src="/lib/backbone-min.js"></script>
+    <script src="/lib/vue.min.js"></script>
     <script src="/lib/zip.js/zip.js"></script>
     <script src="/lib/require-lite.js"></script>
     <script src="/common.js"></script>
     <script src="/cache.js"></script>
   </head>
   <body>
-    <div id="app"></div>
+    <component :is="type" :params="params"></component>
     <script src="app.js"></script>
   </body>
 </html>

+ 0 - 55
src/options/model.js

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

+ 32 - 36
src/options/style.css

@@ -3,6 +3,7 @@
   padding: 0;
   box-sizing: border-box;
 }
+html,
 body {
   height: 100%;
   background: #eee;
@@ -44,14 +45,27 @@ hr {
   text-overflow: ellipsis;
   overflow: hidden;
 }
-
-#app {
+.dropdown {
+  position: relative;
+  cursor: pointer;
+  display: inline-block;
+}
+.dropdown:not(.open) .dropdown-menu {
+  display: none;
+}
+.dropdown-menu {
   position: absolute;
-  top: 0;
-  left: 0;
+  top: 100%;
   right: 0;
-  bottom: 0;
+  width: 180px;
+  margin-top: 5px;
+  padding: 1em;
+  background: #f8f8f8;
+  border-radius: 4px;
+  box-shadow: 1px 2px 2px gray;
+  z-index: 10;
 }
+
 .main {
   position: relative;
   max-width: 900px;
@@ -85,14 +99,15 @@ aside img {
 .sidemenu > a:hover {
   color: black;
 }
-#tab {
+.content {
   position: relative;
   height: 100%;
   margin-left: 200px;
+  padding: 20px;
   background: white;
   border-left: 1px solid darkgray;
   border-right: 1px solid darkgray;
-  overflow-y: hidden;
+  overflow-y: auto;
 }
 .content > header {
   height: 30px;
@@ -138,11 +153,6 @@ aside img {
   display: block;
   line-height: 2;
 }
-.content {
-  height: 100%;
-  padding: 20px;
-  overflow-y: auto;
-}
 .no-pad {
   padding: 0;
 }
@@ -214,25 +224,9 @@ fieldset.title {
   display: inline-block;
   position: relative;
 }
-.button-panel {
-  display: none;
-  position: absolute;
-  top: 100%;
-  right: 0;
-  width: 180px;
-  margin-top: 5px;
-  padding: 1em;
-  background: #f8f8f8;
-  border-radius: 4px;
-  box-shadow: 1px 2px 2px gray;
-  z-index: 10;
-}
 .options-panel label {
   display: block;
 }
-.button-toggle.active ~ .button-panel {
-  display: block;
-}
 .editor-code, .editor-code .CodeMirror {
   height: 100%;
 }
@@ -304,7 +298,7 @@ fieldset.title {
 .dragging {
   position: fixed;
   margin: 0;
-  background: lightgreen;
+  background: wheat;
   z-index: 9;
 }
 .edit .frame-header {
@@ -313,13 +307,13 @@ fieldset.title {
 .edit .frame-header ~ .frame-body {
   top: 4rem;
 }
-.edit .button-panel {
+.edit .dropdown-menu {
   width: 450px;
 }
-.edit .button-panel > table {
+.edit .dropdown-menu > table {
   width: 100%;
 }
-.edit .button-panel td:not(.expand) {
+.edit .dropdown-menu td:not(.expand) {
   width: 1px;
   white-space: nowrap;
 }
@@ -348,15 +342,17 @@ fieldset.title {
   margin-left: -100px;
   padding: 16px;
   z-index: 10;
-  transform: translateY(-120%);
-  transition: transform .5s;
   background: white;
   border-bottom-left-radius: 4px;
   border-bottom-right-radius: 4px;
   box-shadow: 0 0 5px rgba(0,0,0,.2);
 }
-.message-show {
-  transform: translateY(0);
+.message-transition {
+  transition: transform .5s;
+}
+.message-enter,
+.message-leave {
+  transform: translateY(-120%);
 }
 .icon {
   width: 14px;

+ 0 - 11
src/options/templates/confirm-options.html

@@ -1,11 +0,0 @@
-<label>
-  <input type=checkbox data-check="closeAfterInstall" id="cbClose" <%=
-  it.closeAfterInstall ? 'checked' : '' %>>
-  <span data-i18n=installOptionClose></span>
-</label>
-<label>
-  <input type=checkbox data-check="trackLocalFile" id="cbTrack" <%=
-  it.trackLocalFile ? 'checked' : '' %> <%=
-  it.closeAfterInstall ? 'disabled' : '' %>>
-  <span data-i18n=installOptionTrack></span>
-</label>

+ 0 - 16
src/options/templates/confirm.html

@@ -1,16 +0,0 @@
-<div class=frame-header>
-  <div class=buttons>
-    <div>
-      <button class="button-toggle" data-i18n=buttonInstallOptions></button>
-      <!--<div class="button-panel options-panel"></div>-->
-    </div>
-    <button id=btnInstall data-i18n=buttonConfirmInstallation></button>
-    <button id=btnClose data-i18n=buttonClose></button>
-  </div>
-  <h1><span data-i18n=labelInstall></span> - <span data-i18n=extName></span></h1>
-  <div id=url class=ellipsis></div>
-  <div id=msg class=ellipsis></div>
-</div>
-<div class=frame-body>
-  <div class=editor-code></div>
-</div>

+ 0 - 70
src/options/templates/edit-meta.html

@@ -1,70 +0,0 @@
-<table>
-  <tr>
-    <td title="@name" data-i18n=labelName></td>
-    <td class=expand>
-      <input type=text data-id=name value="<%- it.name %>" placeholder="<%- it.__name %>">
-    </td>
-    <td title="@run-at" data-i18n=labelRunAt></td>
-    <td>
-      <select data-id=run-at value="<%= it['run-at'] || 'default' %>">
-        <option value=default data-i18n=labelRunAtDefault></option>
-        <option value=start>document-start</option>
-        <option value=idle>document-idle</option>
-        <option value=end>document-end</option>
-      </select>
-    </td>
-  </tr>
-  <tr title="@homepageURL">
-    <td data-i18n=labelHomepageURL></td>
-    <td colspan=3 class=expand>
-      <input type=text data-id=homepageURL value="<%- it.homepageURL %>" placeholder="<%- it.__homepageURL %>">
-    </td>
-  </tr>
-</table>
-<table>
-  <tr title="@updateURL">
-    <td data-i18n=labelUpdateURL></td>
-    <td class=expand>
-      <input type=text data-id=updateURL value="<%- it.updateURL %>" placeholder="<%- it.__updateURL %>">
-    </td>
-  </tr>
-  <tr title="@downloadURL">
-    <td data-i18n=labelDownloadURL></td>
-    <td class=expand>
-      <input type=text data-id=downloadURL value="<%- it.downloadURL %>" placeholder="<%- it.__downloadURL %>">
-    </td>
-  </tr>
-</table>
-<fieldset title="@include">
-  <legend>
-    <span data-i18n=labelInclude></span>
-    <label>
-      <input type=checkbox data-id="!_include" <%= it._include !== false ? 'checked' : '' %>>
-      <span data-i18n=labelKeepInclude></span>
-    </label>
-  </legend>
-  <div data-i18n=labelCustomInclude></div>
-  <textarea data-id="[include"><%- (it.include || []).join('\n') %></textarea>
-</fieldset>
-<fieldset title="@match">
-  <legend>
-    <span data-i18n=labelMatch></span>
-    <label>
-      <input type=checkbox data-id="!_match" <%= it._match !== false ? 'checked' : '' %>>
-      <span data-i18n=labelKeepMatch></span>
-    </label>
-  </legend>
-  <div data-i18n=labelCustomMatch></div>
-  <textarea data-id="[match"><%- (it.match || []).join('\n') %></textarea>
-</fieldset>
-<fieldset title="@exclude">
-  <legend>
-    <span data-i18n=labelExclude></span>
-    <label>
-      <input type=checkbox data-id="!_exclude" <%= it._exclude !== false ? 'checked' : '' %>>
-      <span data-i18n=labelKeepExclude></span>
-    </label>
-  </legend>
-  <div data-i18n=labelCustomExclude></div>
-  <textarea data-id="[exclude"><%- (it.exclude || []).join('\n') %></textarea>
-</fieldset>

+ 0 - 22
src/options/templates/edit.html

@@ -1,22 +0,0 @@
-<div class="frame-header">
-  <div class="buttons">
-    <div>
-      <button class="button-toggle" data-i18n=buttonCustomMeta></button>
-    </div>
-  </div>
-  <h2 data-i18n="labelScriptEditor"></h2>
-</div>
-<div class="frame-footer">
-  <div class="pull-right">
-    <button id="editorSave" data-i18n=buttonSave></button>
-    <button id="editorSaveClose" data-i18n=buttonSaveClose></button>
-    <button id="editorClose" data-i18n=buttonClose></button>
-  </div>
-  <label>
-    <input type=checkbox data-id="!update" <%= it.update ? 'checked' : '' %>>
-    <span data-i18n=labelAllowUpdate></span>
-  </label>
-</div>
-<div class=frame-body>
-  <div class="editor-code"></div>
-</div>

+ 0 - 14
src/options/templates/main.html

@@ -1,14 +0,0 @@
-<aside>
-  <img src="/images/icon128.png">
-  <h2 data-i18n=extName></h2>
-  <div class="line">2013-2016</div>
-  <hr>
-  <div class=sidemenu>
-    <a href="#main/installed" <%= it.tab == 'main' ? 'class="active"' : ''  %> data-i18n=sideMenuInstalled></a>
-    <a href="#main/settings" <%= it.tab == 'settings' ? 'class="active"' : '' %> data-feature="settings">
-      <span data-i18n=sideMenuSettings class="feature-text"></span>
-    </a>
-    <a href="#main/about" <%= it.tab == 'about' ? 'class="active"' : '' %> data-i18n=sideMenuAbout></a>
-  </div>
-</aside>
-<div id="tab"></div>

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

@@ -1 +0,0 @@
-<div><%= it.data %></div>

+ 0 - 1
src/options/templates/option.html

@@ -1 +0,0 @@
-<option class="ellipsis" selected><%- it.meta.name %></option>

+ 0 - 33
src/options/templates/script.html

@@ -1,33 +0,0 @@
-<img class=script-icon src="<%- it._icon %>">
-<div class="script-version pull-right">
-  <%- it.meta.version ? 'v' + it.meta.version : '' %>
-</div>
-<div class="script-author ellipsis pull-right" title="<%- it.meta.author || '' %>">
-  <%= it.author %>
-</div>
-<div class=script-info>
-  <a class="script-name ellipsis" target=_blank
-    <%= it.homepageURL ? 'href="' + _.escape(it.homepageURL)  + '"' : '' %>
-    ><%- it.custom.name || it.getLocaleString('name') %>
-  </a>
-  <a class="script-support <%= it.meta.supportURL ? '' : 'hide' %>"
-    target=_blank href="<%- it.meta.supportURL %>">
-    <svg class="icon"><use xlink:href="#question"/></svg>
-  </a>
-</div>
-<p class="script-desc ellipsis">
-  <%- it.custom.description || it.getLocaleString('description') %>
-</p>
-<div class=buttons>
-  <button data-id=edit data-i18n="buttonEdit"></button>
-  <button data-id=enable><%=
-    it.enabled ? _.i18n('buttonDisable') : _.i18n('buttonEnable')
-  %></button>
-  <button data-id=remove data-i18n="buttonRemove"></button>
-  <%= it.canUpdate ?
-  '<button data-id=update data-i18n="buttonUpdate"' + (
-    it.checking ? ' disabled' : ''
-  ) + '></button>'
-  : '' %>
-  <span data-id=message><%= it.message %></span>
-</div>

+ 0 - 29
src/options/templates/sync-service.html

@@ -1,29 +0,0 @@
-<label>
-  <input type=checkbox data-check="<%= it.name + 'Enabled' %>" data-sync="<%= it.name %>" <%=
-  it.enabled ? 'checked' : ''
-  %>>
-  <span><%= _.i18n('labelSyncTo', it.displayName || it.name) %></span>
-</label>
-<button data-auth="<%= it.name %>" <%=
-  it.authState === 'unauthorized' ? '' : 'disabled'
-%>><%=
-  {
-    authorized: _.i18n('buttonAuthorized'),
-    authorizing: _.i18n('buttonAuthorizing'),
-  }[it.authState] || _.i18n('buttonAuthorize')
-%></button>
-<button <%=
-  !_.includes(['authorized', 'error'], it.authState) ||
-  _.includes(['ready', 'syncing'], it.syncState)
-  ? 'disabled' : ''
-%> class="sync-start">
-  <svg class="icon"><use xlink:href="#refresh"/></svg>
-</button>
-<span><%=
-  it.authState === 'initializing' ? _.i18n('msgSyncInit') :
-  it.authState === 'error' ? _.i18n('msgSyncInitError') :
-  it.syncState === 'error' ? _.i18n('msgSyncError') :
-  it.syncState === 'ready' ? _.i18n('msgSyncReady') :
-  it.syncState === 'syncing' ? _.i18n('msgSyncing') + it.progress :
-  it.lastSync ? _.i18n('lastSync', it.lastSync) : ''
-%></span>

+ 0 - 26
src/options/templates/tab-about.html

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

+ 0 - 10
src/options/templates/tab-installed.html

@@ -1,10 +0,0 @@
-<header>
-  <button id=bNew data-i18n=buttonNew></button>
-  <button id=bUpdate data-i18n=buttonUpdateAll></button>
-  <button id=bURL data-i18n=buttonInstallFromURL></button>
-  <div class="pull-right">
-    <a href=https://greasyfork.org/scripts target=_blank data-i18n=anchorGetMoreScripts></a>
-  </div>
-</header>
-<div class="backdrop"><div></div></div>
-<div class="scripts"></div>

+ 0 - 43
src/options/templates/tab-settings.html

@@ -1,43 +0,0 @@
-<h1 data-i18n=labelSettings></h1>
-<label class="line">
-  <input id=cUpdate type=checkbox data-check=autoUpdate <%= it.autoUpdate ? 'checked' : '' %>>
-  <span data-i18n=labelAutoUpdate></span>
-</label>
-<label class="line">
-  <input type=checkbox data-check=ignoreGrant <%= it.ignoreGrant ? 'checked' : '' %>>
-  <span data-i18n=labelIgnoreGrant></span>
-</label>
-<label class="line">
-  <input type=checkbox data-check=autoReload <%= it.autoReload ? 'checked' : '' %>>
-  <span data-i18n=labelAutoReloadCurrentTab></span>
-</label>
-<!--
-<label class="line">
-  <span data-i18n=labelInjectMode></span>
-  <select id=sInjectMode>
-    <option value=0 data-i18n=injectModeNormal></option>
-    <option value=1 data-i18n=injectModeAdvanced></option>
-  </select>
-  <span></span>
-</label>
--->
-<fieldset class=title>
-  <legend data-i18n=labelDataImport></legend>
-  <button id=bImport data-i18n=buttonImportData></button>
-  <button id=bVacuum data-i18n=buttonVacuum title="<%- _.i18n('hintVacuum') %>"></button>
-</fieldset>
-<fieldset class=title>
-  <legend data-i18n=labelDataExport></legend>
-  <b data-i18n=labelScriptsToExport></b>
-  <label>
-    <input id=cbValues type=checkbox data-check=exportValues <%= it.exportValues ? 'checked' : '' %>>
-    <span data-i18n=labelExportScriptData></span>
-  </label>
-  <select class=export-list multiple></select>
-  <button id=bSelect data-i18n=buttonAllNone></button>
-  <button id=bExport data-i18n=buttonExportData></button>
-</fieldset>
-<fieldset class=title data-feature="sync">
-  <legend data-i18n=labelSync class="feature-text"></legend>
-  <div class="sync-services"></div>
-</fieldset>

+ 32 - 0
src/options/utils/dropdown.js

@@ -0,0 +1,32 @@
+define('utils/dropdown', function () {
+  Vue.directive('dropdown', {
+    bind: function () {
+      var _this = this;
+      const dropdown = _this.data = {
+        toggle: _this.el.querySelector('[dropdown-toggle]'),
+        data: {
+          isOpen: false,
+        },
+      };
+      const 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');
+    },
+  });
+});

+ 53 - 0
src/options/utils/features.js

@@ -0,0 +1,53 @@
+define('utils/features', function (_require, exports, _module) {
+  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();
+      }
+
+      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();
+    },
+  });
+});

+ 36 - 0
src/options/utils/index.js

@@ -0,0 +1,36 @@
+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,
+      };
+    });
+    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] = matches[i + 1];
+            return params;
+          }, {});
+        }
+      }
+    };
+  }
+
+  exports.routeTester = routeTester;
+  exports.store = {};
+
+  Vue.filter('i18n', _.i18n);
+  require('utils/dropdown');
+  require('utils/features');
+  require('utils/settings');
+});

+ 19 - 0
src/options/utils/settings.js

@@ -0,0 +1,19 @@
+define('utils/settings', function () {
+  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);
+    },
+    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);
+    },
+  });
+});

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

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

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

@@ -1,201 +0,0 @@
-define('views/Confirm', function (require, _exports, module) {
-  var editor = require('editor');
-  var ConfirmOptionsView = require('views/ConfirmOptions');
-  var BaseView = require('cache').BaseView;
-  module.exports = BaseView.extend({
-    events: {
-      'click .button-toggle': 'toggleOptions',
-      'click #btnInstall': 'installScript',
-      'click #btnClose': 'close',
-    },
-    templateUrl: '/options/templates/confirm.html',
-    initialize: function () {
-      var _this = this;
-      _.bindAll(_this, 'hideOptions', 'trackLocalFile');
-      BaseView.prototype.initialize.call(_this);
-    },
-    _render: function () {
-      var _this = this;
-      _this.$el.html(_this.templateFn());
-      _this.loadedEditor = editor.init({
-        container: _this.$('.editor-code')[0],
-        readonly: true,
-        onexit: _this.close,
-      }).then(function (editor) {
-        _this.editor = editor;
-      });
-      _this.$toggler = _this.$('.button-toggle');
-      _this.$('#url').attr('title', _this.url).text(_this.url);
-      _this.showMessage(_.i18n('msgLoadingData'));
-    },
-    initData: function (url, referer) {
-      var _this = this;
-      _this.url = url;
-      _this.from = referer;
-      _this.render();
-      _this.loadData().then(function () {
-        _this.parseMeta();
-      });
-    },
-    loadData: function (changedOnly) {
-      var _this = this;
-      var _text = _this.data && _this.data.code;
-      _this.$('#btnInstall').prop('disabled', true);
-      _this.data = {
-        code: _text,
-        require: {},
-        resources: {},
-        dependencyOK: false,
-        isLocal: /^file:\/\/\//.test(_this.url),
-      };
-      return _this.getScript(_this.url).then(function (text) {
-        if (changedOnly && _text == text) return Promise.reject();
-        _this.data.code = text;
-        _this.loadedEditor.then(function () {
-          _this.editor.setValueAndFocus(_this.data.code);
-        });
-      });
-    },
-    parseMeta: function () {
-      var _this = this;
-      return _.sendMessage({
-        cmd: 'ParseMeta',
-        data: _this.data.code,
-      }).then(function (script) {
-        var urls = _.values(script.resources);
-        var length = script.require.length + urls.length;
-        var finished = 0;
-        if (!length) return;
-        var error = [];
-        var updateStatus = function () {
-          _this.showMessage(_.i18n('msgLoadingDependency', [finished, length]));
-        };
-        updateStatus();
-        var promises = script.require.map(function (url) {
-          return _this.getFile(url).then(function (res) {
-            _this.data.require[url] = res;
-          });
-        });
-        promises = promises.concat(urls.map(function (url) {
-          return _this.getFile(url, true).then(function (res) {
-            _this.data.resources[url] = res;
-          });
-        }));
-        promises = promises.map(function (promise) {
-          return promise.then(function () {
-            finished += 1;
-            updateStatus();
-          }, function (url) {
-            error.push(url);
-          });
-        });
-        return Promise.all(promises).then(function () {
-          if (error.length) return Promise.reject(error.join('\n'));
-          _this.data.dependencyOK = true;
-        });
-      }).then(function () {
-        _this.showMessage(_.i18n('msgLoadedData'));
-        _this.$('#btnInstall').prop('disabled', false);
-      }, function (error) {
-        _this.showMessage(_.i18n('msgErrorLoadingDependency'), error);
-        return Promise.reject();
-      });
-    },
-    hideOptions: function () {
-      if (!this.optionsView) return;
-      this.$toggler.removeClass('active');
-      this.optionsView.remove();
-      this.optionsView = null;
-    },
-    toggleOptions: function () {
-      if (this.optionsView) {
-        this.hideOptions();
-      } else {
-        this.$toggler.addClass('active');
-        this.optionsView = new ConfirmOptionsView;
-        this.optionsView.$el.insertAfter(this.$toggler);
-        $(document).one('mousedown', this.hideOptions);
-      }
-    },
-    close: function () {
-      window.close();
-    },
-    showMessage: function (msg) {
-      this.$('#msg').html(msg);
-    },
-    getFile: function (url, isBlob) {
-      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.showMessage(_.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.$('#btnInstall').prop('disabled', true);
-      _.sendMessage({
-        cmd:'ParseScript',
-        data:{
-          url: _this.url,
-          from: _this.from,
-          code: _this.data.code,
-          require: _this.data.require,
-          resources: _this.data.resources,
-        },
-      }).then(function (res) {
-        _this.showMessage(res.message + '[' + _this.getTimeString() + ']');
-        if (res.code < 0) return;
-        if (_.options.get('closeAfterInstall')) _this.close();
-        else if (_this.data.isLocal && _.options.get('trackLocalFile')) _this.trackLocalFile();
-      });
-    },
-    trackLocalFile: function () {
-      var _this = this;
-      setTimeout(function () {
-        _this.loadData(true).then(function () {
-          var track = _.options.get('trackLocalFile');
-          if (!track) return;
-          _this.parseMeta().then(function () {
-            track && _this.installScript();
-          });
-        }, _this.trackLocalFile);
-      }, 2000);
-    },
-  });
-});

+ 0 - 28
src/options/views/edit-meta.js

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

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

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

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

@@ -1,36 +0,0 @@
-define('views/Main', function (require, _exports, module) {
-  var BaseView = require('cache').BaseView;
-  var MainTab = require('views/TabInstalled');
-  var SettingsTab = require('views/TabSettings');
-  var AboutTab = require('views/TabAbout');
-  module.exports = BaseView.extend({
-    className: 'main',
-    templateUrl: '/options/templates/main.html',
-    tabs: {
-      main: MainTab,
-      settings: SettingsTab,
-      about: AboutTab,
-    },
-    initialize: function () {
-      var _this = this;
-      _this.model = new Backbone.Model({
-        tab: null,
-      });
-      _this.listenTo(_this.model, 'change', _this.render);
-      BaseView.prototype.initialize.call(_this);
-    },
-    _render: function () {
-      var _this = this;
-      var Tab = _this.model.get('tab');
-      var name = Tab.prototype.name;
-      _this.$el.html(_this.templateFn({tab: name}));
-      _this.loadSubview(name, function () {
-        return new Tab;
-      }, '#tab');
-    },
-    loadTab: function (name) {
-      var _this = this;
-      _this.model.set('tab', _this.tabs[name] || _this.tabs['main']);
-    },
-  });
-});

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

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

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

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

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

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

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

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

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

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

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

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

Datei-Diff unterdrückt, da er zu groß ist
+ 5 - 0
src/public/lib/vue.min.js


Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.