mirror of
https://github.com/aleksey-hoffman/sigma-file-manager.git
synced 2026-10-18 17:20:21 -05:00
UX: improved settings search
- Marching shortcuts now appear in the search results
This commit is contained in:
1 parent
5b3137c8c7
commit
d55295a91e
16 files changed
+591
-60
No files matched your search
@@ -1242,4 +1242,4 @@
|
||||
"replaceShareDialogDescription": "文件已在网络上共享。当前共享将被停止并替换为新的共享。",
|
||||
"replaceShareDialogConfirm": "替换"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1242,4 +1242,4 @@
|
||||
"replaceShareDialogDescription": "فایلها از قبل از طریق شبکه به اشتراک گذاشته شدهاند. اشتراکگذاری فعلی متوقف و با مورد جدید جایگزین خواهد شد.",
|
||||
"replaceShareDialogConfirm": "جایگزینی"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1242,4 +1242,4 @@
|
||||
"replaceShareDialogDescription": "קבצים כבר משותפים ברשת. השיתוף הנוכחי ייעצר ויוחלף בחדש.",
|
||||
"replaceShareDialogConfirm": "החלף"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1242,4 +1242,4 @@
|
||||
"replaceShareDialogDescription": "फ़ाइलें पहले से ही नेटवर्क पर साझा की जा रही हैं. वर्तमान शेयर बंद कर दिया जाएगा और उसके स्थान पर नया शेयर ले लिया जाएगा।",
|
||||
"replaceShareDialogConfirm": "प्रतिस्थापित करें"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1242,4 +1242,4 @@
|
||||
"replaceShareDialogDescription": "Ֆայլերն արդեն կիսվում են ցանցում: Ընթացիկ կիսումը կդադարեցվի և կփոխարինվի նորով:",
|
||||
"replaceShareDialogConfirm": "Փոխարինել"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1242,4 +1242,4 @@
|
||||
"replaceShareDialogDescription": "Datoteke se že delijo po omrežju. Trenutno deljenje bo ustavljeno in zamenjano z novim.",
|
||||
"replaceShareDialogConfirm": "Zamenjaj"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1242,4 +1242,4 @@
|
||||
"replaceShareDialogDescription": "Ağ üzerinden zaten dosyalar paylaşılıyor. Mevcut paylaşım durdurulup yenisiyle değiştirilecek.",
|
||||
"replaceShareDialogConfirm": "Değiştir"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1242,4 +1242,4 @@
|
||||
"replaceShareDialogDescription": "فائلیں پہلے سے نیٹ ورک پر ہیں۔ موجودہ شیئر بند ہو کر نئے سے بدل جائے گا۔",
|
||||
"replaceShareDialogConfirm": "بدل دیں"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1242,4 +1242,4 @@
|
||||
"replaceShareDialogDescription": "Các tập tin đang được chia sẻ qua mạng. Chia sẻ hiện tại sẽ bị dừng và thay thế bằng chia sẻ mới.",
|
||||
"replaceShareDialogConfirm": "Thay thế"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@ watch(
|
||||
>
|
||||
<SettingsActions />
|
||||
<SettingsSearch />
|
||||
<SettingsHeader />
|
||||
<SettingsHeader v-if="!settingsStore.search" />
|
||||
<SettingsContent />
|
||||
</PageDefaultLayout>
|
||||
</template>
|
||||
|
||||
@@ -89,8 +89,14 @@ import { useExtensionsStorageStore } from '@/stores/storage/extensions';
|
||||
import { BlocksIcon } from '@lucide/vue';
|
||||
import type { ExtensionKeybindingOverride } from '@/types/extension';
|
||||
import { SettingsItem } from '@/modules/settings';
|
||||
import { useSettingsStore } from '@/stores/runtime/settings';
|
||||
import {
|
||||
filterItemsForSettingsSearch,
|
||||
shortcutSearchItemMatches,
|
||||
} from '@/modules/settings/utils/settings-search';
|
||||
|
||||
const { t } = useI18n();
|
||||
const settingsStore = useSettingsStore();
|
||||
const shortcutsStore = useShortcutsStore();
|
||||
const globalShortcutsStore = useGlobalShortcutsStore();
|
||||
const unassignedShortcutLabel = '—';
|
||||
@@ -1151,6 +1157,64 @@ const globalExtensionShortcuts = computed(() => {
|
||||
keybindingLabel: formatKeybindingKeys(shortcut.keys),
|
||||
}));
|
||||
});
|
||||
|
||||
function filterShortcutRows<T>(
|
||||
rows: T[],
|
||||
rowMatches: (row: T) => boolean,
|
||||
): T[] {
|
||||
return filterItemsForSettingsSearch(
|
||||
rows,
|
||||
settingsStore.search,
|
||||
rowMatches,
|
||||
);
|
||||
}
|
||||
|
||||
const visibleGlobalAppShortcuts = computed(() =>
|
||||
filterShortcutRows(
|
||||
globalShortcutsStore.definitions,
|
||||
definition => shortcutSearchItemMatches({
|
||||
labelKey: definition.labelKey,
|
||||
combinations: [globalShortcutsStore.getShortcutLabel(definition.id)],
|
||||
}, settingsStore.search),
|
||||
),
|
||||
);
|
||||
|
||||
const visibleGlobalExtensionShortcuts = computed(() =>
|
||||
filterShortcutRows(
|
||||
globalExtensionShortcuts.value,
|
||||
shortcut => shortcutSearchItemMatches({
|
||||
titles: [shortcut.commandTitle],
|
||||
combinations: [shortcut.keybindingLabel, formatShortcutKeys(shortcut.keys)],
|
||||
}, settingsStore.search),
|
||||
),
|
||||
);
|
||||
|
||||
const visibleLocalShortcuts = computed(() =>
|
||||
filterShortcutRows(
|
||||
shortcutsStore.definitions,
|
||||
definition => shortcutSearchItemMatches({
|
||||
labelKey: definition.labelKey,
|
||||
combinations: [shortcutsStore.getShortcutBindingLabel(definition)],
|
||||
}, settingsStore.search),
|
||||
),
|
||||
);
|
||||
|
||||
const visibleExtensionKeybindings = computed(() =>
|
||||
filterShortcutRows(
|
||||
extensionKeybindings.value,
|
||||
keybinding => shortcutSearchItemMatches({
|
||||
titles: [keybinding.commandTitle],
|
||||
combinations: [keybinding.keybindingLabel, formatShortcutKeys(keybinding.keys)],
|
||||
}, settingsStore.search),
|
||||
),
|
||||
);
|
||||
|
||||
const hasVisibleGlobalShortcuts = computed(() =>
|
||||
visibleGlobalAppShortcuts.value.length > 0
|
||||
|| visibleGlobalExtensionShortcuts.value.length > 0,
|
||||
);
|
||||
|
||||
const hasVisibleLocalShortcuts = computed(() => visibleLocalShortcuts.value.length > 0);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -1172,7 +1236,10 @@ const globalExtensionShortcuts = computed(() => {
|
||||
</template>
|
||||
|
||||
<div class="shortcuts-section">
|
||||
<div class="shortcuts-section__group">
|
||||
<div
|
||||
v-if="hasVisibleGlobalShortcuts"
|
||||
class="shortcuts-section__group"
|
||||
>
|
||||
<div class="shortcuts-section__group-header">
|
||||
<div class="shortcuts-section__group-info">
|
||||
<h4 class="shortcuts-section__group-title">
|
||||
@@ -1200,7 +1267,7 @@ const globalExtensionShortcuts = computed(() => {
|
||||
|
||||
<div class="shortcuts-table__body">
|
||||
<ContextMenu
|
||||
v-for="definition in globalShortcutsStore.definitions"
|
||||
v-for="definition in visibleGlobalAppShortcuts"
|
||||
:key="definition.id"
|
||||
>
|
||||
<ContextMenuTrigger as-child>
|
||||
@@ -1273,7 +1340,7 @@ const globalExtensionShortcuts = computed(() => {
|
||||
</ContextMenu>
|
||||
|
||||
<ContextMenu
|
||||
v-for="shortcut in globalExtensionShortcuts"
|
||||
v-for="shortcut in visibleGlobalExtensionShortcuts"
|
||||
:key="`${shortcut.extensionId}-${shortcut.commandId}`"
|
||||
>
|
||||
<ContextMenuTrigger as-child>
|
||||
@@ -1351,7 +1418,10 @@ const globalExtensionShortcuts = computed(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shortcuts-section__group">
|
||||
<div
|
||||
v-if="hasVisibleLocalShortcuts"
|
||||
class="shortcuts-section__group"
|
||||
>
|
||||
<div class="shortcuts-section__group-header">
|
||||
<div class="shortcuts-section__group-info">
|
||||
<h4 class="shortcuts-section__group-title">
|
||||
@@ -1378,7 +1448,7 @@ const globalExtensionShortcuts = computed(() => {
|
||||
|
||||
<div class="shortcuts-table__body">
|
||||
<ContextMenu
|
||||
v-for="definition in shortcutsStore.definitions"
|
||||
v-for="definition in visibleLocalShortcuts"
|
||||
:key="`${definition.id}-${definition.bindingSlot ?? 0}`"
|
||||
>
|
||||
<ContextMenuTrigger
|
||||
@@ -1489,7 +1559,7 @@ const globalExtensionShortcuts = computed(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="extensionKeybindings.length > 0">
|
||||
<template v-if="visibleExtensionKeybindings.length > 0">
|
||||
<div class="shortcuts-table shortcuts-table--extensions">
|
||||
<div class="shortcuts-table__section-header">
|
||||
<BlocksIcon :size="16" />
|
||||
@@ -1512,7 +1582,7 @@ const globalExtensionShortcuts = computed(() => {
|
||||
|
||||
<div class="shortcuts-table__body">
|
||||
<ContextMenu
|
||||
v-for="keybinding in extensionKeybindings"
|
||||
v-for="keybinding in visibleExtensionKeybindings"
|
||||
:key="`${keybinding.extensionId}-${keybinding.commandId}`"
|
||||
>
|
||||
<ContextMenuTrigger as-child>
|
||||
|
||||
@@ -25,6 +25,7 @@ import StorageCategory from './categories/storage/index.vue';
|
||||
import TabsCategory from './categories/tabs/index.vue';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { DeleteIcon } from '@lucide/vue';
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
const scrollStore = useScrollRestorationStore();
|
||||
@@ -85,11 +86,12 @@ watch(
|
||||
</div>
|
||||
<Button
|
||||
v-if="settingsStore.search"
|
||||
variant="tertiary"
|
||||
variant="outline"
|
||||
class="settings-view__clear-search"
|
||||
@click="settingsStore.clearSearch"
|
||||
>
|
||||
{{ t('globalSearch.clearSearchField') }}
|
||||
<DeleteIcon :size="18" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// License: GNU GPLv3 or later. See the license file in the project root for more information.
|
||||
// Copyright © 2021 - present Aleksey Hoffman. All rights reserved.
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
filterItemsForSettingsSearch,
|
||||
filterSettingsSections,
|
||||
matchesAnyLocale,
|
||||
matchesShortcutCombination,
|
||||
shortcutSearchItemMatches,
|
||||
} from '@/modules/settings/utils/settings-search';
|
||||
|
||||
describe('settings search', () => {
|
||||
it('matches setting titles in any locale', () => {
|
||||
expect(matchesAnyLocale('shortcuts.setSelectedItemsForCopying', 'copy')).toBe(true);
|
||||
expect(matchesAnyLocale('shortcuts.setSelectedItemsForCopying', 'копирован')).toBe(true);
|
||||
expect(matchesAnyLocale('shortcuts.focusAppWindow', 'показать окно')).toBe(true);
|
||||
expect(matchesAnyLocale('shortcuts.focusAppWindow', 'theme')).toBe(false);
|
||||
});
|
||||
|
||||
it('matches shortcut combinations with partial and alternate spellings', () => {
|
||||
expect(matchesShortcutCombination('Ctrl+C', 'ctrl+c')).toBe(true);
|
||||
expect(matchesShortcutCombination('Ctrl+C', 'ctrl + c')).toBe(true);
|
||||
expect(matchesShortcutCombination('Ctrl+C', 'ctrl-c')).toBe(true);
|
||||
expect(matchesShortcutCombination('Ctrl+C', 'control+c')).toBe(true);
|
||||
expect(matchesShortcutCombination('Ctrl+C', 'ctrlc')).toBe(true);
|
||||
expect(matchesShortcutCombination('Ctrl+C', 'ctrl+')).toBe(true);
|
||||
expect(matchesShortcutCombination('Ctrl+Shift+N', 'ctrl+sh')).toBe(true);
|
||||
expect(matchesShortcutCombination('Ctrl+Shift+N', 'ctrl shift n')).toBe(true);
|
||||
expect(matchesShortcutCombination('Win+Shift+E', 'super+shift')).toBe(true);
|
||||
expect(matchesShortcutCombination('Win+Shift+E', 'cmd+shift+e')).toBe(true);
|
||||
expect(matchesShortcutCombination('Meta+K', 'win+k')).toBe(true);
|
||||
expect(matchesShortcutCombination('F5', 'f5')).toBe(true);
|
||||
expect(matchesShortcutCombination('Del', 'delete')).toBe(true);
|
||||
expect(matchesShortcutCombination('Alt+↑', 'arrowup')).toBe(true);
|
||||
expect(matchesShortcutCombination('Alt+↑', 'up')).toBe(true);
|
||||
expect(matchesShortcutCombination('Ctrl+C', 'c')).toBe(true);
|
||||
expect(matchesShortcutCombination('Ctrl+C', 'ctrl+v')).toBe(false);
|
||||
expect(matchesShortcutCombination('Ctrl+A', 'c')).toBe(false);
|
||||
expect(matchesShortcutCombination('Ctrl+C', '+')).toBe(false);
|
||||
expect(matchesShortcutCombination('Ctrl+C', '-')).toBe(false);
|
||||
expect(matchesShortcutCombination('Ctrl+-', '-')).toBe(true);
|
||||
expect(matchesShortcutCombination('Ctrl++', '+')).toBe(true);
|
||||
expect(matchesShortcutCombination('Ctrl++', 'ctrl++')).toBe(true);
|
||||
expect(matchesShortcutCombination('Ctrl+C', 'ctrl++')).toBe(false);
|
||||
expect(matchesShortcutCombination('', 'ctrl')).toBe(false);
|
||||
});
|
||||
|
||||
it('matches shortcut search items by locale title or combination', () => {
|
||||
expect(shortcutSearchItemMatches({
|
||||
labelKey: 'shortcuts.setSelectedItemsForCopying',
|
||||
combinations: ['Ctrl+C'],
|
||||
}, 'копирован')).toBe(true);
|
||||
|
||||
expect(shortcutSearchItemMatches({
|
||||
labelKey: 'shortcuts.setSelectedItemsForCopying',
|
||||
combinations: ['Ctrl+C'],
|
||||
}, 'ctrl+c')).toBe(true);
|
||||
|
||||
expect(shortcutSearchItemMatches({
|
||||
titles: ['Convert video'],
|
||||
combinations: ['Alt+Shift+V'],
|
||||
}, 'convert')).toBe(true);
|
||||
|
||||
expect(shortcutSearchItemMatches({
|
||||
labelKey: 'shortcuts.setSelectedItemsForCopying',
|
||||
combinations: ['Ctrl+C'],
|
||||
}, 'theme')).toBe(false);
|
||||
});
|
||||
|
||||
it('shows the shortcuts section for title, tag, or matching shortcut data', () => {
|
||||
const sections = [
|
||||
{
|
||||
key: 'theme',
|
||||
titleKey: 'settings.homeBannerEffects.theme.title',
|
||||
tags: 'settingsTags.theme',
|
||||
category: 'appearance',
|
||||
},
|
||||
{
|
||||
key: 'shortcuts',
|
||||
titleKey: 'settingsTabs.shortcuts',
|
||||
tags: 'settingsTags.shortcuts',
|
||||
category: 'shortcuts',
|
||||
},
|
||||
];
|
||||
|
||||
expect(filterSettingsSections(sections, '', 'appearance').map(section => section.key)).toEqual(['theme']);
|
||||
expect(filterSettingsSections(sections, 'shortcuts', 'appearance').map(section => section.key)).toEqual(['shortcuts']);
|
||||
expect(filterSettingsSections(sections, 'шорткаты', 'appearance').map(section => section.key)).toEqual(['shortcuts']);
|
||||
expect(filterSettingsSections(sections, 'ctrl+c', 'appearance', {
|
||||
shortcuts: () => true,
|
||||
}).map(section => section.key)).toEqual(['shortcuts']);
|
||||
expect(filterSettingsSections(sections, 'ctrl+c', 'appearance', {
|
||||
shortcuts: () => false,
|
||||
})).toEqual([]);
|
||||
});
|
||||
|
||||
it('filters shortcut rows unless the query matches the shortcuts section itself', () => {
|
||||
const rows = [
|
||||
{
|
||||
id: 'copy',
|
||||
labelKey: 'shortcuts.setSelectedItemsForCopying',
|
||||
combo: 'Ctrl+C',
|
||||
},
|
||||
{
|
||||
id: 'paste',
|
||||
labelKey: 'shortcuts.transferPreparedForCopying',
|
||||
combo: 'Ctrl+V',
|
||||
},
|
||||
{
|
||||
id: 'rename',
|
||||
labelKey: 'shortcuts.renameSelectedItems',
|
||||
combo: 'F2',
|
||||
},
|
||||
];
|
||||
|
||||
const matchingCopy = filterItemsForSettingsSearch(
|
||||
rows,
|
||||
'ctrl+c',
|
||||
row => shortcutSearchItemMatches({
|
||||
labelKey: row.labelKey,
|
||||
combinations: [row.combo],
|
||||
}, 'ctrl+c'),
|
||||
);
|
||||
|
||||
expect(matchingCopy.map(row => row.id)).toEqual(['copy']);
|
||||
|
||||
const matchingSection = filterItemsForSettingsSearch(
|
||||
rows,
|
||||
'shortcuts',
|
||||
row => shortcutSearchItemMatches({
|
||||
labelKey: row.labelKey,
|
||||
combinations: [row.combo],
|
||||
}, 'shortcuts'),
|
||||
);
|
||||
|
||||
expect(matchingSection.map(row => row.id)).toEqual(['copy', 'paste', 'rename']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,275 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// License: GNU GPLv3 or later. See the license file in the project root for more information.
|
||||
// Copyright © 2021 - present Aleksey Hoffman. All rights reserved.
|
||||
|
||||
import { messages } from '@/localization/data';
|
||||
|
||||
export const SHORTCUTS_SECTION_SEARCH_KEYS = [
|
||||
'settingsTabs.shortcuts',
|
||||
'settingsTags.shortcuts',
|
||||
] as const;
|
||||
|
||||
export type SettingsSectionSearchFields = {
|
||||
key: string;
|
||||
titleKey: string;
|
||||
tags: string;
|
||||
category: string;
|
||||
};
|
||||
|
||||
export type SettingsShortcutSearchItem = {
|
||||
labelKey?: string;
|
||||
titles?: string[];
|
||||
combinations: string[];
|
||||
};
|
||||
|
||||
const SHORTCUT_DISPLAY_ALIASES: Array<[string, string]> = [
|
||||
['↑', 'ArrowUp'],
|
||||
['↓', 'ArrowDown'],
|
||||
['←', 'ArrowLeft'],
|
||||
['→', 'ArrowRight'],
|
||||
['Del', 'Delete'],
|
||||
['Mouse Button 4', 'MouseButton4'],
|
||||
['Mouse Button 5', 'MouseButton5'],
|
||||
];
|
||||
|
||||
function getNestedValue(obj: Record<string, unknown>, path: string): string | undefined {
|
||||
const keys = path.split('.');
|
||||
let current: unknown = obj;
|
||||
|
||||
for (const key of keys) {
|
||||
if (current === null || current === undefined || typeof current !== 'object') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
current = (current as Record<string, unknown>)[key];
|
||||
}
|
||||
|
||||
return typeof current === 'string' ? current : undefined;
|
||||
}
|
||||
|
||||
export function getAllTranslations(key: string): string[] {
|
||||
const translations: string[] = [];
|
||||
|
||||
for (const locale of Object.keys(messages)) {
|
||||
const localeMessages = messages[locale as keyof typeof messages];
|
||||
const value = getNestedValue(localeMessages as Record<string, unknown>, key);
|
||||
|
||||
if (value) {
|
||||
translations.push(value.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
return translations;
|
||||
}
|
||||
|
||||
export function normalizeSettingsSearchTerm(searchTerm: string): string {
|
||||
return searchTerm.trim().toLowerCase();
|
||||
}
|
||||
|
||||
export function matchesAnyLocale(key: string, searchTerm: string): boolean {
|
||||
const normalizedSearch = normalizeSettingsSearchTerm(searchTerm);
|
||||
|
||||
if (!normalizedSearch) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return getAllTranslations(key).some(translation => translation.includes(normalizedSearch));
|
||||
}
|
||||
|
||||
function applyShortcutModifierAliases(value: string): string {
|
||||
return value
|
||||
.replaceAll('commandorcontrol', 'ctrl')
|
||||
.replaceAll('control', 'ctrl')
|
||||
.replaceAll('windows', 'win')
|
||||
.replaceAll('command', 'win')
|
||||
.replaceAll('super', 'win')
|
||||
.replaceAll('meta', 'win')
|
||||
.replaceAll('cmd', 'win');
|
||||
}
|
||||
|
||||
function normalizeShortcutCombinationText(value: string): string {
|
||||
const compact = value
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, '')
|
||||
.replace(/(?<=[\p{L}\p{N}])-(?=[\p{L}\p{N}])/gu, '+');
|
||||
|
||||
return applyShortcutModifierAliases(compact);
|
||||
}
|
||||
|
||||
function splitShortcutComboParts(normalizedCombo: string): string[] {
|
||||
const parts: string[] = [];
|
||||
let currentPart = '';
|
||||
|
||||
for (const character of normalizedCombo) {
|
||||
if (character === '+') {
|
||||
if (currentPart) {
|
||||
parts.push(currentPart);
|
||||
currentPart = '';
|
||||
}
|
||||
else {
|
||||
parts.push('+');
|
||||
}
|
||||
}
|
||||
else {
|
||||
currentPart += character;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentPart) {
|
||||
parts.push(currentPart);
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
export function getShortcutCombinationSearchTexts(formattedLabel: string): string[] {
|
||||
if (!formattedLabel) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const texts = [formattedLabel];
|
||||
|
||||
for (const [displayToken, alias] of SHORTCUT_DISPLAY_ALIASES) {
|
||||
if (formattedLabel.includes(displayToken)) {
|
||||
texts.push(formattedLabel.replaceAll(displayToken, alias));
|
||||
}
|
||||
}
|
||||
|
||||
return texts;
|
||||
}
|
||||
|
||||
function isShortcutPartMatch(comboPart: string, queryPart: string): boolean {
|
||||
if (comboPart === queryPart) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return queryPart.length >= 2 && comboPart.startsWith(queryPart);
|
||||
}
|
||||
|
||||
function matchesNormalizedShortcutCombination(formattedKeys: string, searchTerm: string): boolean {
|
||||
if (!formattedKeys) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const normalizedQuery = normalizeShortcutCombinationText(normalizeSettingsSearchTerm(searchTerm));
|
||||
|
||||
if (!normalizedQuery) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const normalizedCombo = normalizeShortcutCombinationText(formattedKeys);
|
||||
|
||||
if (normalizedCombo === normalizedQuery) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const compactCombo = normalizedCombo.replaceAll('+', '');
|
||||
const compactQuery = normalizedQuery.replaceAll('+', '');
|
||||
|
||||
if (normalizedQuery.includes('+') && compactQuery.length > 0 && normalizedCombo.includes(normalizedQuery)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!normalizedQuery.includes('+') && compactQuery.length >= 3 && compactCombo.includes(compactQuery)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const queryParts = splitShortcutComboParts(normalizedQuery);
|
||||
const comboParts = splitShortcutComboParts(normalizedCombo);
|
||||
|
||||
if (queryParts.length === 0 || comboParts.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (queryParts.length > 1) {
|
||||
return queryParts.every(queryPart =>
|
||||
comboParts.some(comboPart => isShortcutPartMatch(comboPart, queryPart)),
|
||||
);
|
||||
}
|
||||
|
||||
const [singlePart] = queryParts;
|
||||
const mainKey = comboParts[comboParts.length - 1];
|
||||
|
||||
if (singlePart.length >= 3) {
|
||||
return comboParts.some(comboPart => isShortcutPartMatch(comboPart, singlePart) || comboPart.includes(singlePart));
|
||||
}
|
||||
|
||||
return comboParts.some(comboPart => comboPart === singlePart)
|
||||
|| (singlePart.length >= 2 && mainKey.includes(singlePart));
|
||||
}
|
||||
|
||||
export function matchesShortcutCombination(formattedKeys: string, searchTerm: string): boolean {
|
||||
return getShortcutCombinationSearchTexts(formattedKeys).some(combinationText =>
|
||||
matchesNormalizedShortcutCombination(combinationText, searchTerm),
|
||||
);
|
||||
}
|
||||
|
||||
export function shortcutSearchItemMatches(
|
||||
item: SettingsShortcutSearchItem,
|
||||
searchTerm: string,
|
||||
): boolean {
|
||||
const normalizedSearch = normalizeSettingsSearchTerm(searchTerm);
|
||||
|
||||
if (!normalizedSearch) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (item.labelKey && matchesAnyLocale(item.labelKey, normalizedSearch)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (item.titles?.some(title => title.toLowerCase().includes(normalizedSearch))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return item.combinations.some(combination =>
|
||||
matchesShortcutCombination(combination, normalizedSearch),
|
||||
);
|
||||
}
|
||||
|
||||
export function filterItemsForSettingsSearch<T>(
|
||||
items: T[],
|
||||
searchTerm: string,
|
||||
itemMatches: (item: T) => boolean,
|
||||
sectionKeys: readonly string[] = SHORTCUTS_SECTION_SEARCH_KEYS,
|
||||
): T[] {
|
||||
const normalizedSearch = normalizeSettingsSearchTerm(searchTerm);
|
||||
|
||||
if (!normalizedSearch) {
|
||||
return items;
|
||||
}
|
||||
|
||||
const matchingItems = items.filter(itemMatches);
|
||||
|
||||
if (matchingItems.length > 0) {
|
||||
return matchingItems;
|
||||
}
|
||||
|
||||
const sectionMatches = sectionKeys.some(key => matchesAnyLocale(key, normalizedSearch));
|
||||
return sectionMatches ? items : matchingItems;
|
||||
}
|
||||
|
||||
export function filterSettingsSections<T extends SettingsSectionSearchFields>(
|
||||
sections: T[],
|
||||
searchTerm: string,
|
||||
currentTab: string,
|
||||
extraSectionMatchers: Partial<Record<string, (normalizedSearch: string) => boolean>> = {},
|
||||
): T[] {
|
||||
const normalizedSearch = normalizeSettingsSearchTerm(searchTerm);
|
||||
|
||||
if (!normalizedSearch) {
|
||||
return sections.filter(section => section.category === currentTab);
|
||||
}
|
||||
|
||||
return sections.filter((section) => {
|
||||
if (matchesAnyLocale(section.titleKey, normalizedSearch)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (matchesAnyLocale(section.tags, normalizedSearch)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return extraSectionMatchers[section.key]?.(normalizedSearch) ?? false;
|
||||
});
|
||||
}
|
||||
@@ -10,10 +10,20 @@ import {
|
||||
markRaw,
|
||||
type Component,
|
||||
} from 'vue';
|
||||
import { messages } from '@/localization/data';
|
||||
import { i18n } from '@/localization';
|
||||
import { formatKeybindingKeys } from '@/modules/extensions/api';
|
||||
import { useUserSettingsStore } from '@/stores/storage/user-settings';
|
||||
import { usePlatformStore } from '@/stores/runtime/platform';
|
||||
import {
|
||||
formatShortcutKeys,
|
||||
useShortcutsStore,
|
||||
} from '@/stores/runtime/shortcuts';
|
||||
import { useGlobalShortcutsStore } from '@/stores/runtime/global-shortcuts';
|
||||
import { useExtensionsStore } from '@/stores/runtime/extensions';
|
||||
import {
|
||||
filterSettingsSections,
|
||||
shortcutSearchItemMatches,
|
||||
} from '@/modules/settings/utils/settings-search';
|
||||
|
||||
export interface SettingsSection {
|
||||
key: string;
|
||||
@@ -71,39 +81,76 @@ function normalizeSettingsTabName(tabName: string): string {
|
||||
return settingsTabs.some(tab => tab.name === tabName) ? tabName : 'general';
|
||||
}
|
||||
|
||||
function getNestedValue(obj: Record<string, unknown>, path: string): string | undefined {
|
||||
const keys = path.split('.');
|
||||
let current: unknown = obj;
|
||||
function getExtensionCommandSearchTitles(extensionId: string, commandId: string): string[] {
|
||||
const extensionsStore = useExtensionsStore();
|
||||
const titles = [commandId];
|
||||
const command = extensionsStore.commands.find(registeredCommand => registeredCommand.command.id === commandId);
|
||||
|
||||
for (const key of keys) {
|
||||
if (current === null || current === undefined || typeof current !== 'object') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
current = (current as Record<string, unknown>)[key];
|
||||
if (command?.command.title) {
|
||||
titles.push(command.command.title);
|
||||
}
|
||||
|
||||
return typeof current === 'string' ? current : undefined;
|
||||
}
|
||||
const contextMenuItem = extensionsStore.contextMenuItems.find(
|
||||
item => item.extensionId === extensionId && item.item.id === commandId,
|
||||
);
|
||||
|
||||
function getAllTranslations(key: string): string[] {
|
||||
const translations: string[] = [];
|
||||
|
||||
for (const locale of Object.keys(messages)) {
|
||||
const localeMessages = messages[locale as keyof typeof messages];
|
||||
const value = getNestedValue(localeMessages as Record<string, unknown>, key);
|
||||
|
||||
if (value) {
|
||||
translations.push(value.toLowerCase());
|
||||
}
|
||||
if (contextMenuItem?.item.title) {
|
||||
titles.push(contextMenuItem.item.title);
|
||||
}
|
||||
|
||||
return translations;
|
||||
return titles;
|
||||
}
|
||||
|
||||
function matchesAnyLocale(key: string, searchTerm: string): boolean {
|
||||
const translations = getAllTranslations(key);
|
||||
return translations.some(translation => translation.includes(searchTerm));
|
||||
function shortcutsSectionMatchesSearch(searchTerm: string): boolean {
|
||||
const shortcutsStore = useShortcutsStore();
|
||||
const globalShortcutsStore = useGlobalShortcutsStore();
|
||||
const extensionsStore = useExtensionsStore();
|
||||
|
||||
const appShortcutMatches = shortcutsStore.definitions.some(definition =>
|
||||
shortcutSearchItemMatches({
|
||||
labelKey: definition.labelKey,
|
||||
combinations: [shortcutsStore.getShortcutBindingLabel(definition)],
|
||||
}, searchTerm),
|
||||
);
|
||||
|
||||
if (appShortcutMatches) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const globalShortcutMatches = globalShortcutsStore.definitions.some(definition =>
|
||||
shortcutSearchItemMatches({
|
||||
labelKey: definition.labelKey,
|
||||
combinations: [globalShortcutsStore.getShortcutLabel(definition.id)],
|
||||
}, searchTerm),
|
||||
);
|
||||
|
||||
if (globalShortcutMatches) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const extensionLocalMatches = extensionsStore.keybindings.some(keybinding =>
|
||||
shortcutSearchItemMatches({
|
||||
titles: getExtensionCommandSearchTitles(keybinding.extensionId, keybinding.commandId),
|
||||
combinations: [
|
||||
formatKeybindingKeys(keybinding.keys),
|
||||
formatShortcutKeys(keybinding.keys),
|
||||
],
|
||||
}, searchTerm),
|
||||
);
|
||||
|
||||
if (extensionLocalMatches) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return globalShortcutsStore.extensionDefinitions.some(shortcut =>
|
||||
shortcutSearchItemMatches({
|
||||
titles: [shortcut.commandTitle],
|
||||
combinations: [
|
||||
formatKeybindingKeys(shortcut.keys),
|
||||
formatShortcutKeys(shortcut.keys),
|
||||
],
|
||||
}, searchTerm),
|
||||
);
|
||||
}
|
||||
|
||||
export const useSettingsStore = defineStore('settings', () => {
|
||||
@@ -439,20 +486,14 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
return sections.value.filter(section => section.category === category);
|
||||
}
|
||||
|
||||
const filteredSections = computed(() => {
|
||||
if (!search.value) {
|
||||
return sections.value.filter(section => section.category === currentTab.value);
|
||||
}
|
||||
|
||||
const searchTerm = search.value.toLowerCase();
|
||||
|
||||
return sections.value.filter((section) => {
|
||||
const titleMatches = matchesAnyLocale(section.titleKey, searchTerm);
|
||||
const tagsMatch = matchesAnyLocale(section.tags, searchTerm);
|
||||
|
||||
return titleMatches || tagsMatch;
|
||||
});
|
||||
});
|
||||
const filteredSections = computed(() => filterSettingsSections(
|
||||
sections.value,
|
||||
search.value,
|
||||
currentTab.value,
|
||||
{
|
||||
shortcuts: shortcutsSectionMatchesSearch,
|
||||
},
|
||||
));
|
||||
|
||||
const currentTabSections = computed(() => filteredSections.value);
|
||||
|
||||
|
||||
@@ -4,8 +4,11 @@
|
||||
"exclude": [],
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo",
|
||||
|
||||
"lib": [],
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"types": ["node", "jsdom"]
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user