Просмотр исходного кода

feat: add script sort filters

close #222
Gerald 8 лет назад
Родитель
Сommit
50e1db00d5

+ 1 - 1
package.json

@@ -10,7 +10,7 @@
     "analyze:json": "webpack --profile --json --config scripts/webpack.conf.js > stats.json",
     "i18n": "gulp i18n",
     "lint": "eslint --ext .js,.vue .",
-    "svgo": "svgo --config .svgo.yml icons",
+    "svgo": "svgo --config .svgo.yml src/resources/icons",
     "pretest": "cross-env NODE_ENV=test webpack --config scripts/webpack.test.conf.js",
     "test": "node dist/test",
     "prepush": "npm run lint"

+ 1 - 1
scripts/i18n.js

@@ -140,7 +140,7 @@ class Locales {
 function extract(options) {
   const keys = new Set();
   const patterns = {
-    default: ['\\bi18n\\(\'(\\w+)\'', 1],
+    default: ['\\b(?:i18n\\(\'|i18n-key=")(\\w+)[\'"]', 1],
     json: ['__MSG_(\\w+)__', 1],
   };
   const types = {

+ 15 - 0
src/_locales/cs/messages.yml

@@ -418,3 +418,18 @@ buttonUndo:
 labelRemoved:
   description: Label shown when a script is removed.
   message: ''
+buttonFilter:
+  description: Button to show filters menu.
+  message: ''
+optionShowEnabledFirst:
+  description: Option to show enabled scripts first in alphabetical order.
+  message: ''
+filterExecutionOrder:
+  description: Label for option to sort scripts in execution order.
+  message: ''
+filterAlphabeticalOrder:
+  description: Label for option to sort scripts in alphabetical order.
+  message: ''
+labelFilterSort:
+  description: Label for sort filter.
+  message: ''

+ 15 - 0
src/_locales/de/messages.yml

@@ -420,3 +420,18 @@ buttonUndo:
 labelRemoved:
   description: Label shown when a script is removed.
   message: '[Entfernt]'
+buttonFilter:
+  description: Button to show filters menu.
+  message: ''
+optionShowEnabledFirst:
+  description: Option to show enabled scripts first in alphabetical order.
+  message: ''
+filterExecutionOrder:
+  description: Label for option to sort scripts in execution order.
+  message: ''
+filterAlphabeticalOrder:
+  description: Label for option to sort scripts in alphabetical order.
+  message: ''
+labelFilterSort:
+  description: Label for sort filter.
+  message: ''

+ 15 - 0
src/_locales/en/messages.yml

@@ -418,3 +418,18 @@ buttonUndo:
 labelRemoved:
   description: Label shown when a script is removed.
   message: '[Removed]'
+buttonFilter:
+  description: Button to show filters menu.
+  message: Filters
+optionShowEnabledFirst:
+  description: Option to show enabled scripts first in alphabetical order.
+  message: Show enabled scripts first
+filterExecutionOrder:
+  description: Label for option to sort scripts in execution order.
+  message: execution order
+filterAlphabeticalOrder:
+  description: Label for option to sort scripts in alphabetical order.
+  message: alphabetical order
+labelFilterSort:
+  description: Label for sort filter.
+  message: Sort by $1

+ 15 - 0
src/_locales/es/messages.yml

@@ -422,3 +422,18 @@ buttonUndo:
 labelRemoved:
   description: Label shown when a script is removed.
   message: ''
+buttonFilter:
+  description: Button to show filters menu.
+  message: ''
+optionShowEnabledFirst:
+  description: Option to show enabled scripts first in alphabetical order.
+  message: ''
+filterExecutionOrder:
+  description: Label for option to sort scripts in execution order.
+  message: ''
+filterAlphabeticalOrder:
+  description: Label for option to sort scripts in alphabetical order.
+  message: ''
+labelFilterSort:
+  description: Label for sort filter.
+  message: ''

+ 15 - 0
src/_locales/fr/messages.yml

@@ -421,3 +421,18 @@ buttonUndo:
 labelRemoved:
   description: Label shown when a script is removed.
   message: '[Supprimé]'
+buttonFilter:
+  description: Button to show filters menu.
+  message: ''
+optionShowEnabledFirst:
+  description: Option to show enabled scripts first in alphabetical order.
+  message: ''
+filterExecutionOrder:
+  description: Label for option to sort scripts in execution order.
+  message: ''
+filterAlphabeticalOrder:
+  description: Label for option to sort scripts in alphabetical order.
+  message: ''
+labelFilterSort:
+  description: Label for sort filter.
+  message: ''

+ 15 - 0
src/_locales/hr/messages.yml

@@ -418,3 +418,18 @@ buttonUndo:
 labelRemoved:
   description: Label shown when a script is removed.
   message: ''
+buttonFilter:
+  description: Button to show filters menu.
+  message: ''
+optionShowEnabledFirst:
+  description: Option to show enabled scripts first in alphabetical order.
+  message: ''
+filterExecutionOrder:
+  description: Label for option to sort scripts in execution order.
+  message: ''
+filterAlphabeticalOrder:
+  description: Label for option to sort scripts in alphabetical order.
+  message: ''
+labelFilterSort:
+  description: Label for sort filter.
+  message: ''

+ 15 - 0
src/_locales/id/messages.yml

@@ -420,3 +420,18 @@ buttonUndo:
 labelRemoved:
   description: Label shown when a script is removed.
   message: ''
+buttonFilter:
+  description: Button to show filters menu.
+  message: ''
+optionShowEnabledFirst:
+  description: Option to show enabled scripts first in alphabetical order.
+  message: ''
+filterExecutionOrder:
+  description: Label for option to sort scripts in execution order.
+  message: ''
+filterAlphabeticalOrder:
+  description: Label for option to sort scripts in alphabetical order.
+  message: ''
+labelFilterSort:
+  description: Label for sort filter.
+  message: ''

+ 15 - 0
src/_locales/ja/messages.yml

@@ -418,3 +418,18 @@ buttonUndo:
 labelRemoved:
   description: Label shown when a script is removed.
   message: ''
+buttonFilter:
+  description: Button to show filters menu.
+  message: ''
+optionShowEnabledFirst:
+  description: Option to show enabled scripts first in alphabetical order.
+  message: ''
+filterExecutionOrder:
+  description: Label for option to sort scripts in execution order.
+  message: ''
+filterAlphabeticalOrder:
+  description: Label for option to sort scripts in alphabetical order.
+  message: ''
+labelFilterSort:
+  description: Label for sort filter.
+  message: ''

+ 15 - 0
src/_locales/pl/messages.yml

@@ -423,3 +423,18 @@ buttonUndo:
 labelRemoved:
   description: Label shown when a script is removed.
   message: '[Usunięty]'
+buttonFilter:
+  description: Button to show filters menu.
+  message: ''
+optionShowEnabledFirst:
+  description: Option to show enabled scripts first in alphabetical order.
+  message: ''
+filterExecutionOrder:
+  description: Label for option to sort scripts in execution order.
+  message: ''
+filterAlphabeticalOrder:
+  description: Label for option to sort scripts in alphabetical order.
+  message: ''
+labelFilterSort:
+  description: Label for sort filter.
+  message: ''

+ 15 - 0
src/_locales/pt_PT/messages.yml

@@ -422,3 +422,18 @@ buttonUndo:
 labelRemoved:
   description: Label shown when a script is removed.
   message: '[Removido]'
+buttonFilter:
+  description: Button to show filters menu.
+  message: ''
+optionShowEnabledFirst:
+  description: Option to show enabled scripts first in alphabetical order.
+  message: ''
+filterExecutionOrder:
+  description: Label for option to sort scripts in execution order.
+  message: ''
+filterAlphabeticalOrder:
+  description: Label for option to sort scripts in alphabetical order.
+  message: ''
+labelFilterSort:
+  description: Label for sort filter.
+  message: ''

+ 15 - 0
src/_locales/ro/messages.yml

@@ -418,3 +418,18 @@ buttonUndo:
 labelRemoved:
   description: Label shown when a script is removed.
   message: ''
+buttonFilter:
+  description: Button to show filters menu.
+  message: ''
+optionShowEnabledFirst:
+  description: Option to show enabled scripts first in alphabetical order.
+  message: ''
+filterExecutionOrder:
+  description: Label for option to sort scripts in execution order.
+  message: ''
+filterAlphabeticalOrder:
+  description: Label for option to sort scripts in alphabetical order.
+  message: ''
+labelFilterSort:
+  description: Label for sort filter.
+  message: ''

+ 15 - 0
src/_locales/ru/messages.yml

@@ -425,3 +425,18 @@ buttonUndo:
 labelRemoved:
   description: Label shown when a script is removed.
   message: ''
+buttonFilter:
+  description: Button to show filters menu.
+  message: ''
+optionShowEnabledFirst:
+  description: Option to show enabled scripts first in alphabetical order.
+  message: ''
+filterExecutionOrder:
+  description: Label for option to sort scripts in execution order.
+  message: ''
+filterAlphabeticalOrder:
+  description: Label for option to sort scripts in alphabetical order.
+  message: ''
+labelFilterSort:
+  description: Label for sort filter.
+  message: ''

+ 15 - 0
src/_locales/sr/messages.yml

@@ -418,3 +418,18 @@ buttonUndo:
 labelRemoved:
   description: Label shown when a script is removed.
   message: ''
+buttonFilter:
+  description: Button to show filters menu.
+  message: ''
+optionShowEnabledFirst:
+  description: Option to show enabled scripts first in alphabetical order.
+  message: ''
+filterExecutionOrder:
+  description: Label for option to sort scripts in execution order.
+  message: ''
+filterAlphabeticalOrder:
+  description: Label for option to sort scripts in alphabetical order.
+  message: ''
+labelFilterSort:
+  description: Label for sort filter.
+  message: ''

+ 23 - 4
src/_locales/tr/messages.yml

@@ -266,7 +266,9 @@ buttonReplaceAll:
   message: Tümü
 labelAutoReloadCurrentTab:
   description: Option to reload current tab after a script is switched on or off from menu.
-  message: Bir script dosyasını menüden açıp / kapattıktan sonra geçerli sekmeyi yeniden yükle
+  message: >-
+    Bir script dosyasını menüden açıp / kapattıktan sonra geçerli sekmeyi
+    yeniden yükle
 hintInputURL:
   description: Hint for a prompt box to input URL of a user script.
   message: 'URL Girdisi:'
@@ -298,7 +300,9 @@ msgNamespaceConflict:
   description: >-
     Message shown when namespace of the new script conflicts with an existent
     one.
-  message: Script isiminde anlaşmazlık! Lütfen @name ve @namespace alanlarını düzenleyin.
+  message: >-
+    Script isiminde anlaşmazlık! Lütfen @name ve @namespace alanlarını
+    düzenleyin.
 msgInvalidScript:
   description: Message shown when script is invalid.
   message: Geçersiz Script!
@@ -350,8 +354,8 @@ labelCustomCSS:
 descCustomCSS:
   description: Description of custom CSS section.
   message: >-
-   Seçenekler sayfası ve script yükleme sayfası için geçerli CSS. 
-   Eğer ne yaptığınızdan emin değilseniz, lütfen düzenlemeyiniz.
+    Seçenekler sayfası ve script yükleme sayfası için geçerli CSS.  Eğer ne
+    yaptığınızdan emin değilseniz, lütfen düzenlemeyiniz.
 buttonSaveCustomCSS:
   description: Label for button to save custom CSS.
   message: Kaydet
@@ -418,3 +422,18 @@ buttonUndo:
 labelRemoved:
   description: Label shown when a script is removed.
   message: '[Silindi]'
+buttonFilter:
+  description: Button to show filters menu.
+  message: ''
+optionShowEnabledFirst:
+  description: Option to show enabled scripts first in alphabetical order.
+  message: ''
+filterExecutionOrder:
+  description: Label for option to sort scripts in execution order.
+  message: ''
+filterAlphabeticalOrder:
+  description: Label for option to sort scripts in alphabetical order.
+  message: ''
+labelFilterSort:
+  description: Label for sort filter.
+  message: ''

+ 15 - 0
src/_locales/vi/messages.yml

@@ -418,3 +418,18 @@ buttonUndo:
 labelRemoved:
   description: Label shown when a script is removed.
   message: '[Đã xóa]'
+buttonFilter:
+  description: Button to show filters menu.
+  message: ''
+optionShowEnabledFirst:
+  description: Option to show enabled scripts first in alphabetical order.
+  message: ''
+filterExecutionOrder:
+  description: Label for option to sort scripts in execution order.
+  message: ''
+filterAlphabeticalOrder:
+  description: Label for option to sort scripts in alphabetical order.
+  message: ''
+labelFilterSort:
+  description: Label for sort filter.
+  message: ''

+ 15 - 0
src/_locales/zh_CN/messages.yml

@@ -416,3 +416,18 @@ buttonUndo:
 labelRemoved:
   description: Label shown when a script is removed.
   message: 【已删除】
+buttonFilter:
+  description: Button to show filters menu.
+  message: 筛选
+optionShowEnabledFirst:
+  description: Option to show enabled scripts first in alphabetical order.
+  message: 先显示被启用的脚本
+filterExecutionOrder:
+  description: Label for option to sort scripts in execution order.
+  message: 执行顺序
+filterAlphabeticalOrder:
+  description: Label for option to sort scripts in alphabetical order.
+  message: 字母顺序
+labelFilterSort:
+  description: Label for sort filter.
+  message: 按 $1 排序

+ 15 - 0
src/_locales/zh_TW/messages.yml

@@ -416,3 +416,18 @@ buttonUndo:
 labelRemoved:
   description: Label shown when a script is removed.
   message: 【已刪除】
+buttonFilter:
+  description: Button to show filters menu.
+  message: ''
+optionShowEnabledFirst:
+  description: Option to show enabled scripts first in alphabetical order.
+  message: ''
+filterExecutionOrder:
+  description: Label for option to sort scripts in execution order.
+  message: ''
+filterAlphabeticalOrder:
+  description: Label for option to sort scripts in alphabetical order.
+  message: ''
+labelFilterSort:
+  description: Label for sort filter.
+  message: ''

+ 3 - 0
src/background/utils/options.js

@@ -20,6 +20,9 @@ const defaults = {
   importSettings: true,
   notifyUpdates: false,
   version: null,
+  filters: {
+    sort: 'exec',
+  },
 };
 let changes = {};
 const hooks = initHooks();

+ 3 - 3
src/common/hook-setting.js

@@ -9,15 +9,15 @@ options.hook(data => {
   });
 });
 
-export default function hook(key, item) {
+export default function hook(key, update) {
   let list = hooks[key];
   if (!list) {
     list = [];
     hooks[key] = list;
   }
-  list.push(item);
+  list.push(update);
   return () => {
-    const i = list.indexOf(item);
+    const i = list.indexOf(update);
     if (i >= 0) list.splice(i, 1);
   };
 }

+ 22 - 0
src/common/ui/locale-group.vue

@@ -0,0 +1,22 @@
+<template>
+  <span>
+    {{parts[0]}}
+    <slot></slot>
+    {{parts[1]}}
+  </span>
+</template>
+
+<script>
+import { i18n } from 'src/common';
+
+const SEP = '¥¥';
+
+export default {
+  props: ['i18nKey'],
+  computed: {
+    parts() {
+      return i18n(this.i18nKey, [SEP]).split(SEP);
+    },
+  },
+};
+</script>

+ 6 - 0
src/common/ui/style/style.css

@@ -32,6 +32,7 @@ a {
   color: dodgerblue;
 }
 hr {
+  margin: .5rem;
   border: none;
   border-top: 1px solid darkgray;
 }
@@ -196,3 +197,8 @@ button {
   border: 1px solid #bbb;
   background: white;
 }
+
+.modal-content {
+  padding: 1rem;
+  background: white;
+}

+ 10 - 6
src/options/app.js

@@ -37,16 +37,20 @@ function initialize() {
   });
 }
 
