From 711f7cf57c1c987676923977fc65920dc8ee7042 Mon Sep 17 00:00:00 2001 From: BiggerRain <15911122312@163.COM> Date: Wed, 15 Jan 2025 10:33:51 +0800 Subject: [PATCH] feat: add hotkey hook (#101) * feat: add hotkey hook * chore: debug shortcut keys * chore: shortcut key echo * chore: add number key * chore: remove log * build: build config --- src-tauri/src/lib.rs | 16 +- src-tauri/tauri.conf.json | 2 +- src/components/AppAI/DropdownList.tsx | 11 +- src/components/SearchChat/DropdownList.tsx | 2 +- src/components/SearchChat/Search.tsx | 7 +- src/components/SearchChat/index.tsx | 11 +- src/components/Settings/GeneralSettings.tsx | 293 ++++---------------- src/components/Settings/ShortcutItem.tsx | 79 ++++++ src/components/Settings/shortcut.ts | 1 + src/hooks/useShortcutEditor.ts | 111 ++++++++ src/pages/app/index.tsx | 7 +- src/utils/index.ts | 4 +- src/utils/keyboardUtils.ts | 101 +++++++ 13 files changed, 373 insertions(+), 272 deletions(-) create mode 100644 src/components/Settings/ShortcutItem.tsx create mode 100644 src/components/Settings/shortcut.ts create mode 100644 src/hooks/useShortcutEditor.ts create mode 100644 src/utils/keyboardUtils.ts diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 1a9a161..f1212a7 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -205,6 +205,18 @@ fn change_shortcut( let main_window = app.get_webview_window("main").unwrap(); + if key.trim().is_empty() { + let path = app.path().app_config_dir().unwrap(); + if !path.exists() { + create_dir(&path).unwrap(); + } + + let file_path = path.join("shortcut.txt"); + let mut file = File::create(file_path).unwrap(); + file.write_all(b"").unwrap(); + return Ok(()); + } + let shortcut: Shortcut = key .parse() .map_err(|_| "The format of the shortcut key is incorrect".to_owned())?; @@ -364,12 +376,12 @@ fn enable_tray(app: &mut tauri::App) { let settings_i = MenuItem::with_id(app, "settings", "Settings...", true, None::<&str>).unwrap(); let open_i = MenuItem::with_id(app, "open", "Open Coco", true, None::<&str>).unwrap(); let about_i = MenuItem::with_id(app, "about", "About Coco", true, None::<&str>).unwrap(); - let hide_i = MenuItem::with_id(app, "hide", "Hide Coco", true, None::<&str>).unwrap(); + // let hide_i = MenuItem::with_id(app, "hide", "Hide Coco", true, None::<&str>).unwrap(); let menu = MenuBuilder::new(app) .item(&open_i) .separator() - .item(&hide_i) + // .item(&hide_i) .item(&about_i) .item(&settings_i) .separator() diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index e6997bf..c37aafa 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -22,7 +22,7 @@ "maximizable": false, "skipTaskbar": true, "resizable": false, - "shadow": false, + "shadow": true, "transparent": true, "fullscreen": false, "center": false, diff --git a/src/components/AppAI/DropdownList.tsx b/src/components/AppAI/DropdownList.tsx index a93b237..2b82936 100644 --- a/src/components/AppAI/DropdownList.tsx +++ b/src/components/AppAI/DropdownList.tsx @@ -1,7 +1,8 @@ import { useEffect, useRef, useState } from "react"; import { CircleAlert, Bolt, X } from "lucide-react"; - import { isTauri } from "@tauri-apps/api/core"; +import { open } from "@tauri-apps/plugin-shell"; + import { useAppStore } from "@/stores/appStore"; interface DropdownListProps { @@ -24,7 +25,6 @@ function DropdownList({ selected, suggests, IsError }: DropdownListProps) { if (!url) return; try { if (isTauri()) { - const { open } = await import("@tauri-apps/plugin-shell"); await open(url); console.log("URL opened in default browser"); } @@ -107,10 +107,11 @@ function DropdownList({ selected, suggests, IsError }: DropdownListProps) { }, [selectedItem]); function getIcon(_source: any) { - const name = _source?.source?.name || "" - const result = connector_data.find((item: any) => item._source.category === name); + const name = _source?.source?.name || ""; + const result = connector_data.find( + (item: any) => item._source.category === name + ); const icons = result?._source?.assets?.icons || {}; - console.log(11111, icons,name, _source.icon, icons[_source.icon]) return icons[_source.icon] || _source.icon; } diff --git a/src/components/SearchChat/DropdownList.tsx b/src/components/SearchChat/DropdownList.tsx index 061c2a5..a1f3815 100644 --- a/src/components/SearchChat/DropdownList.tsx +++ b/src/components/SearchChat/DropdownList.tsx @@ -1,5 +1,6 @@ import { useEffect, useRef, useState } from "react"; import { isTauri } from "@tauri-apps/api/core"; +import { open } from "@tauri-apps/plugin-shell"; interface DropdownListProps { selected: (item: any) => void; @@ -17,7 +18,6 @@ function DropdownList({ selected, suggests }: DropdownListProps) { if (!url) return; try { if (isTauri()) { - const { open } = await import("@tauri-apps/plugin-shell"); await open(url); console.log("URL opened in default browser"); } diff --git a/src/components/SearchChat/Search.tsx b/src/components/SearchChat/Search.tsx index bfe6dc1..d93ce50 100644 --- a/src/components/SearchChat/Search.tsx +++ b/src/components/SearchChat/Search.tsx @@ -1,5 +1,7 @@ import { useEffect, useState, useCallback, useRef } from "react"; import { isTauri } from "@tauri-apps/api/core"; +import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; +import { LogicalSize } from "@tauri-apps/api/dpi"; import DropdownList from "./DropdownList"; import { Footer } from "./Footer"; @@ -32,11 +34,6 @@ function Search({ isTransitioned, isChatMode, input }: SearchProps) { if (!element) return; const resizeObserver = new ResizeObserver(async (entries) => { - const { getCurrentWebviewWindow } = await import( - "@tauri-apps/api/webviewWindow" - ); - const { LogicalSize } = await import("@tauri-apps/api/dpi"); - for (let entry of entries) { let newHeight = entry.contentRect.height; console.log("Height updated:", newHeight); diff --git a/src/components/SearchChat/index.tsx b/src/components/SearchChat/index.tsx index bfa2d5d..79cf51e 100644 --- a/src/components/SearchChat/index.tsx +++ b/src/components/SearchChat/index.tsx @@ -1,5 +1,7 @@ import { useEffect, useState, useRef } from "react"; import { isTauri } from "@tauri-apps/api/core"; +import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; +import { LogicalSize } from "@tauri-apps/api/dpi"; // import { Window, LogicalPosition } from "@tauri-apps/api/window"; // import { currentMonitor } from "@tauri-apps/plugin-window"; @@ -62,10 +64,6 @@ export default function SearchChat() { async function setWindowSize() { if (isTauri() && !isTransitioned) { - const { getCurrentWebviewWindow } = await import( - "@tauri-apps/api/webviewWindow" - ); - const { LogicalSize } = await import("@tauri-apps/api/dpi"); await getCurrentWebviewWindow()?.setSize(new LogicalSize(680, 90)); } } @@ -89,11 +87,6 @@ export default function SearchChat() { setInput(value); if (isChatMode) { if (isTauri()) { - const { getCurrentWebviewWindow } = await import( - "@tauri-apps/api/webviewWindow" - ); - const { LogicalSize } = await import("@tauri-apps/api/dpi"); - await getCurrentWebviewWindow()?.setSize(new LogicalSize(680, 596)); } setIsTransitioned(true); diff --git a/src/components/Settings/GeneralSettings.tsx b/src/components/Settings/GeneralSettings.tsx index 16a859b..67bdf86 100644 --- a/src/components/Settings/GeneralSettings.tsx +++ b/src/components/Settings/GeneralSettings.tsx @@ -1,5 +1,13 @@ import { useState, useEffect } from "react"; -import { Command, Monitor, Palette, Moon, Sun, Power, Tags, CircleX } from "lucide-react"; +import { + Command, + Monitor, + Palette, + Moon, + Sun, + Power, + Tags, +} from "lucide-react"; import { isTauri, invoke } from "@tauri-apps/api/core"; import { isEnabled, @@ -7,30 +15,18 @@ import { } from "@tauri-apps/plugin-autostart"; import SettingsItem from "./SettingsItem"; -import SettingsSelect from "./SettingsSelect"; import SettingsToggle from "./SettingsToggle"; +import { ShortcutItem } from "./ShortcutItem"; +import { Shortcut } from "./shortcut"; +import { useShortcutEditor } from "@/hooks/useShortcutEditor"; import { ThemeOption } from "./index2"; -import { type Hotkey } from "../../utils/tauri"; -import { useTheme } from "../../contexts/ThemeContext"; -import { useAppStore } from '../../stores/appStore'; - -const RESERVED_SHORTCUTS = [ - "ctrl+c", - "ctrl+v", - "ctrl+x", - "ctrl+a", - "ctrl+z", - "ctrl+y", - "ctrl+s", -]; +import { useAppStore } from "@/stores/appStore"; export default function GeneralSettings() { - const { theme, changeTheme } = useTheme(); - const [launchAtLogin, setLaunchAtLogin] = useState(true); - const showTooltip = useAppStore(state => state.showTooltip); - const setShowTooltip = useAppStore(state => state.setShowTooltip); + const showTooltip = useAppStore((state) => state.showTooltip); + const setShowTooltip = useAppStore((state) => state.setShowTooltip); useEffect(() => { const fetchAutoStartStatus = async () => { @@ -71,218 +67,48 @@ export default function GeneralSettings() { setLaunchAtLogin(false); }; - const [errorInfo, setErrorInfo] = useState(""); - const [listening, setListening] = useState(false); - const [pressedKeys, setPressedKeys] = useState>(new Set()); - const [hotkey, setHotkey] = useState(null); - - const parseHotkey = (hotkeyString: string): Hotkey | null => { - if (!hotkeyString || hotkeyString === "None") return null; - - const hotkey: Hotkey = { - meta: false, - ctrl: false, - alt: false, - shift: false, - code: "", - }; - - const parts = hotkeyString - .split("+") - .map( - (item: any) => - item.charAt(0).toUpperCase() + item.slice(1).toLowerCase() - ); - - parts.forEach((part) => { - if (part === "⌘") hotkey.meta = true; // "⌘" -> meta - else if (part === "Super") hotkey.meta = true; // "Win" -> meta - else if (part === "Ctrl") hotkey.ctrl = true; // "Ctrl" -> ctrl - else if (part === "Alt") hotkey.alt = true; // "Alt" -> alt - else if (part === "Shift") hotkey.shift = true; // "Shift" -> shift - else if (part === "Space") - hotkey.code = "Space"; // "Space" -> code = "Space" - else if (part.startsWith("Key")) - hotkey.code = `Key${part.slice(3)}`; // "Key" -> "KeyA" - else if (part.startsWith("Digit")) - hotkey.code = `Digit${part.slice(5)}`; // "Digit" -> "Digit1" - else hotkey.code = `Key${part}`; - }); - - return hotkey; - }; - async function getCurrentShortcut() { const res: any = await invoke("get_current_shortcut"); - const currentHotkey = parseHotkey(res); - setHotkey(currentHotkey); + setShortcut(res?.split("+")); } useEffect(() => { getCurrentShortcut(); }, []); - const handleKeyDown = (e: KeyboardEvent) => { - e.preventDefault(); - e.stopPropagation(); - - if ( - e.code === "MetaLeft" || e.code === "MetaRight" || - e.code === "ControlLeft" || e.code === "ControlRight" || - e.code === "AltLeft" || e.code === "AltRight" || - e.code === "ShiftLeft" || e.code === "ShiftRight" || - e.code.startsWith("Key") || - e.code.startsWith("Digit") || - e.code === "Space" - ) { - setPressedKeys((prev) => new Set(prev).add(e.code)); - } - }; + const [shortcut, setShortcut] = useState([]); - const handleKeyUp = (e: KeyboardEvent) => { - e.preventDefault(); - e.stopPropagation(); - - setPressedKeys((prev) => { - const next = new Set(prev); - next.delete(e.code); - return next; - }); - }; + const { isEditing, currentKeys, startEditing, saveShortcut, cancelEditing } = + useShortcutEditor(shortcut, setShortcut); useEffect(() => { - if (listening) { - window.addEventListener("keydown", handleKeyDown); - window.addEventListener("keyup", handleKeyUp); - } else { - window.removeEventListener("keydown", handleKeyDown); - window.removeEventListener("keyup", handleKeyUp); - } - - return () => { - window.removeEventListener("keydown", handleKeyDown); - window.removeEventListener("keyup", handleKeyUp); - }; - }, [listening]); - - const isReservedShortcut = (hotkey: Hotkey): boolean => { - const hotkeyStr = formatHotkeyToString(hotkey).toLowerCase(); - return RESERVED_SHORTCUTS.some(reserved => { - const normalizedReserved = reserved.replace(/\s+/g, '').toLowerCase(); - return hotkeyStr === normalizedReserved; + if (shortcut.length === 0) return; + invoke("change_shortcut", { key: shortcut?.join("+") }).catch((err) => { + console.error("Failed to save hotkey:", err); + startEditing(); }); - }; - - const formatHotkeyToString = (hotkey: Hotkey): string => { - const parts: string[] = []; - if (hotkey.ctrl) parts.push('ctrl'); - if (hotkey.alt) parts.push('alt'); - if (hotkey.shift) parts.push('shift'); - if (hotkey.meta) parts.push('meta'); - - if (hotkey.code.startsWith('Key')) { - parts.push(hotkey.code.slice(3).toLowerCase()); - } else if (hotkey.code.startsWith('Digit')) { - parts.push(hotkey.code.slice(5)); - } else if (hotkey.code === 'Space') { - parts.push('space'); - } - - return parts.join('+'); - }; - - useEffect(() => { - if (pressedKeys.size === 0) return; + }, [shortcut]); - const currentHotkey: Hotkey = { - meta: pressedKeys.has("MetaLeft") || pressedKeys.has("MetaRight"), - ctrl: pressedKeys.has("ControlLeft") || pressedKeys.has("ControlRight"), - shift: pressedKeys.has("ShiftLeft") || pressedKeys.has("ShiftRight"), - alt: pressedKeys.has("AltLeft") || pressedKeys.has("AltRight"), - code: - Array.from(pressedKeys).find( - (key) => - key.startsWith("Key") || key.startsWith("Digit") || key === "Space" - ) ?? "", - }; - - const hasModifier = currentHotkey.meta || currentHotkey.ctrl || currentHotkey.alt || currentHotkey.shift; - const hasMainKey = currentHotkey.code !== ""; - - if (hasModifier && hasMainKey) { - if (isReservedShortcut(currentHotkey)) { - setErrorInfo("This shortcut is reserved by system"); - setPressedKeys(new Set()); - setListening(false); - return; - } - - const currentHotkeyStr = formatHotkeyToString(currentHotkey); - const existingHotkeyStr = hotkey ? formatHotkeyToString(hotkey) : ''; - - if (currentHotkeyStr === existingHotkeyStr) { - setErrorInfo("Same as current shortcut"); - } - - setHotkey(currentHotkey); - setPressedKeys(new Set()); - setListening(false); - } - }, [pressedKeys]); - - const convertShortcut = (shortcut: string): string => { - return shortcut - .replace(/⌘/g, "command") - .replace(/⇧/g, "shift") - .replace(/⎇/g, "alt") - .replace(/control/i, "ctrl") - .toLowerCase() - .replace(/\s+/g, "") - .trim(); - }; - - const formatHotkey = (hotkey: Hotkey | null): string => { - if (!hotkey) return "Press shortcut"; - const parts: string[] = []; - - if (hotkey.meta) parts.push(navigator.platform.includes('Mac') ? "⌘" : "Win"); - if (hotkey.ctrl) parts.push("Ctrl"); - if (hotkey.alt) parts.push("Alt"); - if (hotkey.shift) parts.push("Shift"); - - if (hotkey.code === "Space") { - parts.push("Space"); - } else if (hotkey.code.startsWith("Key")) { - parts.push(hotkey.code.slice(3)); - } else if (hotkey.code.startsWith("Digit")) { - parts.push(hotkey.code.slice(5)); - } - - const shortcut = parts.join("+"); - - if (parts.length >= 2) { - invoke("change_shortcut", { key: convertShortcut(shortcut) }).catch((err) => { - console.error("Failed to save hotkey:", err); - setErrorInfo("Failed to save shortcut"); - }); - } - - return parts.join(" + "); + const onEditShortcut = async () => { + startEditing(); + // + invoke("change_shortcut", { key: "" }).catch((err) => { + console.error("Failed to save hotkey:", err); + startEditing(); + }); }; - const handleStartListening = () => { - setPressedKeys(new Set()); - setListening(listening?false:true); + const onCancelShortcut = async () => { + cancelEditing(); + // + invoke("change_shortcut", { key: shortcut?.join("+") }).catch((err) => { + console.error("Failed to save hotkey:", err); + startEditing(); + }); }; - const handleClearHotkey = (e: React.MouseEvent) => { - e.stopPropagation(); - setHotkey(null); - setPressedKeys(new Set()); - setListening(false); - invoke("change_shortcut", { key: "" }).catch((err) => { - console.error("Failed to clear shortcut:", err); - }); + const onSaveShortcut = async () => { + saveShortcut(); }; return ( @@ -292,7 +118,7 @@ export default function GeneralSettings() { General Settings
-
- {errorInfo} - {listening ? "Listening..." : ""} -
- - {hotkey && ( - - )} -
+
@@ -349,11 +164,7 @@ export default function GeneralSettings() { title="Appearance" description="Choose your preferred theme" > - changeTheme(value)} - /> +
@@ -368,9 +179,7 @@ export default function GeneralSettings() { > - setShowTooltip(value) - } + onChange={(value) => setShowTooltip(value)} label="Tooltip display" /> diff --git a/src/components/Settings/ShortcutItem.tsx b/src/components/Settings/ShortcutItem.tsx new file mode 100644 index 0000000..ffb06a1 --- /dev/null +++ b/src/components/Settings/ShortcutItem.tsx @@ -0,0 +1,79 @@ +import { formatKey, sortKeys } from "@/utils/keyboardUtils"; +import { X } from "lucide-react"; +interface ShortcutItemProps { + shortcut: string[]; + isEditing: boolean; + currentKeys: string[]; + onEdit: () => void; + onSave: () => void; + onCancel: () => void; +} + +export function ShortcutItem({ + shortcut, + isEditing, + currentKeys, + onEdit, + onSave, + onCancel, +}: ShortcutItemProps) { + const renderKeys = (keys: string[]) => { + const sortedKeys = sortKeys(keys); + return sortedKeys.map((key, index) => ( + + {formatKey(key)} + + )); + }; + + return ( +
+
+ {isEditing ? ( + <> +
+ {currentKeys.length > 0 ? ( + renderKeys(currentKeys) + ) : ( + + Press keys... + + )} +
+
+ + +
+ + ) : ( + <> +
{renderKeys(shortcut)}
+ + + )} +
+
+ ); +} diff --git a/src/components/Settings/shortcut.ts b/src/components/Settings/shortcut.ts new file mode 100644 index 0000000..0e1d2cb --- /dev/null +++ b/src/components/Settings/shortcut.ts @@ -0,0 +1 @@ +export type Shortcut = string[]; \ No newline at end of file diff --git a/src/hooks/useShortcutEditor.ts b/src/hooks/useShortcutEditor.ts new file mode 100644 index 0000000..0e0f4dc --- /dev/null +++ b/src/hooks/useShortcutEditor.ts @@ -0,0 +1,111 @@ +import { useState, useCallback, useEffect } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; + +import { Shortcut } from '@/components/Settings/shortcut'; +import { normalizeKey, isModifierKey, sortKeys } from '@/utils/keyboardUtils'; + +export function useShortcutEditor(shortcut: Shortcut, onChange: (shortcut: Shortcut) => void) { + console.log("shortcut", shortcut) + + const [isEditing, setIsEditing] = useState(false); + const [currentKeys, setCurrentKeys] = useState([]); + const [pressedKeys] = useState(new Set()); + + const startEditing = useCallback(() => { + setIsEditing(true); + setCurrentKeys([]); + }, []); + + const saveShortcut = useCallback(() => { + if (!isEditing || currentKeys.length < 2) return; + + const hasModifier = currentKeys.some(isModifierKey); + const hasNonModifier = currentKeys.some(key => !isModifierKey(key)); + + if (!hasModifier || !hasNonModifier) return; + + // Sort keys to ensure consistent order (modifiers first) + const sortedKeys = sortKeys(currentKeys); + + onChange(sortedKeys); + setIsEditing(false); + setCurrentKeys([]); + }, [isEditing, currentKeys, onChange]); + + const cancelEditing = useCallback(() => { + setIsEditing(false); + setCurrentKeys([]); + }, []); + + // Register key capture for editing state + useHotkeys( + '*', + (e) => { + if (!isEditing) return; + + e.preventDefault(); + e.stopPropagation(); + + const key = normalizeKey(e.code); + + // Update pressed keys + pressedKeys.add(key); + + setCurrentKeys(() => { + const keys = Array.from(pressedKeys); + let modifiers = keys.filter(isModifierKey); + let nonModifiers = keys.filter(k => !isModifierKey(k)); + + if (modifiers.length > 2) { + modifiers = modifiers.slice(0, 2) + } + + if (nonModifiers.length > 2) { + nonModifiers = nonModifiers.slice(0, 2) + } + + // Combine modifiers and non-modifiers + return [...modifiers, ...nonModifiers]; + }); + }, + { + enabled: isEditing, + keydown: true, + enableOnContentEditable: true + }, + [isEditing, pressedKeys] + ); + + // Handle key up events + useHotkeys( + '*', + (e) => { + if (!isEditing) return; + const key = normalizeKey(e.code); + pressedKeys.delete(key); + }, + { + enabled: isEditing, + keyup: true, + enableOnContentEditable: true + }, + [isEditing, pressedKeys] + ); + + // Clean up editing state when component unmounts + useEffect(() => { + return () => { + if (isEditing) { + cancelEditing(); + } + }; + }, [isEditing, cancelEditing]); + + return { + isEditing, + currentKeys, + startEditing, + saveShortcut, + cancelEditing + }; +} \ No newline at end of file diff --git a/src/pages/app/index.tsx b/src/pages/app/index.tsx index ce16cca..e959d17 100644 --- a/src/pages/app/index.tsx +++ b/src/pages/app/index.tsx @@ -1,5 +1,7 @@ import { useState, useRef, useEffect } from "react"; import { isTauri } from "@tauri-apps/api/core"; +import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; +import { LogicalSize } from "@tauri-apps/api/dpi"; import InputBox from "@/components/AppAI/InputBox"; import Search from "@/components/AppAI/Search"; @@ -55,11 +57,6 @@ export default function DesktopApp() { setInput(value); if (isChatMode) { if (isTauri()) { - const { getCurrentWebviewWindow } = await import( - "@tauri-apps/api/webviewWindow" - ); - const { LogicalSize } = await import("@tauri-apps/api/dpi"); - await getCurrentWebviewWindow()?.setSize(new LogicalSize(680, 596)); } chatAIRef.current?.init(); diff --git a/src/utils/index.ts b/src/utils/index.ts index e38a595..954ae15 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,5 +1,6 @@ import { useEffect, useState } from "react"; import { isTauri } from "@tauri-apps/api/core"; +import { open } from "@tauri-apps/plugin-shell"; // 1 export async function copyToClipboard(text: string) { @@ -64,7 +65,6 @@ export const OpenBrowserURL = async (url: string) => { if (!url) return; if (isTauri()) { try { - const { open } = await import("@tauri-apps/plugin-shell"); await open(url); console.log("URL opened in default browser"); } catch (error) { @@ -77,7 +77,7 @@ export const OpenBrowserURL = async (url: string) => { export const authWitheGithub = (uid: string) => { const authorizeUrl = "https://github.com/login/oauth/authorize"; - console.log(111, process.env.NODE_ENV, uid) + console.log("github", process.env.NODE_ENV, uid) location.href = `${authorizeUrl}?client_id=${"Ov23li4IcdbbWp2RgLTN"}&redirect_uri=${"http://localhost:1420/login"}`; }; diff --git a/src/utils/keyboardUtils.ts b/src/utils/keyboardUtils.ts new file mode 100644 index 0000000..a4d14be --- /dev/null +++ b/src/utils/keyboardUtils.ts @@ -0,0 +1,101 @@ +// Platform detection +export const isMac = navigator.platform.toLowerCase().includes('mac'); + +// Mapping of keys to their display symbols +export const KEY_SYMBOLS: Record = { + // Modifier keys + Control: isMac ? '⌃' : 'Ctrl', + control: isMac ? '⌃' : 'Ctrl', + Shift: isMac ? '⇧' : 'Shift', + shift: isMac ? '⇧' : 'Shift', + Alt: isMac ? '⌥' : 'Alt', + alt: isMac ? '⌥' : 'Alt', + Meta: isMac ? '⌘' : 'Win', + Command: isMac ? '⌘' : 'Win', + super: isMac ? '⌘' : 'Win', + // Special keys + Space: 'Space', + Enter: '↵', + Backspace: '⌫', + Delete: 'Del', + Escape: 'Esc', + ArrowUp: '↑', + ArrowDown: '↓', + ArrowLeft: '←', + ArrowRight: '→', + Tab: '⇥', +}; + +// Normalize key names +export const normalizeKey = (key: string): string => { + const keyMap: Record = { + 'ControlLeft': 'Control', + 'ControlRight': 'Control', + 'ShiftLeft': 'Shift', + 'ShiftRight': 'Shift', + 'AltLeft': 'Alt', + 'AltRight': 'Alt', + 'MetaLeft': 'Command', + 'MetaRight': 'Command', + 'Space': 'Space' // Add explicit mapping for Space + }; + + if (keyMap[key]) { + return keyMap[key]; + } + + if (key.startsWith('Key')) { + return key.replace('Key', ''); + } + + if (key.startsWith('Digit')) { + return key.replace('Digit', ''); + } + + if (key.startsWith('Numpad')) { + return key.replace('Numpad', ''); + } + + return key; +}; + +// Format key for display +export const formatKey = (key: string): string => { + if (KEY_SYMBOLS[key]) { + return KEY_SYMBOLS[key]; + } + + if (key.startsWith('Key')) { + return key.replace('Key', ''); + } + + if (key.startsWith('Digit')) { + return key.replace('Digit', ''); + } + + if (key.startsWith('Numpad')) { + return key.replace('Numpad', ''); + } + + return key; +}; + +// Check if key is a modifier +export const isModifierKey = (key: string): boolean => { + return ['Control', 'Shift', 'Alt', 'Meta', 'Command'].includes(key); +}; + +// Sort keys to ensure consistent order (modifiers first) +export const sortKeys = (keys: string[]): string[] => { + const modifierOrder = ['Control', 'Alt', 'Shift', 'Meta', 'Command']; + + return [...keys].sort((a, b) => { + const aIndex = modifierOrder.indexOf(a); + const bIndex = modifierOrder.indexOf(b); + + if (aIndex === -1 && bIndex === -1) return 0; + if (aIndex === -1) return 1; + if (bIndex === -1) return -1; + return aIndex - bIndex; + }); +}; \ No newline at end of file