feat: hotkey viewer
This commit is contained in:
parent
8fa7fb3b1f
commit
f8d9e5e027
96
src/components/setting/mods/hotkey-input.tsx
Normal file
96
src/components/setting/mods/hotkey-input.tsx
Normal 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;
|
132
src/components/setting/mods/hotkey-viewer.tsx
Normal file
132
src/components/setting/mods/hotkey-viewer.tsx
Normal 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;
|
@ -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 (
|
||||
<SettingList title={t("Verge Setting")}>
|
||||
<HotkeyViewer handler={hotkeyHandler} />
|
||||
|
||||
<SettingItem label={t("Language")}>
|
||||
<GuardState
|
||||
value={language ?? "en"}
|
||||
@ -108,6 +114,17 @@ const SettingVerge = ({ onError }: Props) => {
|
||||
</IconButton>
|
||||
</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")}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
|
1
src/services/types.d.ts
vendored
1
src/services/types.d.ts
vendored
@ -147,6 +147,7 @@ declare namespace CmdType {
|
||||
proxy_guard_duration?: number;
|
||||
system_proxy_bypass?: string;
|
||||
web_ui_list?: string[];
|
||||
hotkeys?: string[];
|
||||
theme_setting?: {
|
||||
primary_color?: string;
|
||||
secondary_color?: string;
|
||||
|
29
src/utils/parse-hotkey.ts
Normal file
29
src/utils/parse-hotkey.ts
Normal 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;
|
Loading…
Reference in New Issue
Block a user