1
0
Эх сурвалжийг харах

fix: migrate options to chrome.storage

Gerald 8 жил өмнө
parent
commit
736e0cf346

+ 1 - 2
package.json

@@ -11,7 +11,7 @@
     "i18n": "gulp i18n",
     "lint": "eslint --ext .js,.vue .",
     "svgo": "svgo --config .svgo.yml icons",
-    "pretest": "webpack --config scripts/webpack.test.conf.js",
+    "pretest": "NODE_ENV=test webpack --config scripts/webpack.test.conf.js",
     "test": "node dist/test",
     "prepush": "npm run lint"
   },
@@ -41,7 +41,6 @@
     "html-webpack-plugin": "^2.29.0",
     "husky": "^0.14.3",
     "js-yaml": "^3.9.0",
-    "localStorage": "^1.0.3",
     "postcss-loader": "^2.0.6",
     "postcss-scss": "^1.0.2",
     "precss": "^2.0.0",

+ 2 - 0
scripts/utils.js

@@ -1,6 +1,7 @@
 const ExtractTextPlugin = require('extract-text-webpack-plugin');
 process.env.NODE_ENV = process.env.NODE_ENV || 'development';
 const IS_DEV = process.env.NODE_ENV === 'development';
+const IS_TEST = process.env.NODE_ENV === 'test';
 
 function styleLoader({ loaders = [], extract = !IS_DEV, minimize = !IS_DEV, fallback = 'style-loader' } = {}) {
   const cssLoader = {
@@ -43,6 +44,7 @@ function merge(obj1, obj2) {
 }
 
 exports.IS_DEV = IS_DEV;
+exports.IS_TEST = IS_TEST;
 exports.styleLoader = styleLoader;
 exports.styleRule = styleRule;
 exports.merge = merge;

+ 0 - 3
scripts/webpack.conf.js

@@ -52,9 +52,6 @@ targets.push(merge(base, {
     // new FriendlyErrorsPlugin(),
     !IS_DEV && new ExtractTextPlugin('[name].css'),
   ].filter(Boolean),
-  externals: {
-    localStorage: 'localStorage',
-  },
 }));
 
 targets.push(merge(base, {

+ 3 - 1
src/background/app.js

@@ -8,6 +8,7 @@ import {
   newScript, parseMeta,
   setClipboard, checkUpdate,
   getOption, setOption, hookOptions, getAllOptions,
+  initialize,
 } from './utils';
 
 const VM_VER = browser.runtime.getManifest().version;
@@ -197,7 +198,8 @@ const commands = {
   },
 };
 
-vmdb.initialized.then(() => {
+initialize()
+.then(() => {
   browser.runtime.onMessage.addListener((req, src) => {
     const func = commands[req.cmd];
     let res;

+ 2 - 1
src/background/utils/db.js

@@ -2,6 +2,7 @@ import Promise from 'sync-promise-lite';
 import { i18n, request, buffer2string, getFullUrl } from 'src/common';
 import { getNameURI, getScriptInfo, isRemote, parseMeta, newScript } from './script';
 import { testScript, testBlacklist } from './tester';
+import { register } from './init';
 
 let db;
 
@@ -18,7 +19,7 @@ const position = {
   },
 };
 
-export const initialized = openDatabase().then(initPosition);
+register(openDatabase().then(initPosition));
 
 function openDatabase() {
   return new Promise((resolve, reject) => {

+ 1 - 0
src/background/utils/index.js

@@ -9,6 +9,7 @@ export * from './options';
 export * from './requests';
 export * as vmdb from './db';
 export * from './search';
+export { initialize } from './init';
 
 export function notify(options) {
   browser.notifications.create(options.id || 'ViolentMonkey', {

+ 13 - 0
src/background/utils/init.js

@@ -0,0 +1,13 @@
+const initializers = [];
+
+export function register(init) {
+  initializers.push(init);
+}
+
+export function initialize() {
+  return Promise.all(initializers.map(init => {
+    if (typeof init === 'function') return init();
+    return init;
+  }))
+  .then(() => {});
+}

+ 35 - 14
src/background/utils/options.js

@@ -1,5 +1,5 @@
 import { initHooks, debounce, normalizeKeys, object } from 'src/common';
-import storage from 'localStorage'; // eslint-disable-line import/no-extraneous-dependencies
+import { register } from './init';
 
 const defaults = {
   isApplied: true,
@@ -23,6 +23,34 @@ let changes = {};
 const hooks = initHooks();
 const callHooksLater = debounce(callHooks, 100);
 
+let options = {};
+const init = browser.storage.local.get('options')
+.then(({ options: value }) => {
+  options = value;
+  if (!options || typeof options !== 'object') options = {};
+});
+register(init);
+
+// v2.8.0+ stores options in browser.storage.local
+// Upgrade from v2.7.x
+if (localStorage.length) {
+  Object.keys(defaults)
+  .forEach(key => {
+    let value = localStorage.getItem(key);
+    if (value) {
+      try {
+        value = JSON.parse(value);
+      } catch (e) {
+        value = null;
+      }
+    }
+    if (value) {
+      setOption(key, value);
+    }
+    localStorage.clear();
+  });
+}
+
 function fireChange(key, value) {
   changes[key] = value;
   callHooksLater();
@@ -36,18 +64,10 @@ function callHooks() {
 export function getOption(key, def) {
   const keys = normalizeKeys(key);
   const mainKey = keys[0];
-  const value = storage.getItem(mainKey);
-  let obj;
-  if (value) {
-    try {
-      obj = JSON.parse(value);
-    } catch (e) {
-      // ignore invalid JSON
-    }
-  }
-  if (obj == null) obj = defaults[mainKey];
-  if (obj == null) obj = def;
-  return keys.length > 1 ? object.get(obj, keys.slice(1), def) : obj;
+  let value = options[mainKey];
+  if (value == null) value = defaults[mainKey];
+  if (value == null) value = def;
+  return keys.length > 1 ? object.get(value, keys.slice(1), def) : value;
 }
 
 export function setOption(key, value) {
@@ -59,7 +79,8 @@ export function setOption(key, value) {
     if (keys.length > 1) {
       optionValue = object.set(getOption(mainKey), keys.slice(1), value);
     }
-    storage.setItem(mainKey, JSON.stringify(optionValue));
+    options[mainKey] = optionValue;
+    browser.storage.local.set({ options });
     fireChange(optionKey, value);
   }
 }

+ 2 - 1
src/background/utils/tester.js

@@ -77,7 +77,8 @@ function autoReg(str) {
   if (str.length > 1 && str[0] === '/' && str[str.length - 1] === '/') {
     return RegExp(str.slice(1, -1)); // Regular-expression
   }
-  return str2RE(str); // String with wildcards
+  const re = str2RE(str); // String with wildcards
+  return { test: tstr => re.test(tstr) };
 }
 
 function matchScheme(rule, data) {

+ 10 - 0
src/common/browser.js

@@ -59,6 +59,9 @@ const meta = {
             }, error => {
               if (process.env.DEBUG) console.warn(error);
               sendResponse({ error });
+            })
+            .catch(() => {
+              // Ignore sendResponse error
             });
             return true;
           } else if (typeof result !== 'undefined') {
@@ -90,6 +93,13 @@ const meta = {
       };
     },
   },
+  storage: {
+    local: {
+      get: wrapAsync,
+      set: wrapAsync,
+      remove: wrapAsync,
+    },
+  },
   tabs: {
     onUpdated: true,
     onRemoved: true,

+ 2 - 1
src/manifest.json

@@ -46,6 +46,7 @@
     "<all_urls>",
     "webRequest",
     "webRequestBlocking",
-    "notifications"
+    "notifications",
+    "storage"
   ]
 }

+ 1 - 3
src/options/app.js

@@ -7,7 +7,7 @@ import options from 'src/common/options';
 import getPathInfo from 'src/common/pathinfo';
 import handlers from 'src/common/handlers';
 import 'src/common/ui/style';
-import { store, features } from './utils';
+import { store } from './utils';
 import App from './views/app';
 
 Vue.prototype.i18n = i18n;
@@ -64,8 +64,6 @@ function loadData() {
       store.scripts.forEach(initSearch);
     }
     store.loading = false;
-    // features.reset(data.version);
-    features.reset('sync');
   });
 }
 

+ 0 - 19
src/options/style.css

@@ -49,25 +49,6 @@ section {
     margin-bottom: .3rem;
   }
 }
-.feature {
-  &-text {
-    position: relative;
-    &::after {
-      .feature & {
-        content: '';
-        display: block;
-        position: absolute;
-        width: 6px;
-        height: 6px;
-        top: -.1rem;
-        left: 100%;
-        border-radius: 50%;
-        margin-left: .1rem;
-        background: red;
-      }
-    }
-  }
-}
 
 .tab {
   position: relative;

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

@@ -1,79 +0,0 @@
-import Vue from 'vue';
-import { initHooks } from 'src/common';
-import options from 'src/common/options';
-
-const FEATURES = 'features';
-let features = options.get(FEATURES);
-let hooks = initHooks();
-let revoke = options.hook((data) => {
-  if (data[FEATURES]) {
-    features = data[FEATURES];
-    revoke();
-    revoke = null;
-    hooks.fire();
-    hooks = null;
-  }
-});
-if (!features || !features.data) {
-  features = {
-    data: {},
-  };
-}
-const items = {};
-
-export default function resetFeatures(version) {
-  if (features.version !== version) {
-    options.set(FEATURES, features = {
-      version,
-      data: {},
-    });
-  }
-}
-
-function getContext(el, value) {
-  function onFeatureClick() {
-    features.data[value] = 1;
-    options.set(FEATURES, features);
-    el.classList.remove('feature');
-    el.removeEventListener('click', onFeatureClick, false);
-  }
-  function clear() {
-    el.removeEventListener('click', onFeatureClick, false);
-  }
-  function reset() {
-    clear();
-    if (!features.version || features.data[value]) return;
-    el.classList.add('feature');
-    el.addEventListener('click', onFeatureClick, false);
-  }
-  return {
-    el,
-    clear,
-    reset,
-  };
-}
-
-Vue.directive('feature', {
-  bind(el, binding) {
-    const { value } = binding;
-    const item = getContext(el, value);
-    let list = items[value];
-    if (!list) {
-      list = [];
-      items[value] = list;
-    }
-    list.push(item);
-    item.reset();
-    if (hooks) hooks.hook(item.reset);
-  },
-  unbind(el, binding) {
-    const list = items[binding.value];
-    if (list) {
-      const index = list.findIndex(item => item.el === el);
-      if (index >= 0) {
-        list[index].clear();
-        list.splice(index, 1);
-      }
-    }
-  },
-});

+ 6 - 3
src/options/utils/index.js

@@ -1,11 +1,9 @@
 import Vue from 'vue';
-import resetFeatures from './features';
 import Message from '../views/message';
 
 export const store = {
   messages: null,
 };
-export const features = { reset: resetFeatures };
 
 function initMessage() {
   if (store.messages) return;
@@ -17,9 +15,14 @@ function initMessage() {
   }).$mount(el);
 }
 
+let id = 0;
+
 export function showMessage(options) {
   initMessage();
-  const message = Object.assign({}, options, !options.buttons && {
+  id += 1;
+  const message = Object.assign({
+    id,
+  }, options, !options.buttons && {
     onInit(vm) {
       setTimeout(() => {
         vm.$emit('dismiss');

+ 9 - 6
src/options/views/app.vue

@@ -6,9 +6,9 @@
       <hr>
       <div class="sidemenu">
         <a href="#?t=Installed" :class="{active: tab === 'Installed'}" v-text="i18n('sideMenuInstalled')"></a>
-        <a href="#?t=Settings" :class="{active: tab === 'Settings'}" v-feature="'settings'">
-          <span v-text="i18n('sideMenuSettings')" class="feature-text"></span>
-        </a>
+        <feature name="settings" tag="a" href="#?t=Settings" :class="{active: tab === 'Settings'}">
+          <span class="feature-text" v-text="i18n('sideMenuSettings')"></span>
+        </feature>
         <a href="#?t=About" :class="{active: tab === 'About'}" v-text="i18n('sideMenuAbout')"></a>
       </div>
     </aside>
@@ -21,22 +21,25 @@ import { store } from '../utils';
 import Installed from './tab-installed';
 import Settings from './tab-settings';
 import About from './tab-about';
+import Feature from './feature';
 
-const components = {
+const tabs = {
   Installed,
   Settings,
   About,
 };
 
 export default {
-  components,
+  components: Object.assign({
+    Feature,
+  }, tabs),
   data() {
     return store;
   },
   computed: {
     tab() {
       let tab = this.route.query.t;
-      if (!components[tab]) tab = 'Installed';
+      if (!tabs[tab]) tab = 'Installed';
       return tab;
     },
   },

+ 82 - 0
src/options/views/feature.vue

@@ -0,0 +1,82 @@
+<template>
+  <component :is="tag" :class="{ feature: featured }" @click="onClick">
+    <slot></slot>
+  </component>
+</template>
+
+<script>
+import Vue from 'vue';
+import options from 'src/common/options';
+import { object } from 'src/common';
+import { store } from '../utils';
+
+const FEATURES_KEY = 'features';
+store.features = options.get(FEATURES_KEY);
+options.hook(data => {
+  const features = data[FEATURES_KEY];
+  if (features) {
+    Vue.set(store, 'features', features);
+  }
+});
+options.ready(() => reset('sync'));
+window.store = store;
+
+function reset(version) {
+  if (object.get(store, 'features.version') !== version) {
+    options.set(FEATURES_KEY, {
+      version,
+      data: {},
+    });
+  }
+}
+
+export default {
+  props: {
+    name: {
+      type: String,
+      required: true,
+    },
+    tag: {
+      type: String,
+      default: 'span',
+    },
+  },
+  data() {
+    return { store };
+  },
+  computed: {
+    featured() {
+      return this.store.features && !object.get(this.store, ['features', 'data', this.name]);
+    },
+  },
+  methods: {
+    onClick() {
+      const { features } = this.store;
+      if (object.get(features, 'version')) {
+        features.data[this.name] = 1;
+        options.set(FEATURES_KEY, features);
+      }
+    },
+  },
+};
+</script>
+
+<style>
+.feature {
+  .feature-text {
+    position: relative;
+    &::after {
+      content: '';
+      display: block;
+      position: absolute;
+      width: 6px;
+      height: 6px;
+      top: -.1rem;
+      left: 100%;
+      border-radius: 50%;
+      margin-left: .1rem;
+      background: red;
+    }
+  }
+}
+</style>

+ 1 - 1
src/options/views/message/index.vue

@@ -1,6 +1,6 @@
 <template>
   <transition-group tag="div" name="message">
-    <item v-for="message in store.messages" :key="message"
+    <item v-for="message in store.messages" :key="message.id"
     :message="message" @dismiss="onDismiss(message)" />
   </transition-group>
 </template>

+ 4 - 2
src/options/views/tab-settings/vm-blacklist.vue

@@ -1,5 +1,5 @@
 <template>
-  <section v-feature="'blacklist'">
+  <feature name="blacklist" tag="section">
     <h3>
       <span class="feature-text" v-text="i18n('labelBlacklist')"></span>
     </h3>
@@ -9,7 +9,7 @@
     </p>
     <setting-text name="blacklist" ref="blacklist" />
     <button v-text="i18n('buttonSaveBlacklist')" @click="onSave"></button>
-  </section>
+  </feature>
 </template>
 
 <script>
@@ -17,10 +17,12 @@ import { i18n, sendMessage } from 'src/common';
 import options from 'src/common/options';
 import { showMessage } from 'src/options/utils';
 import SettingText from 'src/common/ui/setting-text';
+import Feature from '../feature';
 
 export default {
   components: {
     SettingText,
+    Feature,
   },
   methods: {
     onSave() {

+ 4 - 2
src/options/views/tab-settings/vm-css.vue

@@ -1,12 +1,12 @@
 <template>
-  <section v-feature="'css'">
+  <feature name="css" tag="section">
     <h3>
       <span class="feature-text" v-text="i18n('labelCustomCSS')"></span>
     </h3>
     <p v-html="i18n('descCustomCSS')"></p>
     <setting-text name="customCSS" ref="css" />
     <button v-text="i18n('buttonSaveCustomCSS')" @click="onSave"></button>
-  </section>
+  </feature>
 </template>
 
 <script>
@@ -14,10 +14,12 @@ import { i18n } from 'src/common';
 import options from 'src/common/options';
 import { showMessage } from 'src/options/utils';
 import SettingText from 'src/common/ui/setting-text';
+import Feature from '../feature';
 
 export default {
   components: {
     SettingText,
+    Feature,
   },
   methods: {
     onSave() {

+ 4 - 2
src/options/views/tab-settings/vm-sync.vue

@@ -1,5 +1,5 @@
 <template>
-  <section v-feature="'sync'">
+  <feature name="sync" tag="section">
     <h3>
       <span class="feature-text" v-text="i18n('labelSync')"></span>
     </h3>
@@ -21,7 +21,7 @@
         <span v-text="i18n('labelSyncScriptStatus')"></span>
       </label>
     </div>
-  </section>
+  </feature>
 </template>
 
 <script>
@@ -30,6 +30,7 @@ import options from 'src/common/options';
 import SettingCheck from 'src/common/ui/setting-check';
 import Icon from 'src/common/ui/icon';
 import { store } from '../../utils';
+import Feature from '../feature';
 
 const SYNC_CURRENT = 'sync.current';
 const syncConfig = {
@@ -45,6 +46,7 @@ export default {
   components: {
     SettingCheck,
     Icon,
+    Feature,
   },
   data() {
     return {

+ 1 - 0
test/index.js

@@ -1,2 +1,3 @@
+import './polyfill';
 import './background/tester';
 import './background/script';

+ 13 - 0
test/polyfill.js

@@ -0,0 +1,13 @@
+global.localStorage = {};
+global.browser = {
+  storage: {
+    local: {
+      get() {
+        return Promise.resolve({});
+      },
+      set() {
+        return Promise.resolve();
+      },
+    },
+  },
+};