diff --git a/src/interface/components/HotkeyInput.svelte b/src/interface/components/HotkeyInput.svelte new file mode 100644 index 00000000..f575259c --- /dev/null +++ b/src/interface/components/HotkeyInput.svelte @@ -0,0 +1,229 @@ + + +
Quick search...
- ⌘K - `; + + const updateSearchButtonDisplay = () => { + searchButton.innerHTML = /* html */ ` + +Quick search...
+ ${hotkeyDisplay} + `; + }; + updateSearchButtonDisplay(); 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"); document.body.appendChild(searchRoot); const searchRootShadow = searchRoot.attachShadow({ mode: "open" }); @@ -38,6 +62,7 @@ export function mountSearchBar( appRef.current = renderSvelte(SearchBar, searchRootShadow, { transparencyEffects: api.settings.transparencyEffects ? true : false, showRecentFirst: api.settings.showRecentFirst, + searchHotkey: currentHotkey, }); } catch (error) { console.error("Error rendering Svelte component:", error); @@ -45,12 +70,30 @@ export function mountSearchBar( } export function cleanupSearchBar(appRef: { current: any }) { - const searchButton = document.querySelector(".search-trigger"); - const searchRoot = document.querySelector(".global-search-root"); - if (searchButton) searchButton.remove(); - if (searchRoot) searchRoot.remove(); + if (appRef.current) { + try { + unmount(appRef.current); + 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(); - unmount(appRef.current); + + // Remove storage listener + browser.storage.onChanged.removeListener(() => {}); } diff --git a/src/plugins/built-in/globalSearch/src/utils/hotkeyUtils.ts b/src/plugins/built-in/globalSearch/src/utils/hotkeyUtils.ts new file mode 100644 index 00000000..653b8add --- /dev/null +++ b/src/plugins/built-in/globalSearch/src/utils/hotkeyUtils.ts @@ -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; + } +} \ No newline at end of file diff --git a/src/plugins/core/manager.ts b/src/plugins/core/manager.ts index f973a08c..ff6621db 100644 --- a/src/plugins/core/manager.ts +++ b/src/plugins/core/manager.ts @@ -6,6 +6,7 @@ import type { SelectSetting, StringSetting, ButtonSetting, + HotkeySetting, } from "./types"; import { createPluginAPI } from "./createAPI"; import browser from "webextension-polyfill"; @@ -193,7 +194,8 @@ export class PluginManager { id: string; options: Array<{ value: string; label: string }>; }) - | (Omit