-function initSearch(script) {
+function initScript(script) {
   const meta = script.meta || {};
-  script._search = [
+  const localeName = getLocaleString(meta, 'name');
+  const search = [
     meta.name,
-    getLocaleString(meta, 'name'),
+    localeName,
     meta.description,
     getLocaleString(meta, 'description'),
     script.custom.name,
     script.custom.description,
   ].filter(Boolean).join('\n').toLowerCase();
+  const name = script.custom.name || localeName;
+  const lowerName = name.toLowerCase();
+  script._cache = { search, name, lowerName };
 }
 
 function loadData() {
@@ -60,7 +64,7 @@ function loadData() {
       Vue.set(store, key, data[key]);
     });
     if (store.scripts) {
-      store.scripts.forEach(initSearch);
+      store.scripts.forEach(initScript);
     }
     store.loading = false;
   });
@@ -76,7 +80,7 @@ function initMain() {
     },
     AddScript({ update }) {
       update.message = '';
-      initSearch(update);
+      initScript(update);
       store.scripts.push(update);
     },
     UpdateScript(data) {
@@ -85,7 +89,7 @@ function initMain() {
       if (index >= 0) {
         const updated = Object.assign({}, store.scripts[index], data.update);
         Vue.set(store.scripts, index, updated);
-        initSearch(updated);
+        initScript(updated);
       }
     },
   });

