Feat/custom hotkeys (#496)
* feat: 2024-04-07 15:27:52 - custom hotkeys * feat: 2024-04-07 15:52:13 - add custome compare shortcuts * feat: 2024-04-09 10:17:23 - Modify the code according to the code review suggestions and optimize the experience. --------- Co-authored-by: more.tai <more.tai@huolala.cn>
This commit is contained in:
103
enjoy/src/renderer/components/change-hotkey-dialog.tsx
Normal file
103
enjoy/src/renderer/components/change-hotkey-dialog.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useContext, useRef, useState } from "react";
|
||||
import {
|
||||
AppSettingsProviderContext,
|
||||
HotKeysSettingsProviderContext,
|
||||
MediaPlayerProviderContext,
|
||||
} from "@renderer/context";
|
||||
import { MediaRecorder, RecordingDetail } from "@renderer/components";
|
||||
@@ -60,6 +61,9 @@ export const MediaCurrentRecording = () => {
|
||||
currentTime: mediaCurrentTime,
|
||||
} = useContext(MediaPlayerProviderContext);
|
||||
const { webApi, EnjoyApp } = useContext(AppSettingsProviderContext);
|
||||
const { enabled, currentHotkeys } = useContext(
|
||||
HotKeysSettingsProviderContext
|
||||
);
|
||||
const [player, setPlayer] = useState(null);
|
||||
const [regions, setRegions] = useState<Regions | null>(null);
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
@@ -328,7 +332,7 @@ export const MediaCurrentRecording = () => {
|
||||
}
|
||||
|
||||
const subscriptions = [
|
||||
regions.on("region-created", () => { }),
|
||||
regions.on("region-created", () => {}),
|
||||
|
||||
regions.on("region-clicked", (region, e) => {
|
||||
e.stopPropagation();
|
||||
@@ -399,7 +403,7 @@ export const MediaCurrentRecording = () => {
|
||||
}, [currentRecording, isRecording, layout?.width]);
|
||||
|
||||
useHotkeys(
|
||||
["Ctrl+R", "Meta+R"],
|
||||
currentHotkeys.PlayOrPauseRecording,
|
||||
(keyboardEvent, hotkeyEvent) => {
|
||||
if (!player) return;
|
||||
keyboardEvent.preventDefault();
|
||||
@@ -411,6 +415,7 @@ export const MediaCurrentRecording = () => {
|
||||
document.getElementById("recording-play-or-pause-button").click();
|
||||
}
|
||||
},
|
||||
{ enabled },
|
||||
[player]
|
||||
);
|
||||
|
||||
@@ -422,7 +427,9 @@ export const MediaCurrentRecording = () => {
|
||||
<div
|
||||
className="m-auto"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: t("noRecordingForThisSegmentYet"),
|
||||
__html: t("noRecordingForThisSegmentYet", {
|
||||
key: currentHotkeys.StartOrStopRecording?.toUpperCase(),
|
||||
}),
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
@@ -551,16 +558,17 @@ export const MediaCurrentRecording = () => {
|
||||
>
|
||||
<GaugeCircleIcon
|
||||
className={`w-4 h-4 mr-4
|
||||
${currentRecording.pronunciationAssessment
|
||||
? currentRecording.pronunciationAssessment
|
||||
.pronunciationScore >= 80
|
||||
? "text-green-500"
|
||||
: currentRecording.pronunciationAssessment
|
||||
.pronunciationScore >= 60
|
||||
? "text-yellow-600"
|
||||
: "text-red-500"
|
||||
: ""
|
||||
}
|
||||
${
|
||||
currentRecording.pronunciationAssessment
|
||||
? currentRecording.pronunciationAssessment
|
||||
.pronunciationScore >= 80
|
||||
? "text-green-500"
|
||||
: currentRecording.pronunciationAssessment
|
||||
.pronunciationScore >= 60
|
||||
? "text-yellow-600"
|
||||
: "text-red-500"
|
||||
: ""
|
||||
}
|
||||
`}
|
||||
/>
|
||||
<span>{t("pronunciationAssessment")}</span>
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import {
|
||||
MediaPlayerProviderContext,
|
||||
AppSettingsProviderContext,
|
||||
HotKeysSettingsProviderContext,
|
||||
} from "@renderer/context";
|
||||
import {
|
||||
ScissorsIcon,
|
||||
@@ -56,6 +57,7 @@ export const MediaPlayerControls = () => {
|
||||
setTranscriptionDraft,
|
||||
} = useContext(MediaPlayerProviderContext);
|
||||
const { EnjoyApp } = useContext(AppSettingsProviderContext);
|
||||
const { currentHotkeys, enabled } = useContext(HotKeysSettingsProviderContext)
|
||||
const [playMode, setPlayMode] = useState<"loop" | "single" | "all">("single");
|
||||
const [playbackRate, setPlaybackRate] = useState<number>(1);
|
||||
const [grouping, setGrouping] = useState(false);
|
||||
@@ -372,30 +374,32 @@ export const MediaPlayerControls = () => {
|
||||
}, [wavesurfer, decoded, playMode, activeRegion, currentTime]);
|
||||
|
||||
useHotkeys(
|
||||
["Space", "p", "n", "r", "c"],
|
||||
[currentHotkeys.PlayOrPause, currentHotkeys.PlayPreviousSegment, currentHotkeys.PlayNextSegment, currentHotkeys.StartOrStopRecording, currentHotkeys.Compare],
|
||||
(keyboardEvent, hotkeyEvent) => {
|
||||
if (!wavesurfer) return;
|
||||
keyboardEvent.preventDefault();
|
||||
|
||||
switch (hotkeyEvent.keys.join("")) {
|
||||
case "space":
|
||||
case currentHotkeys.PlayOrPause.toLowerCase():
|
||||
document.getElementById("media-play-or-pause-button").click();
|
||||
break;
|
||||
case "p":
|
||||
case currentHotkeys.PlayPreviousSegment.toLowerCase():
|
||||
document.getElementById("media-play-previous-button").click();
|
||||
break;
|
||||
case "n":
|
||||
case currentHotkeys.PlayNextSegment.toLowerCase():
|
||||
document.getElementById("media-play-next-button").click();
|
||||
break;
|
||||
case "r":
|
||||
case currentHotkeys.StartOrStopRecording.toLowerCase():
|
||||
document.getElementById("media-record-button").click();
|
||||
break;
|
||||
case "c":
|
||||
case currentHotkeys.Compare.toLowerCase():
|
||||
document.getElementById("media-compare-button").click();
|
||||
break;
|
||||
}
|
||||
},{
|
||||
enabled
|
||||
},
|
||||
[wavesurfer]
|
||||
[wavesurfer, currentHotkeys]
|
||||
);
|
||||
|
||||
/*
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from "@renderer/components/ui";
|
||||
import {
|
||||
AppSettingsProviderContext,
|
||||
HotKeysSettingsProviderContext,
|
||||
MediaPlayerProviderContext,
|
||||
} from "@renderer/context";
|
||||
import {
|
||||
@@ -29,6 +30,7 @@ import { t } from "i18next";
|
||||
import { formatDateTime, formatDuration } from "@renderer/lib/utils";
|
||||
|
||||
export const MediaRecordings = () => {
|
||||
const { currentHotkeys } = useContext(HotKeysSettingsProviderContext);
|
||||
const containerRef = useRef<HTMLDivElement>();
|
||||
const {
|
||||
recordings = [],
|
||||
@@ -59,7 +61,9 @@ export const MediaRecordings = () => {
|
||||
<div
|
||||
className="text-center px-6 py-8 text-sm text-muted-foreground"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: t("noRecordingForThisSegmentYet"),
|
||||
__html: t("noRecordingForThisSegmentYet", {
|
||||
key: currentHotkeys.StartOrStopRecording?.toUpperCase(),
|
||||
}),
|
||||
}}
|
||||
></div>
|
||||
)}
|
||||
|
||||
@@ -1,19 +1,45 @@
|
||||
import { t } from "i18next";
|
||||
import { Separator } from "@renderer/components/ui";
|
||||
import { HotKeysSettingsProviderContext, Hotkey } from "@/renderer/context";
|
||||
import { useContext, useState } from "react";
|
||||
import { ChangeHotkeyDialog } from "../change-hotkey-dialog";
|
||||
|
||||
export const Hotkeys = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedItem, setSelectedItem] = useState<{
|
||||
name: string;
|
||||
keyName: string;
|
||||
} | null>(null);
|
||||
const {
|
||||
currentHotkeys,
|
||||
startRecordingHotkeys,
|
||||
stopRecordingHotkeys,
|
||||
} = 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>
|
||||
|
||||
<div className="mb-6">
|
||||
<div className="text-sm text-muted-foreground">{t("system")}</div>
|
||||
|
||||
<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">
|
||||
<kbd className="bg-muted px-2 py-1 rounded-md text-sm text-muted-foreground cursor-not-allowed">
|
||||
{commandOrCtrl} + Q
|
||||
</kbd>
|
||||
</div>
|
||||
@@ -24,8 +50,16 @@ export const Hotkeys = () => {
|
||||
<div className="flex items-center space-x-2">
|
||||
{t("openPreferences")}
|
||||
</div>
|
||||
<kbd className="bg-muted px-2 py-1 rounded-md text-sm text-muted-foreground">
|
||||
{commandOrCtrl} + ,
|
||||
<kbd
|
||||
onClick={() =>
|
||||
handleItemSelected({
|
||||
name: "Open preferences",
|
||||
keyName: "OpenPreferences",
|
||||
})
|
||||
}
|
||||
className="bg-muted px-2 py-1 rounded-md text-sm text-muted-foreground cursor-pointer"
|
||||
>
|
||||
{currentHotkeys.OpenPreferences}
|
||||
</kbd>
|
||||
</div>
|
||||
<Separator />
|
||||
@@ -36,8 +70,16 @@ export const Hotkeys = () => {
|
||||
|
||||
<div className="flex items-center justify-between py-4">
|
||||
<div className="flex items-center space-x-2">{t("playOrPause")}</div>
|
||||
<kbd className="bg-muted px-2 py-1 rounded-md text-sm text-muted-foreground">
|
||||
Space
|
||||
<kbd
|
||||
onClick={() =>
|
||||
handleItemSelected({
|
||||
name: t("playOrPause"),
|
||||
keyName: "PlayOrPause",
|
||||
})
|
||||
}
|
||||
className="bg-muted px-2 py-1 rounded-md text-sm text-muted-foreground cursor-pointer"
|
||||
>
|
||||
{currentHotkeys.PlayOrPause}
|
||||
</kbd>
|
||||
</div>
|
||||
|
||||
@@ -47,8 +89,16 @@ export const Hotkeys = () => {
|
||||
<div className="flex items-center space-x-2 capitalize">
|
||||
{t("startOrStopRecording")}
|
||||
</div>
|
||||
<kbd className="bg-muted px-2 py-1 rounded-md text-sm text-muted-foreground">
|
||||
r
|
||||
<kbd
|
||||
onClick={() =>
|
||||
handleItemSelected({
|
||||
name: t("startOrStopRecording"),
|
||||
keyName: "StartOrStopRecording",
|
||||
})
|
||||
}
|
||||
className="bg-muted px-2 py-1 rounded-md text-sm text-muted-foreground cursor-pointer"
|
||||
>
|
||||
{currentHotkeys.StartOrStopRecording}
|
||||
</kbd>
|
||||
</div>
|
||||
|
||||
@@ -58,8 +108,16 @@ export const Hotkeys = () => {
|
||||
<div className="flex items-center space-x-2">
|
||||
{t("playOrPauseRecording")}
|
||||
</div>
|
||||
<kbd className="bg-muted px-2 py-1 rounded-md text-sm text-muted-foreground">
|
||||
{commandOrCtrl} + r
|
||||
<kbd
|
||||
onClick={() =>
|
||||
handleItemSelected({
|
||||
name: t("playOrPauseRecording"),
|
||||
keyName: "PlayOrPauseRecording",
|
||||
})
|
||||
}
|
||||
className="bg-muted px-2 py-1 rounded-md text-sm text-muted-foreground cursor-pointer"
|
||||
>
|
||||
{currentHotkeys.PlayOrPauseRecording}
|
||||
</kbd>
|
||||
</div>
|
||||
|
||||
@@ -69,8 +127,16 @@ export const Hotkeys = () => {
|
||||
<div className="flex items-center space-x-2 capitalize">
|
||||
{t("playPreviousSegment")}
|
||||
</div>
|
||||
<kbd className="bg-muted px-2 py-1 rounded-md text-sm text-muted-foreground">
|
||||
p
|
||||
<kbd
|
||||
onClick={() =>
|
||||
handleItemSelected({
|
||||
name: t("playPreviousSegment"),
|
||||
keyName: "PlayPreviousSegment",
|
||||
})
|
||||
}
|
||||
className="bg-muted px-2 py-1 rounded-md text-sm text-muted-foreground cursor-pointer"
|
||||
>
|
||||
{currentHotkeys.PlayPreviousSegment}
|
||||
</kbd>
|
||||
</div>
|
||||
|
||||
@@ -80,8 +146,16 @@ export const Hotkeys = () => {
|
||||
<div className="flex items-center space-x-2 capitalize">
|
||||
{t("playNextSegment")}
|
||||
</div>
|
||||
<kbd className="bg-muted px-2 py-1 rounded-md text-sm text-muted-foreground">
|
||||
n
|
||||
<kbd
|
||||
onClick={() =>
|
||||
handleItemSelected({
|
||||
name: t("playNextSegment"),
|
||||
keyName: "PlayNextSegment",
|
||||
})
|
||||
}
|
||||
className="bg-muted px-2 py-1 rounded-md text-sm text-muted-foreground cursor-pointer"
|
||||
>
|
||||
{currentHotkeys.PlayNextSegment}
|
||||
</kbd>
|
||||
</div>
|
||||
|
||||
@@ -91,14 +165,28 @@ export const Hotkeys = () => {
|
||||
<div className="flex items-center space-x-2 capitalize">
|
||||
{t("compare")}
|
||||
</div>
|
||||
<kbd className="bg-muted px-2 py-1 rounded-md text-sm text-muted-foreground">
|
||||
c
|
||||
<kbd
|
||||
onClick={() =>
|
||||
handleItemSelected({
|
||||
name: t("compare"),
|
||||
keyName: "Compare",
|
||||
})
|
||||
}
|
||||
className="bg-muted px-2 py-1 rounded-md text-sm text-muted-foreground cursor-pointer"
|
||||
>
|
||||
{currentHotkeys.Compare}
|
||||
</kbd>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
</div>
|
||||
|
||||
<ChangeHotkeyDialog
|
||||
open={open}
|
||||
keyName={selectedItem?.keyName}
|
||||
name={selectedItem?.name}
|
||||
onOpenChange={handleOpenChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user