|
|
@@ -2,7 +2,7 @@
|
|
|
<div class="tab-installed" ref="scroller">
|
|
|
<div v-if="store.canRenderScripts">
|
|
|
<header class="flex">
|
|
|
- <div class="flex flex-auto" v-if="!showRecycle">
|
|
|
+ <div class="flex" v-if="!showRecycle">
|
|
|
<Dropdown
|
|
|
:closeAfterClick="true"
|
|
|
:class="{active: state.menuNewActive}"
|
|
|
@@ -35,7 +35,8 @@
|
|
|
</a>
|
|
|
</Tooltip>
|
|
|
</div>
|
|
|
- <div class="flex-auto" v-else v-text="i18n('headerRecycleBin')" />
|
|
|
+ <div v-else v-text="i18n('headerRecycleBin')" />
|
|
|
+ <div class="flex-auto"></div>
|
|
|
<LocaleGroup i18n-key="labelFilterSort">
|
|
|
<select :value="filters.sort" @change="handleOrderChange" class="h-100">
|
|
|
<option
|
|
|
@@ -67,31 +68,29 @@
|
|
|
</template>
|
|
|
</Dropdown>
|
|
|
<!-- form and id are required for the built-in autocomplete using entered values -->
|
|
|
- <form class="filter-search hidden-xs flex" @submit.prevent>
|
|
|
- <Tooltip placement="bottom">
|
|
|
- <label>
|
|
|
- <input
|
|
|
- type="search"
|
|
|
- :class="{'has-error': state.searchError}"
|
|
|
- :placeholder="i18n('labelSearchScript')"
|
|
|
- v-model="state.search"
|
|
|
- ref="refSearch"
|
|
|
- id="installed-search">
|
|
|
- <Icon name="search" />
|
|
|
- </label>
|
|
|
- <template #content>
|
|
|
- <pre
|
|
|
- class="filter-search-tooltip"
|
|
|
- v-text="state.searchError || i18n('titleSearchHint')"
|
|
|
- />
|
|
|
- </template>
|
|
|
- </Tooltip>
|
|
|
- <select v-model="filters.searchScope" @change="handleOnScopeChange">
|
|
|
- <option value="name" v-text="i18n('filterScopeName')"/>
|
|
|
- <option value="code" v-text="i18n('filterScopeCode')"/>
|
|
|
- <option value="all" v-text="i18n('filterScopeAll')"/>
|
|
|
- </select>
|
|
|
+ <form class="filter-search hidden-xs" @submit.prevent>
|
|
|
+ <label>
|
|
|
+ <input
|
|
|
+ type="search"
|
|
|
+ :class="{'has-error': state.search.error}"
|
|
|
+ :placeholder="i18n('labelSearchScript')"
|
|
|
+ v-model="state.search.value"
|
|
|
+ ref="refSearch"
|
|
|
+ id="installed-search">
|
|
|
+ <Icon name="search" />
|
|
|
+ </label>
|
|
|
</form>
|
|
|
+ <Dropdown align="right">
|
|
|
+ <a class="btn-ghost" tabindex="0" :class="{'has-error': state.search.error}">
|
|
|
+ <Icon name="question"></Icon>
|
|
|
+ </a>
|
|
|
+ <template #content>
|
|
|
+ <div class="filter-search-tooltip">
|
|
|
+ <div class="has-error" v-if="state.search.error" v-text="state.search.error" />
|
|
|
+ <div v-html="i18n('titleSearchHintV2')" />
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </Dropdown>
|
|
|
</header>
|
|
|
<div v-if="showRecycle" class="hint mx-1 my-1 flex flex-col">
|
|
|
<span v-text="i18n('hintRecycleBin')"/>
|
|
|
@@ -106,9 +105,9 @@
|
|
|
:data-columns="state.numColumns"
|
|
|
:data-show-order="filters.showOrder || null"
|
|
|
:data-table="filters.viewTable || null">
|
|
|
- <script-item
|
|
|
+ <ScriptItem
|
|
|
v-for="(script, index) in state.sortedScripts"
|
|
|
- v-show="!state.search || script.$cache.show !== false"
|
|
|
+ v-show="!state.search.rules.length || script.$cache.show !== false"
|
|
|
:key="script.props.id"
|
|
|
:focused="selectedScript === script"
|
|
|
:showHotkeys="state.showHotkeys"
|
|
|
@@ -117,11 +116,13 @@
|
|
|
:visible="index < state.batchRender.limit"
|
|
|
:viewTable="filters.viewTable"
|
|
|
:hotkeys="scriptHotkeys"
|
|
|
+ :activeTags="activeTags"
|
|
|
@remove="handleActionRemove"
|
|
|
@restore="handleActionRestore"
|
|
|
@toggle="handleActionToggle"
|
|
|
@update="handleActionUpdate"
|
|
|
@scrollDelta="handleSmoothScroll"
|
|
|
+ @clickTag="handleClickTag"
|
|
|
/>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -140,7 +141,7 @@
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
-<script>
|
|
|
+<script setup>
|
|
|
import { computed, reactive, nextTick, onMounted, watch, ref } from 'vue';
|
|
|
import { i18n, sendCmdDirectly, debounce, makePause, trueJoin } from '@/common';
|
|
|
import options from '@/common/options';
|
|
|
@@ -156,7 +157,7 @@ import SettingCheck from '@/common/ui/setting-check';
|
|
|
import Icon from '@/common/ui/icon';
|
|
|
import LocaleGroup from '@/common/ui/locale-group';
|
|
|
import { customCssElem, findStyleSheetRules } from '@/common/ui/style';
|
|
|
-import { markRemove, store } from '../utils';
|
|
|
+import { markRemove, store, createSearchRules, testSearchRule } from '../utils';
|
|
|
import toggleDragging from '../utils/dragging';
|
|
|
import ScriptItem from './script-item';
|
|
|
import Edit from './edit';
|
|
|
@@ -240,8 +241,11 @@ const state = reactive({
|
|
|
focusedIndex: -1,
|
|
|
menuNewActive: false,
|
|
|
showHotkeys: false,
|
|
|
- search: '',
|
|
|
- searchError: null,
|
|
|
+ search: {
|
|
|
+ value: '',
|
|
|
+ error: null,
|
|
|
+ ...createSearchRules(''),
|
|
|
+ },
|
|
|
sortedScripts: [],
|
|
|
filteredScripts: [],
|
|
|
script: null,
|
|
|
@@ -261,17 +265,17 @@ const message = computed(() => {
|
|
|
if (store.loading) {
|
|
|
return null;
|
|
|
}
|
|
|
- if (state.search ? !state.sortedScripts.find(s => s.$cache.show !== false) : !state.sortedScripts.length) {
|
|
|
+ if (state.search.rules.length ? !state.sortedScripts.find(s => s.$cache.show !== false) : !state.sortedScripts.length) {
|
|
|
return i18n('labelNoSearchScripts');
|
|
|
}
|
|
|
return null;
|
|
|
});
|
|
|
-const searchNeedsCodeIds = computed(() => state.search
|
|
|
- && ['code', 'all'].includes(filters.searchScope)
|
|
|
+const searchNeedsCodeIds = computed(() => state.search.rules.some(rule => !rule.scope || rule.scope === 'code')
|
|
|
&& store.scripts.filter(s => s.$cache.code == null).map(s => s.props.id));
|
|
|
+const activeTags = computed(() => state.search.tokens.filter(token => token.prefix === '#' && !token.negative).map(token => token.parsed));
|
|
|
const getCurrentList = () => showRecycle.value ? store.removedScripts : store.scripts;
|
|
|
|
|
|
-const debouncedUpdate = debounce(onUpdate, 100);
|
|
|
+const debouncedSearch = debounce(scheduleSearch, 200);
|
|
|
const debouncedRender = debounce(renderScripts);
|
|
|
|
|
|
function resetList() {
|
|
|
@@ -291,11 +295,11 @@ async function refreshUI() {
|
|
|
}
|
|
|
function onUpdate() {
|
|
|
const scripts = [...getCurrentList()];
|
|
|
- const numFound = state.search ? performSearch(scripts) : scripts.length;
|
|
|
+ const numFound = state.search.rules.length ? performSearch(scripts) : scripts.length;
|
|
|
const cmp = currentSortCompare.value;
|
|
|
if (cmp) scripts.sort(combinedCompare(cmp));
|
|
|
state.sortedScripts = scripts;
|
|
|
- state.filteredScripts = state.search ? scripts.filter(({ $cache }) => $cache.show) : scripts;
|
|
|
+ state.filteredScripts = state.search.rules.length ? scripts.filter(({ $cache }) => $cache.show) : scripts;
|
|
|
selectScript(state.focusedIndex);
|
|
|
if (!step || numFound < step) renderScripts();
|
|
|
else debouncedRender();
|
|
|
@@ -338,10 +342,6 @@ async function moveScript(from, to) {
|
|
|
function handleOrderChange(e) {
|
|
|
options.set('filters.sort', e.target.value);
|
|
|
}
|
|
|
-function handleOnScopeChange(e) {
|
|
|
- if (state.search) scheduleSearch();
|
|
|
- options.set('filters.searchScope', e.target.value);
|
|
|
-}
|
|
|
function handleStateChange(active) {
|
|
|
state.menuNewActive = active;
|
|
|
}
|
|
|
@@ -388,7 +388,7 @@ async function renderScripts() {
|
|
|
const startTime = performance.now();
|
|
|
// If we entered a new loop of rendering, state.batchRender will no longer be batchRender
|
|
|
while (limit < length && batchRender === state.batchRender) {
|
|
|
- if (step && state.search) {
|
|
|
+ if (step && state.search.rules.length) {
|
|
|
// Only visible items contribute to the batch size
|
|
|
for (let vis = 0; vis < step && limit < length; limit += 1) {
|
|
|
vis += state.sortedScripts[limit].$cache.show ? 1 : 0;
|
|
|
@@ -404,40 +404,40 @@ async function renderScripts() {
|
|
|
if (step && limit < length) await makePause();
|
|
|
}
|
|
|
}
|
|
|
-function performSearch(scripts) {
|
|
|
- let searchRE;
|
|
|
+function performSearch() {
|
|
|
let count = 0;
|
|
|
- const [,
|
|
|
- expr = state.search.replace(/[.+^*$?|\\()[\]{}]/g, '\\$&'),
|
|
|
- flags = 'i',
|
|
|
-] = state.search.match(/^\/(.+?)\/(\w*)$|$/);
|
|
|
- const scope = filters.searchScope;
|
|
|
- const scopeName = scope === 'name' || scope === 'all';
|
|
|
- const scopeCode = scope === 'code' || scope === 'all';
|
|
|
+ store.scripts.forEach(({ $cache }) => {
|
|
|
+ const dataMap = {
|
|
|
+ name: $cache.lowerName,
|
|
|
+ code: $cache.code,
|
|
|
+ tags: $cache.tags,
|
|
|
+ '': $cache.search,
|
|
|
+ };
|
|
|
+ $cache.show = state.search.rules.every(rule => testSearchRule(rule, dataMap[rule.scope]));
|
|
|
+ count += $cache.show;
|
|
|
+ });
|
|
|
+ return count;
|
|
|
+}
|
|
|
+function scheduleSearch() {
|
|
|
try {
|
|
|
- searchRE = expr && new RegExp(expr, flags);
|
|
|
- scripts.forEach(({ $cache }) => {
|
|
|
- $cache.show = !expr
|
|
|
- || scopeName && searchRE.test($cache.search)
|
|
|
- || scopeCode && searchRE.test($cache.code);
|
|
|
- count += $cache.show;
|
|
|
- });
|
|
|
- state.searchError = null;
|
|
|
+ state.search = {
|
|
|
+ ...state.search,
|
|
|
+ ...createSearchRules(state.search.value),
|
|
|
+ };
|
|
|
+ state.search.error = null;
|
|
|
} catch (err) {
|
|
|
- state.searchError = err.message;
|
|
|
+ state.search.error = err.message;
|
|
|
}
|
|
|
- return count;
|
|
|
-}
|
|
|
-async function scheduleSearch() {
|
|
|
const ids = searchNeedsCodeIds.value;
|
|
|
- if (ids?.length) await getCodeFromStorage(ids);
|
|
|
- debouncedUpdate();
|
|
|
+ if (ids?.length) getCodeFromStorage(ids);
|
|
|
+ onUpdate();
|
|
|
}
|
|
|
async function getCodeFromStorage(ids) {
|
|
|
const data = await sendCmdDirectly('GetScriptCode', ids);
|
|
|
store.scripts.forEach(({ $cache, props: { id } }) => {
|
|
|
if (id in data) $cache.code = data[id];
|
|
|
});
|
|
|
+ onUpdate();
|
|
|
}
|
|
|
async function handleEmptyRecycleBin() {
|
|
|
if (await showConfirmation(i18n('buttonEmptyRecycleBin'))) {
|
|
|
@@ -501,6 +501,16 @@ function handleActionToggle(script) {
|
|
|
function handleActionUpdate(script) {
|
|
|
sendCmdDirectly('CheckUpdate', script.props.id);
|
|
|
}
|
|
|
+function handleClickTag(tag) {
|
|
|
+ if (activeTags.value.includes(tag)) {
|
|
|
+ // remove tag
|
|
|
+ const tokens = state.search.tokens.filter(token => !(token.prefix === '#' && token.parsed === tag));
|
|
|
+ state.search.value = tokens.map(token => `${token.prefix}${token.raw}`).join(' ');
|
|
|
+ } else {
|
|
|
+ // add tag
|
|
|
+ state.search.value = [state.search.value.trim(), `#${tag} `].filter(Boolean).join(' ');
|
|
|
+ }
|
|
|
+}
|
|
|
function handleSmoothScroll(delta) {
|
|
|
if (!delta) return;
|
|
|
const el = refList.value;
|
|
|
@@ -629,100 +639,51 @@ function bindKeys() {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
-export default {
|
|
|
- components: {
|
|
|
- Dropdown,
|
|
|
- Tooltip,
|
|
|
- SettingCheck,
|
|
|
- Icon,
|
|
|
- LocaleGroup,
|
|
|
- ScriptItem,
|
|
|
- Edit,
|
|
|
- },
|
|
|
- directives: {
|
|
|
- focus: vFocus,
|
|
|
- },
|
|
|
- setup() {
|
|
|
- resetList();
|
|
|
- watch(showRecycle, resetList);
|
|
|
- watch(() => store.canRenderScripts && refList.value && draggableRaw.value,
|
|
|
- dr => toggleDragging(refList.value, moveScript, dr));
|
|
|
- watch(() => state.search, scheduleSearch);
|
|
|
- watch(() => [filters.sort, filters.showEnabledFirst], debouncedUpdate);
|
|
|
- if (screen.availWidth > 767) {
|
|
|
- watch(() => filters.viewSingleColumn, adjustScriptWidth);
|
|
|
- watch(() => filters.viewTable, adjustNarrowWidth);
|
|
|
- }
|
|
|
- watch(getCurrentList, refreshUI);
|
|
|
- watch(() => store.route.paths[1], onHashChange);
|
|
|
- watch(selectedScript, script => {
|
|
|
- keyboardService.setContext('selectedScript', script);
|
|
|
- });
|
|
|
- watch(() => state.showHotkeys, value => {
|
|
|
- keyboardService.setContext('showHotkeys', value);
|
|
|
- });
|
|
|
+resetList();
|
|
|
+watch(showRecycle, resetList);
|
|
|
+watch(() => store.canRenderScripts && refList.value && draggableRaw.value,
|
|
|
+ dr => toggleDragging(refList.value, moveScript, dr));
|
|
|
+watch(() => state.search.value, debouncedSearch);
|
|
|
+watch(() => [filters.sort, filters.showEnabledFirst], debouncedSearch);
|
|
|
+if (screen.availWidth > 767) {
|
|
|
+ watch(() => filters.viewSingleColumn, adjustScriptWidth);
|
|
|
+ watch(() => filters.viewTable, adjustNarrowWidth);
|
|
|
+}
|
|
|
+watch(getCurrentList, refreshUI);
|
|
|
+watch(() => store.route.paths[1], onHashChange);
|
|
|
+watch(selectedScript, script => {
|
|
|
+ keyboardService.setContext('selectedScript', script);
|
|
|
+});
|
|
|
+watch(() => state.showHotkeys, value => {
|
|
|
+ keyboardService.setContext('showHotkeys', value);
|
|
|
+});
|
|
|
|
|
|
- onMounted(() => {
|
|
|
- // Ensure the correct UI is shown when mounted:
|
|
|
- // * on subsequent navigation via history back/forward;
|
|
|
- // * on first initialization in some weird case the scripts got loaded early.
|
|
|
- if (!store.loading) refreshUI();
|
|
|
- // Extract --columns-cards and --columns-table from `:root` or `html` selector. CustomCSS may override it.
|
|
|
- if (!columnsForCardsMode.length) {
|
|
|
- const style = customCssElem?.textContent.match(/--columns-(cards|table)\b/)
|
|
|
- && getComputedStyle(document.documentElement);
|
|
|
- if (style) {
|
|
|
- for (const [type, arr] of [
|
|
|
- ['cards', columnsForCardsMode],
|
|
|
- ['table', columnsForTableMode],
|
|
|
- ]) {
|
|
|
- const val = style.getPropertyValue(`--columns-${type}`);
|
|
|
- if (val) arr.push(...val.split(',').map(Number).filter(Boolean));
|
|
|
- }
|
|
|
- } else {
|
|
|
- columnsForCardsMode.push(1300, 1900, 2500); // 1366x768, 1920x1080, 2560x1440
|
|
|
- columnsForTableMode.push(1600, 2500, 3400); // 1680x1050, 2560x1440, 3440x1440
|
|
|
- }
|
|
|
- addEventListener('resize', adjustScriptWidth);
|
|
|
+onMounted(() => {
|
|
|
+ // Ensure the correct UI is shown when mounted:
|
|
|
+ // * on subsequent navigation via history back/forward;
|
|
|
+ // * on first initialization in some weird case the scripts got loaded early.
|
|
|
+ if (!store.loading) refreshUI();
|
|
|
+ // Extract --columns-cards and --columns-table from `:root` or `html` selector. CustomCSS may override it.
|
|
|
+ if (!columnsForCardsMode.length) {
|
|
|
+ const style = customCssElem?.textContent.match(/--columns-(cards|table)\b/)
|
|
|
+ && getComputedStyle(document.documentElement);
|
|
|
+ if (style) {
|
|
|
+ for (const [type, arr] of [
|
|
|
+ ['cards', columnsForCardsMode],
|
|
|
+ ['table', columnsForTableMode],
|
|
|
+ ]) {
|
|
|
+ const val = style.getPropertyValue(`--columns-${type}`);
|
|
|
+ if (val) arr.push(...val.split(',').map(Number).filter(Boolean));
|
|
|
}
|
|
|
- adjustScriptWidth();
|
|
|
- return bindKeys();
|
|
|
- });
|
|
|
-
|
|
|
- return {
|
|
|
- // Refs
|
|
|
- refSearch,
|
|
|
- refList,
|
|
|
- scroller,
|
|
|
-
|
|
|
- // Values
|
|
|
- store,
|
|
|
- state,
|
|
|
- filters,
|
|
|
- filterOptions,
|
|
|
- currentSortCompare,
|
|
|
- selectedScript,
|
|
|
- draggable,
|
|
|
- scriptHotkeys,
|
|
|
- showRecycle,
|
|
|
- message,
|
|
|
-
|
|
|
- // Methods
|
|
|
- handleStateChange,
|
|
|
- handleOrderChange,
|
|
|
- handleEditScript,
|
|
|
- handleEmptyRecycleBin,
|
|
|
- handleInstallFromURL,
|
|
|
- handleUpdateAll,
|
|
|
- handleOnScopeChange,
|
|
|
- handleActionRemove,
|
|
|
- handleActionRestore,
|
|
|
- handleActionToggle,
|
|
|
- handleActionUpdate,
|
|
|
- handleSmoothScroll,
|
|
|
- };
|
|
|
- },
|
|
|
-};
|
|
|
+ } else {
|
|
|
+ columnsForCardsMode.push(1300, 1900, 2500); // 1366x768, 1920x1080, 2560x1440
|
|
|
+ columnsForTableMode.push(1600, 2500, 3400); // 1680x1050, 2560x1440, 3440x1440
|
|
|
+ }
|
|
|
+ addEventListener('resize', adjustScriptWidth);
|
|
|
+ }
|
|
|
+ adjustScriptWidth();
|
|
|
+ return bindKeys();
|
|
|
+});
|
|
|
</script>
|
|
|
|
|
|
<style>
|
|
|
@@ -768,9 +729,6 @@ $iconSize: 2rem; // from .icon in ui/style.css
|
|
|
text-align: center;
|
|
|
color: var(--fill-8);
|
|
|
}
|
|
|
-.scripts {
|
|
|
- overflow-y: auto;
|
|
|
-}
|
|
|
.backdrop > *,
|
|
|
.backdrop::after {
|
|
|
display: inline-block;
|
|
|
@@ -800,6 +758,7 @@ $iconSize: 2rem; // from .icon in ui/style.css
|
|
|
}
|
|
|
}
|
|
|
.filter-search {
|
|
|
+ min-width: 14rem;
|
|
|
label {
|
|
|
position: relative;
|
|
|
}
|
|
|
@@ -810,13 +769,16 @@ $iconSize: 2rem; // from .icon in ui/style.css
|
|
|
right: .5rem;
|
|
|
}
|
|
|
input {
|
|
|
- width: 14rem;
|
|
|
- max-width: calc(100vw - 16rem);
|
|
|
+ width: 100%;
|
|
|
+ height: 2rem;
|
|
|
padding-left: .5rem;
|
|
|
padding-right: 2rem;
|
|
|
- height: 100%;
|
|
|
}
|
|
|
&-tooltip {
|
|
|
+ width: 24rem;
|
|
|
+ max-width: 100vw;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 1.5;
|
|
|
white-space: pre-wrap;
|
|
|
}
|
|
|
}
|