From f8d9e5e0276854dfe9013485f5e6a176875d7d7b Mon Sep 17 00:00:00 2001 From: GyDi Date: Sun, 18 Sep 2022 15:52:53 +0800 Subject: [PATCH] feat: hotkey viewer --- src/components/setting/mods/hotkey-input.tsx | 96 +++++++++++++ src/components/setting/mods/hotkey-viewer.tsx | 132 ++++++++++++++++++ src/components/setting/setting-verge.tsx | 17 +++ src/services/types.d.ts | 1 + src/utils/parse-hotkey.ts | 29 ++++ 5 files changed, 275 insertions(+) create mode 100644 src/components/setting/mods/hotkey-input.tsx create mode 100644 src/components/setting/mods/hotkey-viewer.tsx create mode 100644 src/utils/parse-hotkey.ts diff --git a/src/components/setting/mods/hotkey-input.tsx b/src/components/setting/mods/hotkey-input.tsx new file mode 100644 index 0000000..63b53eb --- /dev/null +++ b/src/components/setting/mods/hotkey-input.tsx @@ -0,0 +1,96 @@ +import { useState } from "react"; +import { alpha, Box, IconButton, styled } from "@mui/material"; +import { DeleteRounded } from "@mui/icons-material"; +import parseHotkey from "@/utils/parse-hotkey"; + +const KeyWrapper = styled("div")(({ theme }) => ({ + position: "relative", + width: 165, + minHeight: 36, + + "> input": { + position: "absolute", + top: 0, + left: 0, + width: "100%", + height: "100%", + zIndex: 1, + opacity: 0, + }, + "> input:focus + .list": { + borderColor: alpha(theme.palette.primary.main, 0.75), + }, + ".list": { + display: "flex", + alignItems: "center", + flexWrap: "wrap", + width: "100%", + height: "100%", + minHeight: 36, + boxSizing: "border-box", + padding: "3px 4px", + border: "1px solid", + borderRadius: 4, + borderColor: alpha(theme.palette.text.secondary, 0.15), + "&:last-child": { + marginRight: 0, + }, + }, + ".item": { + color: theme.palette.text.primary, + border: "1px solid", + borderColor: alpha(theme.palette.text.secondary, 0.2), + borderRadius: "2px", + padding: "1px 1px", + margin: "2px 0", + marginRight: 8, + }, +})); + +interface Props { + value: string[]; + onChange: (value: string[]) => void; +} + +const HotkeyInput = (props: Props) => { + const { value, onChange } = props; + + return ( + + + { + const evt = e.nativeEvent; + e.preventDefault(); + e.stopPropagation(); + + const key = parseHotkey(evt.key); + if (key === "UNIDENTIFIED") return; + + const newList = [...new Set([...value, key])]; + onChange(newList); + }} + /> + +
+ {value.map((key) => ( +
+ {key} +
+ ))} +
+
+ + onChange([])} + > + + +
+ ); +}; + +export default HotkeyInput; diff --git a/src/components/setting/mods/hotkey-viewer.tsx b/src/components/setting/mods/hotkey-viewer.tsx new file mode 100644 index 0000000..8a66f4f --- /dev/null +++ b/src/components/setting/mods/hotkey-viewer.tsx @@ -0,0 +1,132 @@ +import useSWR from "swr"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useLockFn } from "ahooks"; +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + styled, + Typography, +} from "@mui/material"; +import { getVergeConfig, patchVergeConfig } from "@/services/cmds"; +import { ModalHandler } from "@/hooks/use-modal-handler"; +import Notice from "@/components/base/base-notice"; +import HotkeyInput from "./hotkey-input"; + +const ItemWrapper = styled("div")` + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; +`; + +const HOTKEY_FUNC = [ + "clash_mode_rule", + "clash_mode_direct", + "clash_mode_global", + "clash_moda_script", + "toggle_system_proxy", + "enable_system_proxy", + "disable_system_proxy", + "toggle_tun_mode", + "enable_tun_mode", + "disable_tun_mode", +]; + +interface Props { + handler: ModalHandler; +} + +const HotkeyViewer = ({ handler }: Props) => { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + + if (handler) { + handler.current = { + open: () => setOpen(true), + close: () => setOpen(false), + }; + } + + const { data: vergeConfig, mutate: mutateVerge } = useSWR( + "getVergeConfig", + getVergeConfig + ); + + const [hotkeyMap, setHotkeyMap] = useState>({}); + + useEffect(() => { + if (!open) return; + const map = {} as typeof hotkeyMap; + + vergeConfig?.hotkeys?.forEach((text) => { + const [func, key] = text.split(",").map((e) => e.trim()); + + if (!func || !key) return; + + map[func] = key + .split("+") + .map((e) => e.trim()) + .map((k) => (k === "PLUS" ? "+" : k)); + }); + + setHotkeyMap(map); + }, [vergeConfig?.hotkeys, open]); + + const onSave = useLockFn(async () => { + const hotkeys = Object.entries(hotkeyMap) + .map(([func, keys]) => { + if (!func || !keys?.length) return ""; + + const key = keys + .map((k) => k.trim()) + .filter(Boolean) + .map((k) => (k === "+" ? "PLUS" : k)) + .join("+"); + + if (!key) return ""; + return `${func},${key}`; + }) + .filter(Boolean); + + try { + patchVergeConfig({ hotkeys }); + setOpen(false); + mutateVerge(); + } catch (err: any) { + Notice.error(err.message || err.toString()); + } + }); + + return ( + setOpen(false)}> + {t("Hotkey Viewer")} + + + {HOTKEY_FUNC.map((func) => ( + + {t(func)} + setHotkeyMap((m) => ({ ...m, [func]: v }))} + /> + + ))} + + + + + + + + ); +}; + +export default HotkeyViewer; diff --git a/src/components/setting/setting-verge.tsx b/src/components/setting/setting-verge.tsx index ae3b69d..8eaff19 100644 --- a/src/components/setting/setting-verge.tsx +++ b/src/components/setting/setting-verge.tsx @@ -17,8 +17,10 @@ import { import { ArrowForward } from "@mui/icons-material"; import { SettingList, SettingItem } from "./setting"; import { version } from "@root/package.json"; +import useModalHandler from "@/hooks/use-modal-handler"; import ThemeModeSwitch from "./mods/theme-mode-switch"; import ConfigViewer from "./mods/config-viewer"; +import HotkeyViewer from "./mods/hotkey-viewer"; import GuardState from "./mods/guard-state"; import SettingTheme from "./setting-theme"; @@ -43,8 +45,12 @@ const SettingVerge = ({ onError }: Props) => { mutateVerge({ ...vergeConfig, ...patch }, false); }; + const hotkeyHandler = useModalHandler(); + return ( + + { + + hotkeyHandler.current.open()} + > + + + + { + let temp = key.toUpperCase(); + + if (temp.startsWith("ARROW")) { + temp = temp.slice(5); + } else if (temp.startsWith("DIGIT")) { + temp = temp.slice(5); + } else if (temp.startsWith("KEY")) { + temp = temp.slice(3); + } else if (temp.endsWith("LEFT")) { + temp = temp.slice(0, -4); + } else if (temp.endsWith("RIGHT")) { + temp = temp.slice(0, -5); + } + + switch (temp) { + case "CONTROL": + return "CTRL"; + case "META": + return "CMD"; + case " ": + return "SPACE"; + + default: + return temp; + } +}; + +export default parseHotkey;