mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 03:34:40 +00:00
feat: improved hotkey support and controls
This commit is contained in:
@@ -0,0 +1,229 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { isValidHotkey, parseHotkey } from '@/plugins/built-in/globalSearch/src/utils/hotkeyUtils';
|
||||||
|
|
||||||
|
let { value, onChange } = $props<{
|
||||||
|
value: string,
|
||||||
|
onChange: (newValue: string) => void
|
||||||
|
}>();
|
||||||
|
|
||||||
|
let isRecording = $state(false);
|
||||||
|
let recordedKeys = $state<Set<string>>(new Set());
|
||||||
|
let inputElement = $state<HTMLInputElement>();
|
||||||
|
|
||||||
|
const formatKeyForHotkey = (key: string): string => {
|
||||||
|
// Map special keys to their hotkey format
|
||||||
|
const keyMap: Record<string, string> = {
|
||||||
|
'Control': 'ctrl',
|
||||||
|
'Meta': 'cmd',
|
||||||
|
'Alt': 'alt',
|
||||||
|
'Shift': 'shift',
|
||||||
|
' ': 'space',
|
||||||
|
'ArrowUp': 'up',
|
||||||
|
'ArrowDown': 'down',
|
||||||
|
'ArrowLeft': 'left',
|
||||||
|
'ArrowRight': 'right',
|
||||||
|
'Escape': 'esc',
|
||||||
|
'Enter': 'enter',
|
||||||
|
'Tab': 'tab',
|
||||||
|
'Backspace': 'backspace',
|
||||||
|
'Delete': 'delete',
|
||||||
|
};
|
||||||
|
|
||||||
|
return keyMap[key] || key.toLowerCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatKeyForDisplay = (key: string): string => {
|
||||||
|
// Map keys to their display format
|
||||||
|
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
||||||
|
const keyMap: Record<string, string> = {
|
||||||
|
'ctrl': isMac ? '⌃' : 'Ctrl',
|
||||||
|
'cmd': '⌘',
|
||||||
|
'meta': '⌘',
|
||||||
|
'alt': isMac ? '⌥' : 'Alt',
|
||||||
|
'shift': isMac ? '⇧' : 'Shift',
|
||||||
|
'space': 'Space',
|
||||||
|
'up': '↑',
|
||||||
|
'down': '↓',
|
||||||
|
'left': '←',
|
||||||
|
'right': '→',
|
||||||
|
'esc': 'Esc',
|
||||||
|
'enter': 'Enter',
|
||||||
|
'tab': 'Tab',
|
||||||
|
'backspace': 'Backspace',
|
||||||
|
'delete': 'Delete',
|
||||||
|
};
|
||||||
|
|
||||||
|
return keyMap[key.toLowerCase()] || key.toUpperCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHotkeyParts = (hotkeyString: string): string[] => {
|
||||||
|
if (!hotkeyString || !isValidHotkey(hotkeyString)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = parseHotkey(hotkeyString);
|
||||||
|
const parts: string[] = [];
|
||||||
|
|
||||||
|
// Add modifiers in a consistent order
|
||||||
|
if (parsed.ctrl) parts.push('ctrl');
|
||||||
|
if (parsed.meta) parts.push('cmd');
|
||||||
|
if (parsed.alt) parts.push('alt');
|
||||||
|
if (parsed.shift) parts.push('shift');
|
||||||
|
|
||||||
|
// Add the main key
|
||||||
|
if (parsed.key) parts.push(parsed.key);
|
||||||
|
|
||||||
|
return parts;
|
||||||
|
};
|
||||||
|
|
||||||
|
const startRecording = () => {
|
||||||
|
isRecording = true;
|
||||||
|
recordedKeys.clear();
|
||||||
|
inputElement?.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopRecording = () => {
|
||||||
|
if (recordedKeys.size > 0) {
|
||||||
|
if (recordedKeys.has('esc')) {
|
||||||
|
onChange('');
|
||||||
|
isRecording = false;
|
||||||
|
recordedKeys.clear();
|
||||||
|
inputElement?.blur();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the hotkey string
|
||||||
|
const modifiers: string[] = [];
|
||||||
|
let mainKey = '';
|
||||||
|
|
||||||
|
for (const key of recordedKeys) {
|
||||||
|
if (['ctrl', 'cmd', 'alt', 'shift'].includes(key)) {
|
||||||
|
modifiers.push(key);
|
||||||
|
} else {
|
||||||
|
mainKey = key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mainKey) {
|
||||||
|
const hotkeyString = [...modifiers, mainKey].join('+');
|
||||||
|
if (isValidHotkey(hotkeyString)) {
|
||||||
|
onChange(hotkeyString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isRecording = false;
|
||||||
|
recordedKeys.clear();
|
||||||
|
inputElement?.blur();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (!isRecording) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const key = formatKeyForHotkey(e.key);
|
||||||
|
|
||||||
|
// Add modifiers
|
||||||
|
if (e.ctrlKey) recordedKeys.add('ctrl');
|
||||||
|
if (e.metaKey) recordedKeys.add('cmd');
|
||||||
|
if (e.altKey) recordedKeys.add('alt');
|
||||||
|
if (e.shiftKey) recordedKeys.add('shift');
|
||||||
|
|
||||||
|
// Add the main key (ignore modifier keys themselves)
|
||||||
|
if (!['ctrl', 'cmd', 'alt', 'shift'].includes(key)) {
|
||||||
|
recordedKeys.add(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-stop recording if we have a main key
|
||||||
|
if (!['ctrl', 'cmd', 'alt', 'shift'].includes(key)) {
|
||||||
|
setTimeout(stopRecording, 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyUp = (e: KeyboardEvent) => {
|
||||||
|
if (!isRecording) return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
if (isRecording) {
|
||||||
|
stopRecording();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (isRecording && inputElement) {
|
||||||
|
inputElement.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the parts to display
|
||||||
|
const hotkeyParts = $derived(isRecording
|
||||||
|
? Array.from(recordedKeys).map(formatKeyForDisplay)
|
||||||
|
: getHotkeyParts(value).map(formatKeyForDisplay));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<div class="relative">
|
||||||
|
{#if isRecording}
|
||||||
|
<!-- Recording state -->
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-center px-3 py-2 text-sm rounded-md dark:bg-[#38373D]/50 bg-[#DDDDDD]/50 border-[#DDDDDD]/30 dark:border-[#38373D]/30 dark:text-white border cursor-pointer text-nowrap"
|
||||||
|
onclick={startRecording}
|
||||||
|
onkeydown={startRecording}
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
{#if hotkeyParts.length === 0}
|
||||||
|
<span class="text-gray-500 dark:text-gray-400">Press keys...</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else if hotkeyParts.length > 0}
|
||||||
|
<!-- Display current hotkey -->
|
||||||
|
<div
|
||||||
|
class="flex gap-1 items-center text-sm rounded-md border-none cursor-pointer dark:text-white"
|
||||||
|
onclick={startRecording}
|
||||||
|
onkeydown={startRecording}
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
{#each hotkeyParts as part}
|
||||||
|
<div class="size-8 text-sm flex items-center justify-center rounded-md border dark:bg-[#38373D]/50 bg-[#DDDDDD]/50 border-[#DDDDDD]/30 dark:border-[#38373D]/30">
|
||||||
|
{part}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<!-- Empty state -->
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-center px-3 py-2 text-sm rounded-md dark:bg-[#38373D]/50 bg-[#DDDDDD] dark:text-white border-none cursor-pointer text-nowrap"
|
||||||
|
onclick={startRecording}
|
||||||
|
onkeydown={startRecording}
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<span class="text-gray-500 dark:text-gray-400">Click to set</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Hidden input for focus management -->
|
||||||
|
<input
|
||||||
|
bind:this={inputElement}
|
||||||
|
type="text"
|
||||||
|
readonly
|
||||||
|
class="absolute inset-0 opacity-0 pointer-events-none"
|
||||||
|
onkeydown={handleKeyDown}
|
||||||
|
onkeyup={handleKeyUp}
|
||||||
|
onblur={handleBlur}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
input:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
import Button from "../../components/Button.svelte"
|
import Button from "../../components/Button.svelte"
|
||||||
import Slider from "../../components/Slider.svelte"
|
import Slider from "../../components/Slider.svelte"
|
||||||
import Select from "@/interface/components/Select.svelte"
|
import Select from "@/interface/components/Select.svelte"
|
||||||
|
import HotkeyInput from "@/interface/components/HotkeyInput.svelte"
|
||||||
|
|
||||||
import browser from "webextension-polyfill"
|
import browser from "webextension-polyfill"
|
||||||
|
|
||||||
@@ -12,7 +13,7 @@
|
|||||||
import hideSensitiveContent from "@/seqta/ui/dev/hideSensitiveContent"
|
import hideSensitiveContent from "@/seqta/ui/dev/hideSensitiveContent"
|
||||||
|
|
||||||
import { getAllPluginSettings } from "@/plugins"
|
import { getAllPluginSettings } from "@/plugins"
|
||||||
import type { BooleanSetting, StringSetting, NumberSetting, SelectSetting, ButtonSetting } from "@/plugins/core/types"
|
import type { BooleanSetting, StringSetting, NumberSetting, SelectSetting, ButtonSetting, HotkeySetting } from "@/plugins/core/types"
|
||||||
|
|
||||||
// Union type representing all possible settings
|
// Union type representing all possible settings
|
||||||
type SettingType =
|
type SettingType =
|
||||||
@@ -27,6 +28,10 @@
|
|||||||
(Omit<ButtonSetting, 'type'> & {
|
(Omit<ButtonSetting, 'type'> & {
|
||||||
type: 'button',
|
type: 'button',
|
||||||
id: string
|
id: string
|
||||||
|
}) |
|
||||||
|
(Omit<HotkeySetting, 'type'> & {
|
||||||
|
type: 'hotkey',
|
||||||
|
id: string
|
||||||
});
|
});
|
||||||
|
|
||||||
interface Plugin {
|
interface Plugin {
|
||||||
@@ -195,10 +200,10 @@
|
|||||||
{#if (plugin as any).disableToggle}
|
{#if (plugin as any).disableToggle}
|
||||||
<div class="flex justify-between items-center px-4 py-3">
|
<div class="flex justify-between items-center px-4 py-3">
|
||||||
<div class="pr-4">
|
<div class="pr-4">
|
||||||
<h2 class="text-sm font-bold flex items-center gap-2">
|
<h2 class="flex gap-2 items-center text-sm font-bold">
|
||||||
Enable {plugin.name}
|
Enable {plugin.name}
|
||||||
{#if plugin.beta}
|
{#if plugin.beta}
|
||||||
<span class="px-2 py-0.5 text-xs font-medium bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300 rounded-full">
|
<span class="px-2 py-0.5 text-xs font-medium text-orange-800 bg-orange-100 rounded-full dark:bg-orange-900/30 dark:text-orange-300">
|
||||||
Beta
|
Beta
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -258,6 +263,11 @@
|
|||||||
onClick={() => setting.trigger?.()}
|
onClick={() => setting.trigger?.()}
|
||||||
text={setting.title}
|
text={setting.title}
|
||||||
/>
|
/>
|
||||||
|
{:else if setting.type === 'hotkey'}
|
||||||
|
<HotkeyInput
|
||||||
|
value={pluginSettingsValues[plugin.pluginId]?.[key] ?? setting.default}
|
||||||
|
onChange={(value) => updatePluginSetting(plugin.pluginId, key, value)}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,15 +13,22 @@
|
|||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { renderComponentMap } from '../indexing/renderComponents';
|
import { renderComponentMap } from '../indexing/renderComponents';
|
||||||
import HighlightedText from '../utils/HighlightedText.svelte';
|
import HighlightedText from '../utils/HighlightedText.svelte';
|
||||||
|
import { matchesHotkey } from '../utils/hotkeyUtils';
|
||||||
|
import browser from 'webextension-polyfill';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
transparencyEffects,
|
transparencyEffects,
|
||||||
showRecentFirst
|
showRecentFirst,
|
||||||
|
searchHotkey: initialSearchHotkey
|
||||||
} = $props<{
|
} = $props<{
|
||||||
transparencyEffects: boolean,
|
transparencyEffects: boolean,
|
||||||
showRecentFirst: boolean
|
showRecentFirst: boolean,
|
||||||
|
searchHotkey: string
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
// Make searchHotkey reactive to setting changes
|
||||||
|
let currentSearchHotkey = $state(initialSearchHotkey);
|
||||||
|
|
||||||
let commandsFuse = $state<Fuse<StaticCommandItem>>();
|
let commandsFuse = $state<Fuse<StaticCommandItem>>();
|
||||||
let dynamicContentFuse = $state<Fuse<IndexItem>>();
|
let dynamicContentFuse = $state<Fuse<IndexItem>>();
|
||||||
|
|
||||||
@@ -32,6 +39,78 @@
|
|||||||
let completedJobs = $state(0);
|
let completedJobs = $state(0);
|
||||||
let totalJobs = $state(0);
|
let totalJobs = $state(0);
|
||||||
|
|
||||||
|
let commandPalleteOpen = $state(false);
|
||||||
|
let searchTerm = $state('');
|
||||||
|
let selectedIndex = $state(0);
|
||||||
|
let combinedResults = $state<CombinedResult[]>([]);
|
||||||
|
let searchbar = $state<HTMLInputElement>();
|
||||||
|
|
||||||
|
let isLoading = $state(false);
|
||||||
|
let calculatorResult = $state<string | null>(null);
|
||||||
|
let resultsList = $state<HTMLUListElement>();
|
||||||
|
|
||||||
|
const updateCalculatorState = (hasResult: string | null) => {
|
||||||
|
calculatorResult = hasResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
let keydownHandler: ((e: KeyboardEvent) => void) | null = null;
|
||||||
|
|
||||||
|
// Listen for setting changes
|
||||||
|
$effect(() => {
|
||||||
|
const loadSettings = async () => {
|
||||||
|
const settings = await browser.storage.local.get('plugin.global-search.settings');
|
||||||
|
const pluginSettings = settings['plugin.global-search.settings'] as { searchHotkey?: string } | undefined;
|
||||||
|
if (pluginSettings?.searchHotkey) {
|
||||||
|
currentSearchHotkey = pluginSettings.searchHotkey;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadSettings();
|
||||||
|
|
||||||
|
// Listen for storage changes
|
||||||
|
const handleStorageChange = (changes: any, area: string) => {
|
||||||
|
if (area === 'local' && changes['plugin.global-search.settings']) {
|
||||||
|
const newSettings = changes['plugin.global-search.settings'].newValue as { searchHotkey?: string } | undefined;
|
||||||
|
if (newSettings?.searchHotkey) {
|
||||||
|
currentSearchHotkey = newSettings.searchHotkey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
browser.storage.onChanged.addListener(handleStorageChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
browser.storage.onChanged.removeListener(handleStorageChange);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update keydown handler when hotkey changes
|
||||||
|
$effect(() => {
|
||||||
|
if (keydownHandler) {
|
||||||
|
window.removeEventListener('keydown', keydownHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
keydownHandler = (e: KeyboardEvent) => {
|
||||||
|
if (matchesHotkey(e, currentSearchHotkey)) {
|
||||||
|
e.preventDefault();
|
||||||
|
commandPalleteOpen = true;
|
||||||
|
tick().then(() => searchbar?.focus());
|
||||||
|
}
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
commandPalleteOpen = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', keydownHandler);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (keydownHandler) {
|
||||||
|
window.removeEventListener('keydown', keydownHandler);
|
||||||
|
keydownHandler = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const progressHandler = (event: CustomEvent) => {
|
const progressHandler = (event: CustomEvent) => {
|
||||||
const { completed, total, indexing } = event.detail;
|
const { completed, total, indexing } = event.detail;
|
||||||
@@ -49,6 +128,13 @@
|
|||||||
};
|
};
|
||||||
window.addEventListener('dynamic-items-updated', itemsUpdatedHandler);
|
window.addEventListener('dynamic-items-updated', itemsUpdatedHandler);
|
||||||
|
|
||||||
|
setupSearchIndexes();
|
||||||
|
|
||||||
|
// @ts-ignore - Intentionally adding to window
|
||||||
|
window.setCommandPalleteOpen = (open: boolean) => {
|
||||||
|
commandPalleteOpen = open;
|
||||||
|
};
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('indexing-progress', progressHandler as EventListener);
|
window.removeEventListener('indexing-progress', progressHandler as EventListener);
|
||||||
window.removeEventListener('dynamic-items-updated', itemsUpdatedHandler);
|
window.removeEventListener('dynamic-items-updated', itemsUpdatedHandler);
|
||||||
@@ -69,43 +155,6 @@
|
|||||||
|
|
||||||
console.debug(`[Global Search] Indexed ${commands.length} command items and ${dynamicItems.length} dynamic items.`);
|
console.debug(`[Global Search] Indexed ${commands.length} command items and ${dynamicItems.length} dynamic items.`);
|
||||||
}
|
}
|
||||||
let commandPalleteOpen = $state(false);
|
|
||||||
let searchTerm = $state('');
|
|
||||||
let selectedIndex = $state(0);
|
|
||||||
let searchbar = $state<HTMLInputElement>();
|
|
||||||
let combinedResults = $state<CombinedResult[]>([]);
|
|
||||||
let isLoading = $state(false);
|
|
||||||
let calculatorResult = $state<string | null>(null);
|
|
||||||
let resultsList = $state<HTMLUListElement>();
|
|
||||||
|
|
||||||
const updateCalculatorState = (hasResult: string | null) => {
|
|
||||||
calculatorResult = hasResult;
|
|
||||||
};
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
setupSearchIndexes();
|
|
||||||
|
|
||||||
// @ts-ignore - Intentionally adding to window
|
|
||||||
window.setCommandPalleteOpen = (open: boolean) => {
|
|
||||||
commandPalleteOpen = open;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
|
||||||
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
|
|
||||||
e.preventDefault();
|
|
||||||
commandPalleteOpen = true;
|
|
||||||
tick().then(() => searchbar?.focus());
|
|
||||||
}
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
commandPalleteOpen = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('keydown', handleKeyDown);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const performSearch = async () => {
|
const performSearch = async () => {
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
@@ -201,6 +250,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyNav = (e: KeyboardEvent) => {
|
const handleKeyNav = (e: KeyboardEvent) => {
|
||||||
|
// Handle regular navigation
|
||||||
if (e.key === 'ArrowDown') {
|
if (e.key === 'ArrowDown') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
selectNext();
|
selectNext();
|
||||||
@@ -281,11 +331,6 @@
|
|||||||
<span class="ml-4 text-lg truncate">
|
<span class="ml-4 text-lg truncate">
|
||||||
<HighlightedText text={staticItem.text} term={searchTerm} matches={result.matches} />
|
<HighlightedText text={staticItem.text} term={searchTerm} matches={result.matches} />
|
||||||
</span>
|
</span>
|
||||||
{#if staticItem.keybindLabel}
|
|
||||||
<div class="flex-none ml-auto">
|
|
||||||
{@render Shortcut({ text: '', keybind: staticItem.keybindLabel })}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</button>
|
</button>
|
||||||
{:else if result.type === 'dynamic'}
|
{:else if result.type === 'dynamic'}
|
||||||
{@const dynamicItem = item as IndexItem}
|
{@const dynamicItem = item as IndexItem}
|
||||||
@@ -344,38 +389,26 @@
|
|||||||
{#if combinedResults.length > 0 || calculatorResult}
|
{#if combinedResults.length > 0 || calculatorResult}
|
||||||
<div class="flex justify-between items-center h-5 text-sm text-zinc-500 dark:text-zinc-400">
|
<div class="flex justify-between items-center h-5 text-sm text-zinc-500 dark:text-zinc-400">
|
||||||
<div class="flex gap-4 items-center">
|
<div class="flex gap-4 items-center">
|
||||||
{#if !calculatorResult}
|
{@render Shortcut({ text: 'Navigate', keybind: ['↑', '↓']})}
|
||||||
{#if selectedIndex >= 0 && selectedIndex < combinedResults.length}
|
{#if calculatorResult && selectedIndex === 0}
|
||||||
{@const item = combinedResults[selectedIndex].item}
|
{@render Shortcut({ text: 'Copy result', keybind: ['↵']})}
|
||||||
{#if 'keybind' in item && item.keybind}
|
{:else}
|
||||||
{@render Shortcut({ text: 'Shortcut', keybind: [ ...(item?.keybindLabel ?? []) ] })}
|
{@render Shortcut({ text: 'Select', keybind: ['↵']})}
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{#if isIndexing}
|
||||||
<div class="flex gap-4 items-center">
|
<div class="inset-x-0 top-0">
|
||||||
{@render Shortcut({ text: 'Navigate', keybind: ['↑', '↓']})}
|
<div class="absolute right-2 -bottom-4 text-[10px] text-zinc-500 dark:text-zinc-400">
|
||||||
{#if calculatorResult && selectedIndex === 0}
|
Indexing
|
||||||
{@render Shortcut({ text: 'Copy result', keybind: ['↵']})}
|
|
||||||
{:else}
|
|
||||||
{@render Shortcut({ text: 'Select', keybind: ['↵']})}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{#if isIndexing}
|
|
||||||
<div class="inset-x-0 top-0">
|
|
||||||
<div class="absolute right-2 -bottom-4 text-[10px] text-zinc-500 dark:text-zinc-400">
|
|
||||||
Indexing
|
|
||||||
</div>
|
|
||||||
<div class="overflow-hidden h-0.5 bg-zinc-200 dark:bg-zinc-700">
|
|
||||||
<div
|
|
||||||
class="h-full bg-blue-500 transition-all duration-300 ease-out"
|
|
||||||
style="width: {(completedJobs / totalJobs) * 100}%"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
<div class="overflow-hidden h-0.5 bg-zinc-200 dark:bg-zinc-700">
|
||||||
</div>
|
<div
|
||||||
|
class="h-full bg-blue-500 transition-all duration-300 ease-out"
|
||||||
|
style="width: {(completedJobs / totalJobs) * 100}%"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ const staticCommands: StaticCommandItem[] = [
|
|||||||
icon: "\ueb4c",
|
icon: "\ueb4c",
|
||||||
category: "navigation",
|
category: "navigation",
|
||||||
text: "Home",
|
text: "Home",
|
||||||
keybind: ["alt+h"],
|
|
||||||
keybindLabel: ["Alt", "H"],
|
|
||||||
action: () => {
|
action: () => {
|
||||||
window.location.hash = "?page=/home";
|
window.location.hash = "?page=/home";
|
||||||
loadHomePage();
|
loadHomePage();
|
||||||
@@ -35,8 +33,6 @@ const staticCommands: StaticCommandItem[] = [
|
|||||||
icon: "\uebfd",
|
icon: "\uebfd",
|
||||||
category: "navigation",
|
category: "navigation",
|
||||||
text: "Direct Messages",
|
text: "Direct Messages",
|
||||||
keybind: ["alt+m"],
|
|
||||||
keybindLabel: ["Alt", "M"],
|
|
||||||
action: () => {
|
action: () => {
|
||||||
window.location.hash = "?page=/messages";
|
window.location.hash = "?page=/messages";
|
||||||
},
|
},
|
||||||
@@ -47,8 +43,6 @@ const staticCommands: StaticCommandItem[] = [
|
|||||||
icon: "\ue9cd",
|
icon: "\ue9cd",
|
||||||
category: "navigation",
|
category: "navigation",
|
||||||
text: "Timetable",
|
text: "Timetable",
|
||||||
keybind: ["alt+t"],
|
|
||||||
keybindLabel: ["Alt", "T"],
|
|
||||||
action: () => {
|
action: () => {
|
||||||
window.location.hash = "?page=/timetable";
|
window.location.hash = "?page=/timetable";
|
||||||
},
|
},
|
||||||
@@ -59,10 +53,8 @@ const staticCommands: StaticCommandItem[] = [
|
|||||||
icon: "\ueac3",
|
icon: "\ueac3",
|
||||||
category: "navigation",
|
category: "navigation",
|
||||||
text: "Assessments",
|
text: "Assessments",
|
||||||
keybind: ["alt+a"],
|
|
||||||
keybindLabel: ["Alt", "A"],
|
|
||||||
action: () => {
|
action: () => {
|
||||||
window.location.hash = "?page=/assessments";
|
window.location.hash = "?page=/assessments/upcoming";
|
||||||
},
|
},
|
||||||
priority: 4,
|
priority: 4,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
buttonSetting,
|
buttonSetting,
|
||||||
defineSettings,
|
defineSettings,
|
||||||
Setting,
|
Setting,
|
||||||
stringSetting,
|
hotkeySetting,
|
||||||
} from "@/plugins/core/settingsHelpers";
|
} from "@/plugins/core/settingsHelpers";
|
||||||
import styles from "./styles.css?inline";
|
import styles from "./styles.css?inline";
|
||||||
import { waitForElm } from "@/seqta/utils/waitForElm";
|
import { waitForElm } from "@/seqta/utils/waitForElm";
|
||||||
@@ -14,11 +14,17 @@ import { initVectorSearch } from "../search/vector/vectorSearch";
|
|||||||
import { cleanupSearchBar, mountSearchBar } from "./mountSearchBar";
|
import { cleanupSearchBar, mountSearchBar } from "./mountSearchBar";
|
||||||
import { IndexedDbManager } from "embeddia";
|
import { IndexedDbManager } from "embeddia";
|
||||||
|
|
||||||
|
// Platform-aware default hotkey
|
||||||
|
const getDefaultHotkey = () => {
|
||||||
|
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
||||||
|
return isMac ? "cmd+k" : "ctrl+k";
|
||||||
|
};
|
||||||
|
|
||||||
const settings = defineSettings({
|
const settings = defineSettings({
|
||||||
searchHotkey: stringSetting({
|
searchHotkey: hotkeySetting({
|
||||||
default: "ctrl+k",
|
default: getDefaultHotkey(),
|
||||||
title: "Search Hotkey",
|
title: "Search Hotkey",
|
||||||
description: "Keyboard shortcut to open the search (cmd on Mac)",
|
description: "Keyboard shortcut to open the search",
|
||||||
}),
|
}),
|
||||||
showRecentFirst: booleanSetting({
|
showRecentFirst: booleanSetting({
|
||||||
default: true,
|
default: true,
|
||||||
@@ -99,10 +105,15 @@ const globalSearchPlugin: Plugin<typeof settings> = {
|
|||||||
run: async (api) => {
|
run: async (api) => {
|
||||||
const appRef = { current: null };
|
const appRef = { current: null };
|
||||||
|
|
||||||
await IndexedDbManager.create("embeddiaDB", "embeddiaObjectStore", {
|
try {
|
||||||
primaryKey: "id",
|
await IndexedDbManager.create("embeddiaDB", "embeddiaObjectStore", {
|
||||||
autoIncrement: false,
|
primaryKey: "id",
|
||||||
});
|
autoIncrement: false,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to create IndexedDB:", error);
|
||||||
|
// Continue execution - the search might still work without persistence
|
||||||
|
}
|
||||||
|
|
||||||
initVectorSearch();
|
initVectorSearch();
|
||||||
|
|
||||||
@@ -117,8 +128,8 @@ const globalSearchPlugin: Plugin<typeof settings> = {
|
|||||||
if (title) {
|
if (title) {
|
||||||
mountSearchBar(title, api, appRef);
|
mountSearchBar(title, api, appRef);
|
||||||
} else {
|
} else {
|
||||||
await waitForElm("#title", true, 100, 60);
|
const titleElement = await waitForElm("#title", true, 100, 60);
|
||||||
mountSearchBar(document.querySelector("#title") as Element, api, appRef);
|
mountSearchBar(titleElement, api, appRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import renderSvelte from "@/interface/main";
|
|||||||
import SearchBar from "../components/SearchBar.svelte";
|
import SearchBar from "../components/SearchBar.svelte";
|
||||||
import { unmount } from "svelte";
|
import { unmount } from "svelte";
|
||||||
import { VectorWorkerManager } from "../indexing/worker/vectorWorkerManager";
|
import { VectorWorkerManager } from "../indexing/worker/vectorWorkerManager";
|
||||||
|
import { formatHotkeyForDisplay, isValidHotkey } from "../utils/hotkeyUtils";
|
||||||
|
import browser from "webextension-polyfill";
|
||||||
|
|
||||||
export function mountSearchBar(
|
export function mountSearchBar(
|
||||||
titleElement: Element,
|
titleElement: Element,
|
||||||
@@ -12,19 +14,41 @@ export function mountSearchBar(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback to default hotkey if the current one is invalid
|
||||||
|
let currentHotkey = isValidHotkey(api.settings.searchHotkey) ? api.settings.searchHotkey : "ctrl+k";
|
||||||
|
let hotkeyDisplay = formatHotkeyForDisplay(currentHotkey);
|
||||||
|
|
||||||
const searchButton = document.createElement("div");
|
const searchButton = document.createElement("div");
|
||||||
searchButton.className = "search-trigger";
|
searchButton.className = "search-trigger";
|
||||||
searchButton.innerHTML = /* html */ `
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<circle cx="11" cy="11" r="8"></circle>
|
|
||||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
|
||||||
</svg>
|
|
||||||
<p>Quick search...</p>
|
|
||||||
<span style="margin-left: auto; display: flex; align-items: center; color: #777; font-size: 12px;">⌘K</span>
|
|
||||||
`;
|
|
||||||
|
|
||||||
|
const updateSearchButtonDisplay = () => {
|
||||||
|
searchButton.innerHTML = /* html */ `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||||
|
</svg>
|
||||||
|
<p>Quick search...</p>
|
||||||
|
<span style="margin-left: auto; display: flex; align-items: center; color: #777; font-size: 12px;">${hotkeyDisplay}</span>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
updateSearchButtonDisplay();
|
||||||
titleElement.appendChild(searchButton);
|
titleElement.appendChild(searchButton);
|
||||||
|
|
||||||
|
// Listen for hotkey setting changes
|
||||||
|
const handleStorageChange = (changes: any, area: string) => {
|
||||||
|
if (area === 'local' && changes['plugin.global-search.settings']) {
|
||||||
|
const newSettings = changes['plugin.global-search.settings'].newValue as { searchHotkey?: string } | undefined;
|
||||||
|
if (newSettings?.searchHotkey && isValidHotkey(newSettings.searchHotkey)) {
|
||||||
|
currentHotkey = newSettings.searchHotkey;
|
||||||
|
hotkeyDisplay = formatHotkeyForDisplay(currentHotkey);
|
||||||
|
updateSearchButtonDisplay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
browser.storage.onChanged.addListener(handleStorageChange);
|
||||||
|
|
||||||
const searchRoot = document.createElement("div");
|
const searchRoot = document.createElement("div");
|
||||||
document.body.appendChild(searchRoot);
|
document.body.appendChild(searchRoot);
|
||||||
const searchRootShadow = searchRoot.attachShadow({ mode: "open" });
|
const searchRootShadow = searchRoot.attachShadow({ mode: "open" });
|
||||||
@@ -38,6 +62,7 @@ export function mountSearchBar(
|
|||||||
appRef.current = renderSvelte(SearchBar, searchRootShadow, {
|
appRef.current = renderSvelte(SearchBar, searchRootShadow, {
|
||||||
transparencyEffects: api.settings.transparencyEffects ? true : false,
|
transparencyEffects: api.settings.transparencyEffects ? true : false,
|
||||||
showRecentFirst: api.settings.showRecentFirst,
|
showRecentFirst: api.settings.showRecentFirst,
|
||||||
|
searchHotkey: currentHotkey,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error rendering Svelte component:", error);
|
console.error("Error rendering Svelte component:", error);
|
||||||
@@ -45,12 +70,30 @@ export function mountSearchBar(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function cleanupSearchBar(appRef: { current: any }) {
|
export function cleanupSearchBar(appRef: { current: any }) {
|
||||||
const searchButton = document.querySelector(".search-trigger");
|
if (appRef.current) {
|
||||||
const searchRoot = document.querySelector(".global-search-root");
|
try {
|
||||||
if (searchButton) searchButton.remove();
|
unmount(appRef.current);
|
||||||
if (searchRoot) searchRoot.remove();
|
appRef.current = null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error unmounting Svelte component:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Clean up workers
|
// Remove search trigger button
|
||||||
|
const searchTrigger = document.querySelector(".search-trigger");
|
||||||
|
if (searchTrigger) {
|
||||||
|
searchTrigger.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove search root
|
||||||
|
const searchRoot = document.querySelector("div[data-search-root]");
|
||||||
|
if (searchRoot) {
|
||||||
|
searchRoot.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up vector worker
|
||||||
VectorWorkerManager.getInstance().terminate();
|
VectorWorkerManager.getInstance().terminate();
|
||||||
unmount(appRef.current);
|
|
||||||
|
// Remove storage listener
|
||||||
|
browser.storage.onChanged.removeListener(() => {});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
export interface ParsedHotkey {
|
||||||
|
ctrl: boolean;
|
||||||
|
meta: boolean;
|
||||||
|
alt: boolean;
|
||||||
|
shift: boolean;
|
||||||
|
key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseHotkey(hotkeyString: string): ParsedHotkey {
|
||||||
|
const parts = hotkeyString.toLowerCase().split('+').map(part => part.trim()).filter(part => part.length > 0);
|
||||||
|
|
||||||
|
const parsed: ParsedHotkey = {
|
||||||
|
ctrl: false,
|
||||||
|
meta: false,
|
||||||
|
alt: false,
|
||||||
|
shift: false,
|
||||||
|
key: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const part of parts) {
|
||||||
|
switch (part) {
|
||||||
|
case 'ctrl':
|
||||||
|
case 'control':
|
||||||
|
parsed.ctrl = true;
|
||||||
|
break;
|
||||||
|
case 'cmd':
|
||||||
|
case 'meta':
|
||||||
|
case 'command':
|
||||||
|
parsed.meta = true;
|
||||||
|
break;
|
||||||
|
case 'alt':
|
||||||
|
case 'option':
|
||||||
|
parsed.alt = true;
|
||||||
|
break;
|
||||||
|
case 'shift':
|
||||||
|
parsed.shift = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// This should be the key - take the last non-modifier as the key
|
||||||
|
if (part.length > 0) {
|
||||||
|
parsed.key = part;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatHotkeyForDisplay(hotkeyString: string): string {
|
||||||
|
try {
|
||||||
|
const parsed = parseHotkey(hotkeyString);
|
||||||
|
const parts: string[] = [];
|
||||||
|
|
||||||
|
// Detect platform
|
||||||
|
const isMac = (navigator.platform.toUpperCase().indexOf('MAC') >= 0);
|
||||||
|
|
||||||
|
if (parsed.ctrl) {
|
||||||
|
parts.push('Ctrl');
|
||||||
|
}
|
||||||
|
if (parsed.meta) {
|
||||||
|
parts.push('⌘');
|
||||||
|
}
|
||||||
|
if (parsed.alt) {
|
||||||
|
parts.push(isMac ? '⌥' : 'Alt');
|
||||||
|
}
|
||||||
|
if (parsed.shift) {
|
||||||
|
parts.push(isMac ? '⇧' : 'Shift');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.key) {
|
||||||
|
parts.push(parsed.key.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join(isMac ? ' ' : '+');
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Invalid hotkey string:', hotkeyString);
|
||||||
|
return hotkeyString; // Fallback to original string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function matchesHotkey(event: KeyboardEvent, hotkeyString: string): boolean {
|
||||||
|
try {
|
||||||
|
const parsed = parseHotkey(hotkeyString);
|
||||||
|
|
||||||
|
// If no key is specified, don't match anything
|
||||||
|
if (!parsed.key) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check modifiers
|
||||||
|
if (parsed.ctrl && !event.ctrlKey) return false;
|
||||||
|
if (parsed.meta && !event.metaKey) return false;
|
||||||
|
if (parsed.alt && !event.altKey) return false;
|
||||||
|
if (parsed.shift && !event.shiftKey) return false;
|
||||||
|
|
||||||
|
// Check if we have extra modifiers that shouldn't be there
|
||||||
|
if (!parsed.ctrl && event.ctrlKey) return false;
|
||||||
|
if (!parsed.meta && event.metaKey) return false;
|
||||||
|
if (!parsed.alt && event.altKey) return false;
|
||||||
|
if (!parsed.shift && event.shiftKey) return false;
|
||||||
|
|
||||||
|
// Check the key
|
||||||
|
return event.key.toLowerCase() === parsed.key.toLowerCase();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error matching hotkey:', hotkeyString, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isValidHotkey(hotkeyString: string): boolean {
|
||||||
|
try {
|
||||||
|
const parsed = parseHotkey(hotkeyString);
|
||||||
|
return parsed.key.length > 0;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import type {
|
|||||||
SelectSetting,
|
SelectSetting,
|
||||||
StringSetting,
|
StringSetting,
|
||||||
ButtonSetting,
|
ButtonSetting,
|
||||||
|
HotkeySetting,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { createPluginAPI } from "./createAPI";
|
import { createPluginAPI } from "./createAPI";
|
||||||
import browser from "webextension-polyfill";
|
import browser from "webextension-polyfill";
|
||||||
@@ -193,7 +194,8 @@ export class PluginManager {
|
|||||||
id: string;
|
id: string;
|
||||||
options: Array<{ value: string; label: string }>;
|
options: Array<{ value: string; label: string }>;
|
||||||
})
|
})
|
||||||
| (Omit<ButtonSetting, "type"> & { type: "button"; id: string; trigger?: () => void | Promise<void> });
|
| (Omit<ButtonSetting, "type"> & { type: "button"; id: string; trigger?: () => void | Promise<void> })
|
||||||
|
| (Omit<HotkeySetting, "type"> & { type: "hotkey"; id: string });
|
||||||
};
|
};
|
||||||
}> {
|
}> {
|
||||||
return Array.from(this.plugins.entries()).map(([id, plugin]) => {
|
return Array.from(this.plugins.entries()).map(([id, plugin]) => {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type {
|
|||||||
NumberSetting,
|
NumberSetting,
|
||||||
SelectSetting,
|
SelectSetting,
|
||||||
StringSetting,
|
StringSetting,
|
||||||
|
HotkeySetting,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export function numberSetting(
|
export function numberSetting(
|
||||||
@@ -51,6 +52,15 @@ export function buttonSetting(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function hotkeySetting(
|
||||||
|
options: Omit<HotkeySetting, "type">,
|
||||||
|
): HotkeySetting {
|
||||||
|
return {
|
||||||
|
type: "hotkey",
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function defineSettings<T extends Record<string, any>>(settings: T): T {
|
export function defineSettings<T extends Record<string, any>>(settings: T): T {
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,12 +41,20 @@ export interface ButtonSetting {
|
|||||||
trigger?: () => void | Promise<void>;
|
trigger?: () => void | Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HotkeySetting {
|
||||||
|
type: "hotkey";
|
||||||
|
default: string;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type PluginSetting =
|
export type PluginSetting =
|
||||||
| BooleanSetting
|
| BooleanSetting
|
||||||
| StringSetting
|
| StringSetting
|
||||||
| NumberSetting
|
| NumberSetting
|
||||||
| SelectSetting<string>
|
| SelectSetting<string>
|
||||||
| ButtonSetting;
|
| ButtonSetting
|
||||||
|
| HotkeySetting;
|
||||||
|
|
||||||
export type PluginSettings = {
|
export type PluginSettings = {
|
||||||
[key: string]: PluginSetting;
|
[key: string]: PluginSetting;
|
||||||
@@ -61,7 +69,9 @@ export type SettingValue<T extends PluginSetting> = T extends BooleanSetting
|
|||||||
? number
|
? number
|
||||||
: T extends SelectSetting<infer O>
|
: T extends SelectSetting<infer O>
|
||||||
? O
|
? O
|
||||||
: never;
|
: T extends HotkeySetting
|
||||||
|
? string
|
||||||
|
: never;
|
||||||
|
|
||||||
export type SettingsAPI<T extends PluginSettings> = {
|
export type SettingsAPI<T extends PluginSettings> = {
|
||||||
[K in keyof T]: SettingValue<T[K]>;
|
[K in keyof T]: SettingValue<T[K]>;
|
||||||
|
|||||||
Reference in New Issue
Block a user