Skip to content

Commit 024abc4

Browse files
authored
Merge pull request #88 from imguoguo/add-custom-shortcut
feat: add custom shortcut
2 parents 699e132 + 7dfebfb commit 024abc4

File tree

34 files changed

+796
-188
lines changed

34 files changed

+796
-188
lines changed

browser/src/components/keyboard/index.tsx

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ export const Keyboard = () => {
8787

8888
// send keyboard data
8989
async function sendKeyData(event: KeyboardEvent) {
90-
const modifiers = getModifiers(event);
90+
const modifiers = Modifiers.getModifiers(event, pressedModifiersRef.current);
9191
const keys = [
9292
0x00,
9393
0x00,
@@ -98,32 +98,5 @@ export const Keyboard = () => {
9898
await device.sendKeyboardData(modifiers, keys);
9999
}
100100

101-
function getModifiers(event: KeyboardEvent) {
102-
const modifiers = new Modifiers();
103-
104-
if (event.ctrlKey) {
105-
modifiers.leftCtrl = pressedModifiersRef.current.has('ControlLeft');
106-
modifiers.rightCtrl = pressedModifiersRef.current.has('ControlRight');
107-
}
108-
if (event.shiftKey) {
109-
modifiers.leftShift = pressedModifiersRef.current.has('ShiftLeft');
110-
modifiers.rightShift = pressedModifiersRef.current.has('ShiftRight');
111-
}
112-
if (event.altKey) {
113-
modifiers.leftAlt = pressedModifiersRef.current.has('AltLeft');
114-
modifiers.rightAlt = pressedModifiersRef.current.has('AltRight');
115-
}
116-
if (event.metaKey) {
117-
modifiers.leftWindows = pressedModifiersRef.current.has('MetaLeft');
118-
modifiers.rightWindows = pressedModifiersRef.current.has('MetaRight');
119-
}
120-
if (event.getModifierState('AltGraph')) {
121-
modifiers.leftCtrl = true;
122-
modifiers.rightAlt = true;
123-
}
124-
125-
return modifiers;
126-
}
127-
128101
return <></>;
129102
};
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { useEffect, useRef, useState } from 'react';
2+
import { Input, InputRef, Modal, Space } from 'antd';
3+
import { useSetAtom } from 'jotai';
4+
import { KeyboardIcon, SquarePenIcon } from 'lucide-react';
5+
import { useTranslation } from 'react-i18next';
6+
import { isKeyboardEnableAtom } from '@/jotai/keyboard.ts';
7+
import { modifierKeys, Modifiers, ShortcutProps } from '@/libs/device/keyboard.ts';
8+
9+
interface KeyboardShortcutCustomProps {
10+
addShortcut: (shortcut: ShortcutProps) => void;
11+
}
12+
13+
export const KeyboardShortcutCustom = ({ addShortcut }: KeyboardShortcutCustomProps) => {
14+
const { t } = useTranslation();
15+
const setIsKeyboardEnable = useSetAtom(isKeyboardEnableAtom);
16+
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
17+
const [isCapturingShortcut, setIsCapturingShortcut] = useState<boolean>(false);
18+
const [shortcut, setShortcut] = useState<ShortcutProps | null>(null);
19+
const [shortcutLabel, setShortcutLabel] = useState<string>('');
20+
const [shortcutFixedLabel, setShortcutFixedLabel] = useState<string>('');
21+
const pressedModifiersRef = useRef<Set<string>>(new Set());
22+
const inputRef = useRef<InputRef>(null);
23+
24+
function getPressedShortcutString(event: KeyboardEvent) {
25+
let pressedKey: string = '';
26+
pressedKey += event.ctrlKey ? 'Ctrl + ' : '';
27+
pressedKey += event.shiftKey ? 'Shift + ' : '';
28+
pressedKey += event.altKey ? 'Alt + ' : '';
29+
pressedKey += event.metaKey ? 'Meta + ' : '';
30+
if (!modifierKeys.has(event.key)) {
31+
if (event.code.startsWith('Key') || event.code.startsWith('Digit')) {
32+
pressedKey += event.code.slice(-1);
33+
} else {
34+
pressedKey += event.key;
35+
}
36+
}
37+
return pressedKey;
38+
}
39+
40+
function handleModelDone() {
41+
if (shortcut == null) {
42+
cleanShortcut();
43+
return;
44+
}
45+
addShortcut({
46+
modifiers: shortcut.modifiers,
47+
label: shortcutLabel == '' ? shortcutFixedLabel : shortcutLabel,
48+
keyCode: shortcut.keyCode
49+
});
50+
cleanShortcut();
51+
}
52+
53+
function cleanShortcut() {
54+
setIsCapturingShortcut(false);
55+
setIsModalOpen(false);
56+
setShortcut(null);
57+
setShortcutLabel('');
58+
setShortcutFixedLabel('');
59+
}
60+
61+
async function handleKeyDown(event: KeyboardEvent) {
62+
event.preventDefault();
63+
event.stopPropagation();
64+
65+
const label = getPressedShortcutString(event);
66+
setShortcutFixedLabel(label);
67+
68+
if (modifierKeys.has(event.key)) {
69+
if (event.type == 'keydown') {
70+
pressedModifiersRef.current.add(event.code);
71+
} else {
72+
pressedModifiersRef.current.delete(event.code);
73+
}
74+
} else {
75+
setIsCapturingShortcut(false);
76+
window.removeEventListener('keydown', handleKeyDown);
77+
window.removeEventListener('keyup', handleKeyDown);
78+
const modifiers = Modifiers.getModifiers(event, pressedModifiersRef.current);
79+
const ret: ShortcutProps = {
80+
modifiers: modifiers,
81+
label: label,
82+
keyCode: event.code
83+
};
84+
setShortcut(ret);
85+
86+
}
87+
}
88+
89+
function handleFullscreen() {
90+
if (!document.fullscreenElement) {
91+
const element = document.documentElement;
92+
element.requestFullscreen().then();
93+
// @ts-expect-error - https://developer.mozilla.org/en-US/docs/Web/API/Keyboard/lock
94+
navigator.keyboard?.lock();
95+
}
96+
}
97+
98+
useEffect(() => {
99+
setIsKeyboardEnable(!isModalOpen);
100+
}, [isModalOpen]);
101+
102+
useEffect(() => {
103+
if (inputRef.current && !isCapturingShortcut) {
104+
inputRef.current.blur();
105+
}
106+
}, [isCapturingShortcut]);
107+
108+
return (
109+
<>
110+
<div
111+
className="flex h-[30px] cursor-pointer items-center space-x-1 rounded px-3 text-neutral-300 hover:bg-neutral-700/60"
112+
onClick={() => {
113+
setIsModalOpen(true);
114+
}}
115+
>
116+
<span>{t('keyboard.shortcut.custom')}</span>
117+
</div>
118+
<Modal
119+
width={400}
120+
title={t('keyboard.shortcut.custom')}
121+
open={isModalOpen}
122+
onOk={handleModelDone}
123+
onCancel={cleanShortcut}
124+
okText={t('keyboard.shortcut.save')}
125+
cancelText={t('keyboard.shortcut.cancel')}>
126+
<Space direction="vertical" style={{ width: '100%' }} size="middle">
127+
<div className="flex">
128+
{t('keyboard.shortcut.captureTips')}
129+
<a onClick={handleFullscreen}>{t('keyboard.shortcut.enterFullScreen')}</a>
130+
</div>
131+
<Input
132+
ref={inputRef}
133+
placeholder={t('keyboard.shortcut.capture')}
134+
value={shortcutFixedLabel}
135+
prefix={<KeyboardIcon size={16} />}
136+
onFocus={() => {
137+
setIsCapturingShortcut(true);
138+
window.addEventListener('keydown', handleKeyDown);
139+
window.addEventListener('keyup', handleKeyDown);
140+
}}
141+
onBlur={() => {
142+
setIsCapturingShortcut(false);
143+
window.removeEventListener('keydown', handleKeyDown);
144+
window.removeEventListener('keyup', handleKeyDown);
145+
}}
146+
/>
147+
148+
<Input
149+
placeholder={shortcutFixedLabel || t('keyboard.shortcut.label')}
150+
value={shortcutLabel}
151+
prefix={<SquarePenIcon size={16} />}
152+
onChange={(e) => setShortcutLabel(e.target.value)}
153+
/>
154+
155+
</Space>
156+
</Modal>
157+
</>
158+
);
159+
};

browser/src/components/menu/keyboard/shortcut.tsx

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
1-
import { useState } from 'react';
2-
import { SendHorizonal } from 'lucide-react';
3-
1+
import { useState, PropsWithChildren } from 'react';
42
import { device } from '@/libs/device';
53
import { Modifiers } from '@/libs/device/keyboard.ts';
64
import { KeyboardCodes } from '@/libs/keyboard';
5+
import { ShortcutProps } from '@/libs/device/keyboard.ts'
76

8-
interface ShortcutProps {
9-
label: string;
10-
modifiers?: Partial<Modifiers>;
11-
keyCode: string;
12-
}
7+
type ShortcutPropsWithChildren = ShortcutProps & PropsWithChildren
138

14-
export const Shortcut = ({ label, modifiers = {}, keyCode }: ShortcutProps) => {
9+
export const Shortcut = ({ label, modifiers = {}, keyCode , children}: ShortcutPropsWithChildren) => {
1510
const [isLoading, setIsLoading] = useState(false);
1611

1712
async function handleClick(): Promise<void> {
@@ -40,8 +35,8 @@ export const Shortcut = ({ label, modifiers = {}, keyCode }: ShortcutProps) => {
4035
className="flex h-[30px] cursor-pointer items-center space-x-1 rounded px-3 text-neutral-300 hover:bg-neutral-700/60"
4136
onClick={handleClick}
4237
>
43-
<SendHorizonal size={18} />
44-
<span>{label}</span>
38+
<span className="w-full">{label}</span>
39+
{children}
4540
</div>
4641
);
4742
};

browser/src/components/menu/keyboard/shortcuts-menu.tsx

Lines changed: 72 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,86 @@
1-
import { useState } from 'react';
2-
import { SendHorizonal } from 'lucide-react';
1+
import { useEffect, useState } from 'react';
2+
import { Button, Divider, Popover } from 'antd';
3+
import { SendHorizonal, Trash } from 'lucide-react';
34
import { useTranslation } from 'react-i18next';
4-
import { Popover } from 'antd';
5-
5+
import { ShortcutProps } from '@/libs/device/keyboard.ts';
66
import { Shortcut } from './shortcut.tsx';
7+
import { KeyboardShortcutCustom } from './shortcut-custom.tsx';
8+
import { getShortcuts, setShortcuts } from '@/libs/storage';
79

810
export const KeyboardShortcutsMenu = () => {
911
const { t } = useTranslation();
1012
const [open, setOpen] = useState(false);
13+
const [storedShortcuts, setStoredShortcuts] = useState<ShortcutProps[]>([]);
14+
const predefinedShortcuts : ShortcutProps[] = [
15+
{
16+
label: t('keyboard.shortcut.ctrlAltDel'),
17+
modifiers: { leftCtrl: true, leftAlt: true },
18+
keyCode: 'Delete',
19+
},
20+
{
21+
label: t('keyboard.shortcut.ctrlD'),
22+
modifiers: { leftCtrl: true },
23+
keyCode: 'KeyD',
24+
},
25+
{
26+
label: t('keyboard.shortcut.winTab'),
27+
modifiers: { leftWindows: true },
28+
keyCode: 'Tab',
29+
},
30+
]
31+
32+
const addShortcut = (shortcut: ShortcutProps) => {
33+
if (shortcut == null) return;
34+
const shortcuts = getShortcuts();
35+
shortcuts.push(shortcut);
36+
setStoredShortcuts(shortcuts);
37+
setShortcuts(shortcuts);
38+
}
39+
40+
const removeShortcut = (indexToRemove: number) => {
41+
const newShortcuts = storedShortcuts.filter(
42+
(_, index) => index !== indexToRemove
43+
);
44+
setStoredShortcuts(newShortcuts);
45+
setShortcuts(newShortcuts);
46+
}
47+
48+
useEffect(() => {
49+
const shortcuts = getShortcuts();
50+
if (shortcuts.length === 0) {
51+
predefinedShortcuts.forEach(shortcut => {
52+
addShortcut(shortcut);
53+
})
54+
} else {
55+
setStoredShortcuts(shortcuts);
56+
}
57+
}, []);
1158

1259
return (
1360
<Popover
1461
content={
1562
<div className="flex flex-col gap-1">
16-
{[
17-
{
18-
label: t('keyboard.ctrlAltDel'),
19-
modifiers: { leftCtrl: true, leftAlt: true },
20-
keyCode: 'Delete',
21-
},
22-
{
23-
label: t('keyboard.ctrlD'),
24-
modifiers: { leftCtrl: true },
25-
keyCode: 'KeyD',
26-
},
27-
{
28-
label: t('keyboard.winTab'),
29-
modifiers: { leftWindows: true },
30-
keyCode: 'Tab',
31-
},
32-
].map((shortcut) => (
33-
<Shortcut
34-
key={shortcut.keyCode}
35-
label={shortcut.label}
36-
modifiers={shortcut.modifiers}
37-
keyCode={shortcut.keyCode}
38-
/>
63+
{storedShortcuts.map((shortcut, index) => (
64+
<>
65+
<Shortcut
66+
key={index}
67+
label={shortcut.label}
68+
modifiers={shortcut.modifiers}
69+
keyCode={shortcut.keyCode}
70+
>
71+
<Button
72+
className="ml-auto"
73+
type="text"
74+
danger
75+
icon={<Trash size={16} />}
76+
onClick={() => {removeShortcut(index)}}
77+
/>
78+
</Shortcut>
79+
</>
3980
))}
81+
<Divider style={{ margin: '5px 0 5px 0' }} />
82+
<KeyboardShortcutCustom
83+
addShortcut={addShortcut}/>
4084
</div>
4185
}
4286
trigger="click"
@@ -48,7 +92,7 @@ export const KeyboardShortcutsMenu = () => {
4892
>
4993
<div className="flex h-[30px] cursor-pointer items-center space-x-1 rounded px-3 text-neutral-300 hover:bg-neutral-700/60">
5094
<SendHorizonal size={18} />
51-
<span>{t('keyboard.shortcuts')}</span>
95+
<span>{t('keyboard.shortcut.title')}</span>
5296
</div>
5397
</Popover>
5498
);

browser/src/i18n/locales/be.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,12 @@ const be = {
3838
keyboard: {
3939
paste: 'Plakken',
4040
virtualKeyboard: 'Virtueel klavier',
41-
shortcuts: 'Sneltoetsen',
42-
ctrlAltDel: 'Ctrl + Alt + Delete',
43-
ctrlD: 'Ctrl + D',
44-
winTab: 'Win + Tab',
41+
shortcut: {
42+
title: 'Sneltoetsen',
43+
ctrlAltDel: 'Ctrl + Alt + Delete',
44+
ctrlD: 'Ctrl + D',
45+
winTab: 'Win + Tab',
46+
},
4547
},
4648
mouse: {
4749
cursor: {

browser/src/i18n/locales/de.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,12 @@ const de = {
3838
keyboard: {
3939
paste: 'Einfügen',
4040
virtualKeyboard: 'Virtuelle Tastatur',
41-
shortcuts: 'Tastenkürzel',
42-
ctrlAltDel: 'Strg + Alt + Entfernen',
43-
ctrlD: 'Strg + D',
44-
winTab: 'Win + Tab',
41+
shortcut: {
42+
title: 'Tastenkürzel',
43+
ctrlAltDel: 'Strg + Alt + Entfernen',
44+
ctrlD: 'Strg + D',
45+
winTab: 'Win + Tab',
46+
},
4547
},
4648
mouse: {
4749
cursor: {

0 commit comments

Comments
 (0)