+ 14 - 11
src/options/style.css

@@ -11,17 +11,20 @@ aside {
     width: 5rem;
   }
 }
-.sidemenu > a {
-  display: block;
-  padding-top: 0.6rem;
-  padding-bottom: 0.6rem;
-  font-size: 1rem;
-  font-weight: bold;
-  text-decoration: none;
-  color: gray;
-  &.active,
-  &:hover {
-    color: black;
+.sidemenu {
+  border-top: 1px solid #bbb;
+  > a {
+    display: block;
+    padding-top: .6rem;
+    padding-bottom: .6rem;
+    font-size: 1rem;
+    font-weight: bold;
+    text-decoration: none;
+    color: gray;
+    &.active,
+    &:hover {
+      color: black;
+    }
   }
 }
 #currentLang {

+ 0 - 1
src/options/views/app.vue

@@ -3,7 +3,6 @@
     <aside>
       <img src="/public/images/icon128.png">
       <h1 v-text="i18n('extName')"></h1>
-      <hr>
       <div class="sidemenu">
         <a href="#?t=Installed" :class="{active: tab === 'Installed'}" v-text="i18n('sideMenuInstalled')"></a>
         <feature name="settings" tag="a" href="#?t=Settings" :class="{active: tab === 'Settings'}">

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

@@ -1,5 +1,5 @@
 <template>
-  <div class="message">
+  <div class="message modal-content">
     <div class="mb-1" v-if="message.text" v-text="message.text"></div>
     <form v-if="message.buttons" @submit.prevent>
       <input class="mb-1" type="text" v-if="message.input !== false" v-model="message.input">
@@ -42,8 +42,6 @@ export default {
 <style>
 .message {
   width: 18rem;
-  padding: 1rem;
-  background: white;
   border-bottom-left-radius: .2rem;
   border-bottom-right-radius: .2rem;
   box-shadow: 0 0 .2rem rgba(0,0,0,.2);

+ 3 - 3
src/options/views/script-item.vue

@@ -1,8 +1,8 @@
 <template>
-  <div class="script" :class="{ disabled: !script.config.enabled, removed: script.config.removed }" :draggable="!script.config.removed" @dragstart.prevent="onDragStart">
+  <div class="script" :class="{ disabled: !script.config.enabled, removed: script.config.removed }" :draggable="draggable" @dragstart.prevent="onDragStart">
     <img class="script-icon" :src="safeIcon">
     <div class="script-info flex">
-      <div class="script-name ellipsis" v-text="script.custom.name || getLocaleString('name')"></div>
+      <div class="script-name ellipsis" v-text="script._cache.name"></div>
       <div class="flex-auto"></div>
       <div class="script-author ellipsis" :title="script.meta.author" v-if="author">
         <span v-text="i18n('labelAuthor')"></span>
@@ -87,7 +87,7 @@ function loadImage(url) {
 }
 
 export default {
-  props: ['script'],
+  props: ['script', 'draggable'],
   components: {
     Icon,
     Tooltip,

+ 123 - 27
src/options/views/tab-installed.vue

@@ -3,20 +3,46 @@
     <header class="flex">
       <div class="flex-auto">
         <vl-dropdown :closeAfterClick="true">
-          <span class="btn-ghost" slot="toggle">
-            <icon name="plus"></icon>
-          </span>
-          <a href="#" v-text="i18n('buttonNew')" @click.prevent="newScript"></a>
-          <a v-text="i18n('installFrom', 'OpenUserJS')" href="https://openuserjs.org/" target="_blank"></a>
-          <a v-text="i18n('installFrom', 'GreasyFork')" href="https://greasyfork.org/scripts" target="_blank"></a>
-          <a href="#" v-text="i18n('buttonInstallFromURL')" @click.prevent="installFromURL"></a>
+          <tooltip :title="i18n('buttonNew')" placement="down" align="start" slot="toggle">
+            <span class="btn-ghost">
+              <icon name="plus"></icon>
+            </span>
+          </tooltip>
+          <div class="dropdown-menu-item" v-text="i18n('buttonNew')" @click.prevent="newScript"></div>
+          <a class="dropdown-menu-item" v-text="i18n('installFrom', 'OpenUserJS')" href="https://openuserjs.org/" target="_blank"></a>
+          <a class="dropdown-menu-item" v-text="i18n('installFrom', 'GreasyFork')" href="https://greasyfork.org/scripts" target="_blank"></a>
+          <div class="dropdown-menu-item" v-text="i18n('buttonInstallFromURL')" @click.prevent="installFromURL"></div>
         </vl-dropdown>
-        <tooltip :title="i18n('buttonUpdateAll')" placement="down">
+        <tooltip :title="i18n('buttonUpdateAll')" placement="down" align="start">
           <span class="btn-ghost" @click="updateAll">
             <icon name="refresh"></icon>
           </span>
         </tooltip>
       </div>
+      <vl-dropdown align="right" class="filter-sort">
+        <tooltip :title="i18n('buttonFilter')" placement="down" slot="toggle">
+          <span class="btn-ghost">
+            <icon name="filter"></icon>
+          </span>
+        </tooltip>
+        <div>
+          <locale-group i18n-key="labelFilterSort">
+            <select :value="filters.sort.value" @change="onOrderChange">
+              <option
+                v-for="option in filterOptions.sort"
+                v-text="option.title"
+                :value="option.value">
+              </option>
+            </select>
+          </locale-group>
+        </div>
+        <div v-if="filters.sort.value === 'alpha'">
+          <label>
+            <setting-check name="filters.showEnabledFirst" @change="updateLater"></setting-check>
+            <span v-text="i18n('optionShowEnabledFirst')"></span>
+          </label>
+        </div>
+      </vl-dropdown>
       <div class="filter-search">
         <input type="text" :placeholder="i18n('labelSearchScript')" v-model="search">
         <icon name="search"></icon>
@@ -24,7 +50,8 @@
     </header>
     <div class="scripts">
       <item v-for="script in scripts" :key="script.props.id"
-      :script="script" @edit="editScript" @move="moveScript"></item>
+      :script="script" :draggable="filters.sort.value === 'exec' && !script.config.removed"
+      @edit="editScript" @move="moveScript"></item>
     </div>
     <div class="backdrop" :class="{mask: store.loading}" v-show="message">
       <div v-html="message"></div>
@@ -35,33 +62,72 @@
 
 <script>
 import VlDropdown from 'vueleton/lib/dropdown';
+import VlModal from 'vueleton/lib/modal';
 import { i18n, sendMessage, noop, debounce } from 'src/common';
+import options from 'src/common/options';
+import SettingCheck from 'src/common/ui/setting-check';
+import hookSetting from 'src/common/hook-setting';
 import Icon from 'src/common/ui/icon';
 import Tooltip from 'src/common/ui/tooltip';
+import LocaleGroup from 'src/common/ui/locale-group';
 import Item from './script-item';
 import Edit from './edit';
 import { store, showMessage } from '../utils';
 
+const filterOptions = {
+  sort: [
+    { value: 'exec', title: i18n('filterExecutionOrder') },
+    { value: 'alpha', title: i18n('filterAlphabeticalOrder') },
+  ],
+};
+const filters = {
+  sort: {
+    value: null,
+    title: null,
+    set(value) {
+      const option = filterOptions.sort.find(item => item.value === value);
+      const { sort } = filters;
+      if (!option) {
+        sort.set(filterOptions.sort[0].value);
+        return;
+      }
+      sort.value = option && option.value;
+      sort.title = option && option.title;
+    },
+  },
+};
+hookSetting('filters.sort', value => {
+  filters.sort.set(value);
+});
+options.ready(() => {
+  filters.sort.set(options.get('filters.sort'));
+});
+
 export default {
   components: {
     Item,
     Edit,
     Tooltip,
+    SettingCheck,
+    LocaleGroup,
     VlDropdown,
+    VlModal,
     Icon,
   },
   data() {
     return {
       store,
+      filterOptions,
+      filters,
       script: null,
       search: null,
+      modal: null,
       scripts: store.scripts,
     };
   },
   watch: {
-    search() {
-      this.debouncedUpdate();
-    },
+    search: 'updateLater',
+    'filters.sort.value': 'updateLater',
     'store.scripts': 'onUpdate',
   },
   computed: {
@@ -79,11 +145,29 @@ export default {
   },
   methods: {
     onUpdate() {
-      const { search } = this;
+      const { search, filters: { sort } } = this;
+      const lowerSearch = (search || '').toLowerCase();
       const { scripts } = this.store;
-      this.scripts = search
-        ? scripts.filter(script => (script._search || '').includes(search.toLowerCase()))
-        : scripts;
+      const filteredScripts = search
+        ? scripts.filter(script => script._cache.search.includes(lowerSearch))
+        : scripts.slice();
+      if (sort.value === 'alpha') {
+        const showEnabledFirst = options.get('filters.showEnabledFirst');
+        filteredScripts.sort((a, b) => {
+          if (showEnabledFirst && a.config.enabled !== b.config.enabled) {
+            return a.config.enabled ? -1 : 1;
+          }
+          const { _cache: { lowerName: nameA } } = a;
+          const { _cache: { lowerName: nameB } } = b;
+          if (nameA < nameB) return -1;
+          if (nameA > nameB) return 1;
+          return 0;
+        });
+      }
+      this.scripts = filteredScripts;
+    },
+    updateLater() {
+      this.debouncedUpdate();
     },
     newScript() {
       this.script = {};
@@ -148,6 +232,9 @@ export default {
         this.store.scripts = seq.concat.apply([], seq);
       });
     },
+    onOrderChange(e) {
+      options.set('filters.sort', e.target.value);
+    },
   },
   created() {
     this.debouncedUpdate = debounce(this.onUpdate, 200);
@@ -169,17 +256,6 @@ $header-height: 4rem;
   }
   .vl-dropdown-menu {
     white-space: nowrap;
-    > a {
-      display: block;
-      width: 100%;
-      padding: .5rem;
-      text-decoration: none;
-      color: #666;
-      &:hover {
-        color: inherit;
-        background: #fbfbfb;
-      }
-    }
   }
 }
 .backdrop,
@@ -212,6 +288,18 @@ $header-height: 4rem;
   background: rgba(0,0,0,.08);
   /*transition: opacity 1s;*/
 }
+.dropdown-menu-item {
+  display: block;
+  width: 100%;
+  padding: .5rem;
+  text-decoration: none;
+  color: #666;
+  cursor: pointer;
+  &:hover {
+    color: inherit;
+    background: #fbfbfb;
+  }
+}
 .filter-search {
   position: relative;
   width: 12rem;
@@ -227,4 +315,12 @@ $header-height: 4rem;
     line-height: 2;
   }
 }
+.filter-sort {
+  .vl-dropdown-menu {
+    padding: 1rem;
+    > * {
+      margin-bottom: .5rem;
+    }
+  }
+}
 </style>

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

@@ -28,6 +28,7 @@
 import { sendMessage } from 'src/common';
 import options from 'src/common/options';
 import SettingCheck from 'src/common/ui/setting-check';
+import hookSetting from 'src/common/hook-setting';
 import Icon from 'src/common/ui/icon';
 import { store } from '../../utils';
 import Feature from '../feature';
@@ -36,10 +37,8 @@ const SYNC_CURRENT = 'sync.current';
 const syncConfig = {
   current: '',
 };
-options.hook(data => {
-  if (SYNC_CURRENT in data) {
-    syncConfig.current = data[SYNC_CURRENT] || '';
-  }
+hookSetting(SYNC_CURRENT, value => {
+  syncConfig.current = value || '';
 });
 
 export default {

+ 1 - 0
src/resources/icons/filter.svg

@@ -0,0 +1 @@
+<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M66.857 0h890.286c53.257 0 83.215 61.252 50.52 103.292L640 576v420.386c0 12.687-13.353 20.939-24.7 15.264L401.687 904.845a32 32 0 0 1-17.69-28.621V576L16.34 103.292C-16.358 61.252 13.6 0 66.857 0z"/></svg>