summaryrefslogtreecommitdiff
path: root/apps/web
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2025-07-26 12:54:50 +0200
committerMaze Winther <mazewinther@gmail.com>2025-07-26 12:54:50 +0200
commit6dfc80cd70db08f847d114bdbae582c9983bb83e (patch)
treebd5d5fa5fe033d461a8b6c3003bec64451464626 /apps/web
parent043bf7d70abb705059bed5c0dbcc8007448750ec (diff)
downloadopencut-6dfc80cd70db08f847d114bdbae582c9983bb83e.tar.gz
Merge pull request [#433](https://github.com/mazeincoding/AppCut/issues/433)
Diffstat (limited to 'apps/web')
-rw-r--r--apps/web/src/components/keyboard-shortcuts-help.tsx106
-rw-r--r--apps/web/src/components/ui/dialog.tsx2
-rw-r--r--apps/web/src/hooks/use-keybindings.ts6
-rw-r--r--apps/web/src/hooks/use-keyboard-shortcuts-help.ts13
-rw-r--r--apps/web/src/stores/keybindings-store.ts14
5 files changed, 72 insertions, 69 deletions
diff --git a/apps/web/src/components/keyboard-shortcuts-help.tsx b/apps/web/src/components/keyboard-shortcuts-help.tsx
index 4c8bfff5..96d4705f 100644
--- a/apps/web/src/components/keyboard-shortcuts-help.tsx
+++ b/apps/web/src/components/keyboard-shortcuts-help.tsx
@@ -1,49 +1,32 @@
"use client";
-import { useState, useEffect } from "react";
+import { Keyboard } from "lucide-react";
+import { useEffect, useState } from "react";
+import { toast } from "sonner";
+import {
+ type KeyboardShortcut,
+ useKeyboardShortcutsHelp,
+} from "@/hooks/use-keyboard-shortcuts-help";
+import { useKeybindingsStore } from "@/stores/keybindings-store";
import { Button } from "./ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
+ DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "./ui/dialog";
-import { getPlatformSpecialKey } from "@/lib/utils";
-import { Keyboard } from "lucide-react";
-import {
- useKeyboardShortcutsHelp,
- KeyboardShortcut,
-} from "@/hooks/use-keyboard-shortcuts-help";
-import { useKeybindingsStore } from "@/stores/keybindings-store";
-import { toast } from "sonner";
-
-const modifier: {
- [key: string]: string;
-} = {
- Shift: "Shift",
- Alt: "Alt",
- ArrowLeft: "←",
- ArrowRight: "→",
- ArrowUp: "↑",
- ArrowDown: "↓",
- Space: "Space",
-};
-
-function getKeyWithModifier(key: string) {
- if (key === "Ctrl") return getPlatformSpecialKey();
- return modifier[key] || key;
-}
const ShortcutItem = ({
shortcut,
- recordingKey,
+ isRecording,
onStartRecording,
}: {
shortcut: KeyboardShortcut;
- recordingKey: string | null;
- onStartRecording: (keyId: string, shortcut: KeyboardShortcut) => void;
+ isRecording: boolean;
+ onStartRecording: (shortcut: KeyboardShortcut) => void;
}) => {
// Filter out lowercase duplicates for display - if both "j" and "J" exist, only show "J"
const displayKeys = shortcut.keys.filter((key: string) => {
@@ -66,20 +49,17 @@ const ShortcutItem = ({
</div>
<div className="flex items-center gap-1">
{displayKeys.map((key: string, index: number) => (
- <div key={index} className="flex items-center gap-1">
+ <div key={key} className="flex items-center gap-1">
<div className="flex items-center">
{key.split("+").map((keyPart: string, partIndex: number) => {
const keyId = `${shortcut.id}-${index}-${partIndex}`;
return (
<EditableShortcutKey
- key={partIndex}
- keyId={keyId}
- originalKey={key}
- shortcut={shortcut}
- isRecording={recordingKey === keyId}
- onStartRecording={() => onStartRecording(keyId, shortcut)}
+ key={keyId}
+ isRecording={isRecording}
+ onStartRecording={() => onStartRecording(shortcut)}
>
- {getKeyWithModifier(keyPart)}
+ {keyPart}
</EditableShortcutKey>
);
})}
@@ -96,16 +76,10 @@ const ShortcutItem = ({
const EditableShortcutKey = ({
children,
- keyId,
- originalKey,
- shortcut,
isRecording,
onStartRecording,
}: {
children: React.ReactNode;
- keyId: string;
- originalKey: string;
- shortcut: KeyboardShortcut;
isRecording: boolean;
onStartRecording: () => void;
}) => {
@@ -116,7 +90,9 @@ const EditableShortcutKey = ({
};
return (
- <kbd
+ <Button
+ variant="text"
+ size="sm"
className={`inline-flex font-sans text-xs rounded px-2 min-w-[1.5rem] min-h-[1.5rem] leading-none items-center justify-center shadow-sm border mr-1 cursor-pointer hover:bg-opacity-80 ${
isRecording
? "border-primary bg-primary/10"
@@ -128,13 +104,12 @@ const EditableShortcutKey = ({
}
>
{children}
- </kbd>
+ </Button>
);
};
export const KeyboardShortcutsHelp = () => {
const [open, setOpen] = useState(false);
- const [recordingKey, setRecordingKey] = useState<string | null>(null);
const [recordingShortcut, setRecordingShortcut] =
useState<KeyboardShortcut | null>(null);
@@ -144,6 +119,9 @@ export const KeyboardShortcutsHelp = () => {
getKeybindingString,
validateKeybinding,
getKeybindingsForAction,
+ setIsRecording,
+ resetToDefaults,
+ isRecording,
} = useKeybindingsStore();
// Get shortcuts from centralized hook
@@ -152,7 +130,7 @@ export const KeyboardShortcutsHelp = () => {
const categories = Array.from(new Set(shortcuts.map((s) => s.category)));
useEffect(() => {
- if (!recordingKey || !recordingShortcut) return;
+ if (!isRecording || !recordingShortcut) return;
const handleKeyDown = (e: KeyboardEvent) => {
e.preventDefault();
@@ -169,7 +147,6 @@ export const KeyboardShortcutsHelp = () => {
toast.error(
`Key "${keyString}" is already bound to "${conflict.existingAction}"`
);
- setRecordingKey(null);
setRecordingShortcut(null);
return;
}
@@ -181,14 +158,14 @@ export const KeyboardShortcutsHelp = () => {
// Add new keybinding
updateKeybinding(keyString, recordingShortcut.action);
- setRecordingKey(null);
+ setIsRecording(false);
setRecordingShortcut(null);
}
};
- const handleClickOutside = (e: MouseEvent) => {
- setRecordingKey(null);
+ const handleClickOutside = () => {
setRecordingShortcut(null);
+ setIsRecording(false);
};
document.addEventListener("keydown", handleKeyDown);
@@ -199,18 +176,19 @@ export const KeyboardShortcutsHelp = () => {
document.removeEventListener("click", handleClickOutside);
};
}, [
- recordingKey,
recordingShortcut,
getKeybindingString,
updateKeybinding,
removeKeybinding,
validateKeybinding,
getKeybindingsForAction,
+ setIsRecording,
+ isRecording,
]);
- const handleStartRecording = (keyId: string, shortcut: KeyboardShortcut) => {
- setRecordingKey(keyId);
+ const handleStartRecording = (shortcut: KeyboardShortcut) => {
setRecordingShortcut(shortcut);
+ setIsRecording(true);
};
return (
@@ -221,7 +199,7 @@ export const KeyboardShortcutsHelp = () => {
Shortcuts
</Button>
</DialogTrigger>
- <DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
+ <DialogContent className="max-w-2xl overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Keyboard className="w-5 h-5" />
@@ -242,11 +220,13 @@ export const KeyboardShortcutsHelp = () => {
<div className="space-y-0.5">
{shortcuts
.filter((shortcut) => shortcut.category === category)
- .map((shortcut, index) => (
+ .map((shortcut) => (
<ShortcutItem
- key={index}
+ key={shortcut.action}
shortcut={shortcut}
- recordingKey={recordingKey}
+ isRecording={
+ shortcut.action === recordingShortcut?.action
+ }
onStartRecording={handleStartRecording}
/>
))}
@@ -254,6 +234,16 @@ export const KeyboardShortcutsHelp = () => {
</div>
))}
</div>
+ <DialogFooter>
+ <Button
+ size="sm"
+ className="mt-4"
+ variant="destructive"
+ onClick={resetToDefaults}
+ >
+ Reset to Default
+ </Button>
+ </DialogFooter>
</DialogContent>
</Dialog>
);
diff --git a/apps/web/src/components/ui/dialog.tsx b/apps/web/src/components/ui/dialog.tsx
index e8decbd7..35843a5f 100644
--- a/apps/web/src/components/ui/dialog.tsx
+++ b/apps/web/src/components/ui/dialog.tsx
@@ -52,7 +52,7 @@ const DialogContent = React.forwardRef<
}}
{...props}
>
- <ScrollArea className="max-h-[85vh]">
+ <ScrollArea className="max-h-[75vh]">
<div className="p-6 space-y-4">{children}</div>
</ScrollArea>
<DialogPrimitive.Close className="absolute right-4 top-4 opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
diff --git a/apps/web/src/hooks/use-keybindings.ts b/apps/web/src/hooks/use-keybindings.ts
index 7224c1c8..10616900 100644
--- a/apps/web/src/hooks/use-keybindings.ts
+++ b/apps/web/src/hooks/use-keybindings.ts
@@ -8,13 +8,15 @@ import { useKeybindingsStore } from "@/stores/keybindings-store";
* the appropriate actions based on keybindings
*/
export function useKeybindingsListener() {
- const { keybindings, getKeybindingString, keybindingsEnabled } =
+ const { keybindings, getKeybindingString, keybindingsEnabled, isRecording } =
useKeybindingsStore();
useEffect(() => {
const handleKeyDown = (ev: KeyboardEvent) => {
// Do not check keybinds if the mode is disabled
if (!keybindingsEnabled) return;
+ // ignore key events if user is changing keybindings
+ if (isRecording) return;
const binding = getKeybindingString(ev);
if (!binding) return;
@@ -45,7 +47,7 @@ export function useKeybindingsListener() {
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
- }, [keybindings, getKeybindingString, keybindingsEnabled]);
+ }, [keybindings, getKeybindingString, keybindingsEnabled, isRecording]);
}
/**
diff --git a/apps/web/src/hooks/use-keyboard-shortcuts-help.ts b/apps/web/src/hooks/use-keyboard-shortcuts-help.ts
index c7de8466..2f1a4d4a 100644
--- a/apps/web/src/hooks/use-keyboard-shortcuts-help.ts
+++ b/apps/web/src/hooks/use-keyboard-shortcuts-help.ts
@@ -3,6 +3,7 @@
import { useMemo } from "react";
import { useKeybindingsStore } from "@/stores/keybindings-store";
import { Action } from "@/constants/actions";
+import { getPlatformAlternateKey, getPlatformSpecialKey } from "@/lib/utils";
export interface KeyboardShortcut {
id: string;
@@ -67,13 +68,13 @@ const actionDescriptions: Record<
// Convert key binding format to display format
const formatKey = (key: string): string => {
return key
- .replace("ctrl", "Cmd")
- .replace("alt", "Alt")
+ .replace("ctrl", getPlatformSpecialKey())
+ .replace("alt", getPlatformAlternateKey())
.replace("shift", "Shift")
- .replace("left", "ArrowLeft")
- .replace("right", "ArrowRight")
- .replace("up", "ArrowUp")
- .replace("down", "ArrowDown")
+ .replace("left", "←")
+ .replace("right", "→")
+ .replace("up", "↑")
+ .replace("down", "↓")
.replace("space", "Space")
.replace("home", "Home")
.replace("end", "End")
diff --git a/apps/web/src/stores/keybindings-store.ts b/apps/web/src/stores/keybindings-store.ts
index 3e525b86..97cba23f 100644
--- a/apps/web/src/stores/keybindings-store.ts
+++ b/apps/web/src/stores/keybindings-store.ts
@@ -39,6 +39,7 @@ interface KeybindingsState {
keybindings: KeybindingConfig;
isCustomized: boolean;
keybindingsEnabled: boolean;
+ isRecording: boolean;
// Actions
updateKeybinding: (key: ShortcutKey, action: ActionWithOptionalArgs) => void;
@@ -48,6 +49,7 @@ interface KeybindingsState {
exportKeybindings: () => KeybindingConfig;
enableKeybindings: () => void;
disableKeybindings: () => void;
+ setIsRecording: (isRecording: boolean) => void;
// Validation
validateKeybinding: (
@@ -66,6 +68,7 @@ export const useKeybindingsStore = create<KeybindingsState>()(
keybindings: { ...defaultKeybindings },
isCustomized: false,
keybindingsEnabled: true,
+ isRecording: false,
updateKeybinding: (key: ShortcutKey, action: ActionWithOptionalArgs) => {
set((state) => {
@@ -141,6 +144,9 @@ export const useKeybindingsStore = create<KeybindingsState>()(
return null;
},
+ setIsRecording: (isRecording: boolean) => {
+ set({ isRecording });
+ },
getKeybindingsForAction: (action: ActionWithOptionalArgs) => {
const { keybindings } = get();
@@ -211,8 +217,12 @@ function getPressedKey(ev: KeyboardEvent): string | null {
if (key === "backspace") return "backspace";
// Check letter keys
- const isLetter = key.length === 1 && key >= "a" && key <= "z";
- if (isLetter) return key;
+ if (code.startsWith("Key")) {
+ const letter = code.slice(3).toLowerCase();
+ if (letter.length === 1 && letter >= "a" && letter <= "z") {
+ return letter;
+ }
+ }
// Check number keys using physical position for AZERTY support
if (code.startsWith("Digit")) {