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 { 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"
|
||||||
|
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;
|
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
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