feat: hotkey viewer

This commit is contained in:
GyDi 2022-09-18 15:52:53 +08:00
parent 8fa7fb3b1f
commit f8d9e5e027
No known key found for this signature in database
GPG Key ID: 58B15242BA8277A6
5 changed files with 275 additions and 0 deletions

View File

@ -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 (
<Box sx={{ display: "flex", alignItems: "center" }}>
<KeyWrapper>
<input
onKeyDown={(e) => {
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);
}}
/>
<div className="list">
{value.map((key) => (
<div key={key} className="item">
{key}
</div>
))}
</div>
</KeyWrapper>
<IconButton
size="small"
title="Delete"
color="inherit"
onClick={() => onChange([])}
>
<DeleteRounded fontSize="inherit" />
</IconButton>
</Box>
);
};
export default HotkeyInput;

View File

@ -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<Record<string, string[]>>({});
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 (
<Dialog open={open} onClose={() => setOpen(false)}>
<DialogTitle>{t("Hotkey Viewer")}</DialogTitle>
<DialogContent sx={{ width: 450, maxHeight: 330 }}>
{HOTKEY_FUNC.map((func) => (
<ItemWrapper key={func}>
<Typography>{t(func)}</Typography>
<HotkeyInput
value={hotkeyMap[func] ?? []}
onChange={(v) => setHotkeyMap((m) => ({ ...m, [func]: v }))}
/>
</ItemWrapper>
))}
</DialogContent>
<DialogActions>
<Button variant="outlined" onClick={() => setOpen(false)}>
{t("Cancel")}
</Button>
<Button onClick={onSave} variant="contained">
{t("Save")}
</Button>
</DialogActions>
</Dialog>
);
};
export default HotkeyViewer;

View File

@ -17,8 +17,10 @@ import {
import { ArrowForward } from "@mui/icons-material"; import { ArrowForward } from "@mui/icons-material";
import { SettingList, SettingItem } from "./setting"; import { SettingList, SettingItem } from "./setting";
import { version } from "@root/package.json"; import { version } from "@root/package.json";
import useModalHandler from "@/hooks/use-modal-handler";
import ThemeModeSwitch from "./mods/theme-mode-switch"; import ThemeModeSwitch from "./mods/theme-mode-switch";
import ConfigViewer from "./mods/config-viewer"; import ConfigViewer from "./mods/config-viewer";
import HotkeyViewer from "./mods/hotkey-viewer";
import GuardState from "./mods/guard-state"; import GuardState from "./mods/guard-state";
import SettingTheme from "./setting-theme"; import SettingTheme from "./setting-theme";
@ -43,8 +45,12 @@ const SettingVerge = ({ onError }: Props) => {
mutateVerge({ ...vergeConfig, ...patch }, false); mutateVerge({ ...vergeConfig, ...patch }, false);
}; };
const hotkeyHandler = useModalHandler();
return ( return (
<SettingList title={t("Verge Setting")}> <SettingList title={t("Verge Setting")}>
<HotkeyViewer handler={hotkeyHandler} />
<SettingItem label={t("Language")}> <SettingItem label={t("Language")}>
<GuardState <GuardState
value={language ?? "en"} value={language ?? "en"}
@ -108,6 +114,17 @@ const SettingVerge = ({ onError }: Props) => {
</IconButton> </IconButton>
</SettingItem> </SettingItem>
<SettingItem label={t("Hotkey Setting")}>
<IconButton
color="inherit"
size="small"
sx={{ my: "2px" }}
onClick={() => hotkeyHandler.current.open()}
>
<ArrowForward />
</IconButton>
</SettingItem>
<SettingItem label={t("Runtime Config")}> <SettingItem label={t("Runtime Config")}>
<IconButton <IconButton
color="inherit" color="inherit"

View File

@ -147,6 +147,7 @@ declare namespace CmdType {
proxy_guard_duration?: number; proxy_guard_duration?: number;
system_proxy_bypass?: string; system_proxy_bypass?: string;
web_ui_list?: string[]; web_ui_list?: string[];
hotkeys?: string[];
theme_setting?: { theme_setting?: {
primary_color?: string; primary_color?: string;
secondary_color?: string; secondary_color?: string;

29
src/utils/parse-hotkey.ts Normal file
View File

@ -0,0 +1,29 @@
const parseHotkey = (key: string) => {
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;