Refactor hotkey setting UI (#506)
* refactor hotkeys setting * update UI * refactor * fix toast
This commit is contained in:
@@ -523,7 +523,8 @@
|
||||
"AiTranslate": "AI translate",
|
||||
"cambridgeDictionary": "Cambridge dictionary",
|
||||
"customizeShortcuts": "Customize shortcuts",
|
||||
"customizeShortcutsTip":"Press any sequence of keys to set a shortcut",
|
||||
"customizeShortcutsTip":"Click to change",
|
||||
"customizeShortcutsRecordingTip":"Recording new shortcut",
|
||||
"customizeShortcutsInvalidToast": "Your shortcut should only have one modifier (Ctrl, Alt, Shift, or Meta) and one key, like 'Ctrl+C'.",
|
||||
"customizeShortcutsConflictToast": "{{input}} conflicts with the existing {{otherHotkeyName}} shortcut.",
|
||||
"customizeShortcutsUpdated": "Changes saved",
|
||||
|
||||
@@ -522,9 +522,10 @@
|
||||
"AiTranslate": "智能翻译",
|
||||
"cambridgeDictionary": "剑桥词典",
|
||||
"customizeShortcuts": "自定义快捷键",
|
||||
"customizeShortcutsTip":"按任意键序列设置快捷键",
|
||||
"customizeShortcutsInvalidToast":"快捷键应最多含一个修饰键(Ctrl、Alt、Shift 或 Meta)和一个键,如 'Ctrl+C'",
|
||||
"customizeShortcutsConflictToast": "{{input}}和已有{{otherHotkeyName}}的键位冲突了",
|
||||
"customizeShortcutsTip":"点击重新录制",
|
||||
"customizeShortcutsRecordingTip":"正在录制快捷键",
|
||||
"customizeShortcutsInvalidToast":"快捷键应最多含一个修饰键(Ctrl, Alt, Shift 或 Meta)和一个键,如 'Ctrl+C'",
|
||||
"customizeShortcutsConflictToast": "{{input}} 和已有 {{otherHotkeyName}} 的键位冲突了",
|
||||
"customizeShortcutsUpdated": "设置成功",
|
||||
"following": "关注中",
|
||||
"followers": "被关注",
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogContent,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@renderer/components/ui";
|
||||
import { toast } from "@renderer/components/ui";
|
||||
import { useContext, useMemo } from "react";
|
||||
import { HotKeysSettingsProviderContext } from "../context";
|
||||
import { CheckIcon, KeyboardIcon, XIcon } from "lucide-react";
|
||||
import { t } from "i18next";
|
||||
|
||||
export const ChangeHotkeyDialog = ({
|
||||
open,
|
||||
name,
|
||||
keyName,
|
||||
onOpenChange,
|
||||
}: {
|
||||
open: boolean;
|
||||
name: string;
|
||||
keyName: string;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}) => {
|
||||
const {
|
||||
changeHotkey,
|
||||
currentHotkeys,
|
||||
recordingHotkeys,
|
||||
resetRecordingHotkeys,
|
||||
} = useContext(HotKeysSettingsProviderContext);
|
||||
|
||||
const joinedKeys = useMemo(() => [...recordingHotkeys].join("+"), [
|
||||
recordingHotkeys,
|
||||
]);
|
||||
|
||||
const changeKeyMap = async () => {
|
||||
const ret = ((await changeHotkey(
|
||||
keyName,
|
||||
recordingHotkeys
|
||||
)) as unknown) as {
|
||||
error: "conflict" | "invalid";
|
||||
data: string | string[];
|
||||
input: string;
|
||||
};
|
||||
const { error, data, input } = ret ?? {};
|
||||
if (error === "conflict") {
|
||||
toast.error(
|
||||
t("customizeShortcutsConflictToast", {
|
||||
input,
|
||||
otherHotkeyName: (data as string[]).join(","),
|
||||
})
|
||||
);
|
||||
} else if (error === "invalid") {
|
||||
toast.error(t("customizeShortcutsInvalidToast"));
|
||||
} else {
|
||||
toast.success(t("customizeShortcutsUpdated"));
|
||||
}
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
resetRecordingHotkeys();
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{t("customizeShortcuts")}</AlertDialogTitle>
|
||||
</AlertDialogHeader>
|
||||
<div>
|
||||
<p className="pb-4">{name}</p>
|
||||
<div className="flex items-center">
|
||||
<p className="inline-block gap-2 border-2 border-black rounded p-[2px] mr-2">
|
||||
{currentHotkeys[keyName]}
|
||||
</p>
|
||||
{joinedKeys.length > 0 ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<p className="border-2 border-black rounded p-[2px]">
|
||||
{joinedKeys}
|
||||
</p>
|
||||
<div className="cursor-pointer" onClick={changeKeyMap}>
|
||||
<CheckIcon className="text-green-500 w-5 h-5" />
|
||||
</div>
|
||||
<div className="cursor-pointer" onClick={clear}>
|
||||
<XIcon className="text-red-500 w-5 h-5" />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="inline-block gap-2 border-2 border-black rounded p-[2px]">
|
||||
<span className="text-sm">{t("customizeShortcutsTip")}</span>
|
||||
<KeyboardIcon className="inline ml-1 w-6 h-6 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction>Close</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
129
enjoy/src/renderer/components/preferences/hotkeys-settings.tsx
Normal file
129
enjoy/src/renderer/components/preferences/hotkeys-settings.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import { t } from "i18next";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
Button,
|
||||
toast,
|
||||
} from "@renderer/components/ui";
|
||||
import { HotKeysSettingsProviderContext } from "@renderer/context";
|
||||
import { useContext, useMemo, useEffect } from "react";
|
||||
|
||||
export const HotkeysSettings = ({
|
||||
open,
|
||||
name,
|
||||
keyName,
|
||||
onOpenChange,
|
||||
}: {
|
||||
open: boolean;
|
||||
name: string;
|
||||
keyName: string;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}) => {
|
||||
const {
|
||||
changeHotkey,
|
||||
currentHotkeys,
|
||||
recordingHotkeys,
|
||||
resetRecordingHotkeys,
|
||||
startRecordingHotkeys,
|
||||
stopRecordingHotkeys,
|
||||
isRecording,
|
||||
} = useContext(HotKeysSettingsProviderContext);
|
||||
|
||||
const joinedKeys = useMemo(
|
||||
() => [...recordingHotkeys].join("+"),
|
||||
[recordingHotkeys]
|
||||
);
|
||||
|
||||
const changeKeyMap = async () => {
|
||||
const ret = (await changeHotkey(keyName, recordingHotkeys)) as unknown as {
|
||||
error: "conflict" | "invalid";
|
||||
data: string | string[];
|
||||
input: string;
|
||||
};
|
||||
stopRecordingHotkeys();
|
||||
const { error, data, input } = ret ?? {};
|
||||
|
||||
if (error === "conflict") {
|
||||
toast.error(
|
||||
t("customizeShortcutsConflictToast", {
|
||||
input,
|
||||
otherHotkeyName: (data as string[])
|
||||
.map((str) => t(str.charAt(0).toLowerCase() + str.slice(1)))
|
||||
.join(","),
|
||||
})
|
||||
);
|
||||
} else if (error === "invalid") {
|
||||
toast.error(t("customizeShortcutsInvalidToast"));
|
||||
} else {
|
||||
toast.success(t("customizeShortcutsUpdated"));
|
||||
onOpenChange(false);
|
||||
}
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
stopRecordingHotkeys();
|
||||
resetRecordingHotkeys();
|
||||
};
|
||||
|
||||
// ensure recording disabled when dialog close
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
stopRecordingHotkeys();
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{name}</AlertDialogTitle>
|
||||
</AlertDialogHeader>
|
||||
<div>
|
||||
{isRecording ? (
|
||||
<div className="">
|
||||
<div className="flex justify-center mb-4">
|
||||
<Button variant="secondary">
|
||||
{joinedKeys.length > 0 ? (
|
||||
<span className="text-sm">{joinedKeys}</span>
|
||||
) : (
|
||||
<span className="font-mono">-</span>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="py-2 text-center text-sm text-muted-foreground">
|
||||
{t("customizeShortcutsRecordingTip")}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="">
|
||||
<div className="flex justify-center mb-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="font-mono"
|
||||
onClick={() => {
|
||||
startRecordingHotkeys();
|
||||
}}
|
||||
>
|
||||
{currentHotkeys[keyName]}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="py-2 text-center text-sm text-muted-foreground">
|
||||
{t("customizeShortcutsTip")}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<AlertDialogFooter>
|
||||
<Button disabled={!isRecording || !joinedKeys} onClick={changeKeyMap}>
|
||||
{t("save")}
|
||||
</Button>
|
||||
<AlertDialogCancel onClick={reset}>{t("cancel")}</AlertDialogCancel>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
@@ -1,8 +1,10 @@
|
||||
import { t } from "i18next";
|
||||
import { Separator } from "@renderer/components/ui";
|
||||
import { HotKeysSettingsProviderContext, Hotkey } from "@/renderer/context";
|
||||
import {
|
||||
Separator,
|
||||
} from "@renderer/components/ui";
|
||||
import { HotKeysSettingsProviderContext, Hotkey } from "@renderer/context";
|
||||
import { HotkeysSettings } from "@renderer/components";
|
||||
import { useContext, useState } from "react";
|
||||
import { ChangeHotkeyDialog } from "../change-hotkey-dialog";
|
||||
|
||||
export const Hotkeys = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -10,27 +12,15 @@ export const Hotkeys = () => {
|
||||
name: string;
|
||||
keyName: string;
|
||||
} | null>(null);
|
||||
const {
|
||||
currentHotkeys,
|
||||
startRecordingHotkeys,
|
||||
stopRecordingHotkeys,
|
||||
} = useContext(HotKeysSettingsProviderContext);
|
||||
const { currentHotkeys } = useContext(HotKeysSettingsProviderContext);
|
||||
|
||||
const commandOrCtrl = navigator.platform.includes("Mac") ? "Cmd" : "Ctrl";
|
||||
|
||||
const handleItemSelected = (item: { name: string; keyName: Hotkey }) => {
|
||||
setOpen(true);
|
||||
startRecordingHotkeys();
|
||||
setSelectedItem(item);
|
||||
};
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
setOpen(open);
|
||||
if (!open) {
|
||||
stopRecordingHotkeys();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="font-semibold mb-4 capitilized">{t("hotkeys")}</div>
|
||||
@@ -39,8 +29,8 @@ export const Hotkeys = () => {
|
||||
|
||||
<div className="flex items-center justify-between py-4">
|
||||
<div className="flex items-center space-x-2">{t("quitApp")}</div>
|
||||
<kbd className="bg-muted px-2 py-1 rounded-md text-sm text-muted-foreground cursor-not-allowed">
|
||||
{commandOrCtrl} + Q
|
||||
<kbd className="bg-muted px-2 py-1 rounded-md text-sm text-muted-foreground cursor-not-allowed capitalize">
|
||||
{commandOrCtrl}+Q
|
||||
</kbd>
|
||||
</div>
|
||||
|
||||
@@ -53,11 +43,11 @@ export const Hotkeys = () => {
|
||||
<kbd
|
||||
onClick={() =>
|
||||
handleItemSelected({
|
||||
name: "Open preferences",
|
||||
name: t("openPreferences"),
|
||||
keyName: "OpenPreferences",
|
||||
})
|
||||
}
|
||||
className="bg-muted px-2 py-1 rounded-md text-sm text-muted-foreground cursor-pointer"
|
||||
className="bg-muted px-2 py-1 rounded-md text-sm text-muted-foreground cursor-pointer capitalize"
|
||||
>
|
||||
{currentHotkeys.OpenPreferences}
|
||||
</kbd>
|
||||
@@ -77,7 +67,7 @@ export const Hotkeys = () => {
|
||||
keyName: "PlayOrPause",
|
||||
})
|
||||
}
|
||||
className="bg-muted px-2 py-1 rounded-md text-sm text-muted-foreground cursor-pointer"
|
||||
className="bg-muted px-2 py-1 rounded-md text-sm text-muted-foreground cursor-pointer capitalize"
|
||||
>
|
||||
{currentHotkeys.PlayOrPause}
|
||||
</kbd>
|
||||
@@ -96,7 +86,7 @@ export const Hotkeys = () => {
|
||||
keyName: "StartOrStopRecording",
|
||||
})
|
||||
}
|
||||
className="bg-muted px-2 py-1 rounded-md text-sm text-muted-foreground cursor-pointer"
|
||||
className="bg-muted px-2 py-1 rounded-md text-sm text-muted-foreground cursor-pointer capitalize"
|
||||
>
|
||||
{currentHotkeys.StartOrStopRecording}
|
||||
</kbd>
|
||||
@@ -115,7 +105,7 @@ export const Hotkeys = () => {
|
||||
keyName: "PlayOrPauseRecording",
|
||||
})
|
||||
}
|
||||
className="bg-muted px-2 py-1 rounded-md text-sm text-muted-foreground cursor-pointer"
|
||||
className="bg-muted px-2 py-1 rounded-md text-sm text-muted-foreground cursor-pointer capitalize"
|
||||
>
|
||||
{currentHotkeys.PlayOrPauseRecording}
|
||||
</kbd>
|
||||
@@ -153,7 +143,7 @@ export const Hotkeys = () => {
|
||||
keyName: "PlayNextSegment",
|
||||
})
|
||||
}
|
||||
className="bg-muted px-2 py-1 rounded-md text-sm text-muted-foreground cursor-pointer"
|
||||
className="bg-muted px-2 py-1 rounded-md text-sm text-muted-foreground cursor-pointer capitalize"
|
||||
>
|
||||
{currentHotkeys.PlayNextSegment}
|
||||
</kbd>
|
||||
@@ -172,7 +162,7 @@ export const Hotkeys = () => {
|
||||
keyName: "Compare",
|
||||
})
|
||||
}
|
||||
className="bg-muted px-2 py-1 rounded-md text-sm text-muted-foreground cursor-pointer"
|
||||
className="bg-muted px-2 py-1 rounded-md text-sm text-muted-foreground cursor-pointer capitalize"
|
||||
>
|
||||
{currentHotkeys.Compare}
|
||||
</kbd>
|
||||
@@ -181,12 +171,12 @@ export const Hotkeys = () => {
|
||||
<Separator />
|
||||
</div>
|
||||
|
||||
<ChangeHotkeyDialog
|
||||
<HotkeysSettings
|
||||
open={open}
|
||||
keyName={selectedItem?.keyName}
|
||||
name={selectedItem?.name}
|
||||
onOpenChange={handleOpenChange}
|
||||
onOpenChange={setOpen}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,8 @@
|
||||
export * from "./preferences";
|
||||
export * from "./about";
|
||||
|
||||
export * from "./hotkeys";
|
||||
export * from "./hotkeys-settings";
|
||||
|
||||
export * from "./default-engine-settings";
|
||||
export * from "./openai-settings";
|
||||
|
||||
@@ -47,11 +47,11 @@ const defaultKeyMap = {
|
||||
OpenPreferences: `${ControlOrCommand}+Comma`,
|
||||
// player
|
||||
PlayOrPause: "Space",
|
||||
StartOrStopRecording: "r",
|
||||
PlayOrPauseRecording: `${ControlOrCommand}+r`,
|
||||
PlayPreviousSegment: "p",
|
||||
PlayNextSegment: "n",
|
||||
Compare: "c",
|
||||
StartOrStopRecording: "R",
|
||||
PlayOrPauseRecording: `${ControlOrCommand}+R`,
|
||||
PlayPreviousSegment: "P",
|
||||
PlayNextSegment: "N",
|
||||
Compare: "C",
|
||||
// dev tools
|
||||
OpenDevTools: `${ControlOrCommand}+Shift+I`,
|
||||
};
|
||||
@@ -76,6 +76,7 @@ type HotkeysSettingsProviderState = {
|
||||
currentHotkeys: Record<string, string>;
|
||||
recordingHotkeys?: any;
|
||||
enabled: boolean;
|
||||
isRecording: boolean;
|
||||
startRecordingHotkeys?: () => void;
|
||||
stopRecordingHotkeys?: () => void;
|
||||
resetRecordingHotkeys?: () => void;
|
||||
@@ -85,6 +86,7 @@ type HotkeysSettingsProviderState = {
|
||||
const initialState: HotkeysSettingsProviderState = {
|
||||
currentHotkeys: {},
|
||||
enabled: true,
|
||||
isRecording: false,
|
||||
};
|
||||
|
||||
export const HotKeysSettingsProviderContext = createContext<
|
||||
@@ -215,6 +217,7 @@ export const HotKeysSettingsProvider = ({
|
||||
currentHotkeys,
|
||||
recordingHotkeys: keys,
|
||||
enabled: !isRecording,
|
||||
isRecording,
|
||||
startRecordingHotkeys,
|
||||
stopRecordingHotkeys,
|
||||
resetRecordingHotkeys: resetKeys,
|
||||
|
||||
Reference in New Issue
Block a user