feat: improved hotkey support and controls

This commit is contained in:
SethBurkart123
2025-05-25 18:15:06 +10:00
parent f66340cb63
commit 991f80d316
10 changed files with 570 additions and 112 deletions
+229
View File
@@ -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>
+13 -3
View File
@@ -3,6 +3,7 @@
import Button from "../../components/Button.svelte"
import Slider from "../../components/Slider.svelte"
import Select from "@/interface/components/Select.svelte"
import HotkeyInput from "@/interface/components/HotkeyInput.svelte"
import browser from "webextension-polyfill"
@@ -12,7 +13,7 @@
import hideSensitiveContent from "@/seqta/ui/dev/hideSensitiveContent"
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
type SettingType =
@@ -27,6 +28,10 @@
(Omit<ButtonSetting, 'type'> & {
type: 'button',
id: string
}) |
(Omit<HotkeySetting, 'type'> & {
type: 'hotkey',
id: string
});
interface Plugin {
@@ -195,10 +200,10 @@
{#if (plugin as any).disableToggle}
<div class="flex justify-between items-center px-4 py-3">
<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}
{#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
</span>
{/if}
@@ -258,6 +263,11 @@
onClick={() => setting.trigger?.()}
text={setting.title}
/>
{:else if setting.type === 'hotkey'}
<HotkeyInput
value={pluginSettingsValues[plugin.pluginId]?.[key] ?? setting.default}
onChange={(value) => updatePluginSetting(plugin.pluginId, key, value)}
/>
{/if}
</div>
</div>