feat: clash field viewer wip

This commit is contained in:
GyDi 2022-08-08 01:51:30 +08:00
parent 35de2334fb
commit 066b08040a
No known key found for this signature in database
GPG Key ID: 1C95E0D3467B3084
5 changed files with 212 additions and 12 deletions

View File

@ -0,0 +1,169 @@
import useSWR from "swr";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import {
Button,
Checkbox,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Divider,
Stack,
Tooltip,
Typography,
} from "@mui/material";
import { changeProfileValid, getProfiles } from "@/services/cmds";
import { ModalHandler } from "@/hooks/use-modal-handler";
import enhance, {
DEFAULT_FIELDS,
HANDLE_FIELDS,
USE_FLAG_FIELDS,
} from "@/services/enhance";
import { BuildCircleRounded, InfoRounded } from "@mui/icons-material";
interface Props {
handler: ModalHandler;
onError: (err: Error) => void;
}
const fieldSorter = (a: string, b: string) => {
if (a.includes("-") === a.includes("-")) {
if (a.length === b.length) return a.localeCompare(b);
return a.length - b.length;
} else if (a.includes("-")) return 1;
else if (b.includes("-")) return -1;
return 0;
};
const useFields = [...USE_FLAG_FIELDS].sort(fieldSorter);
const handleFields = [...HANDLE_FIELDS, ...DEFAULT_FIELDS].sort(fieldSorter);
const ClashFieldViewer = ({ handler, onError }: Props) => {
const { t } = useTranslation();
const { data, mutate } = useSWR("getProfiles", getProfiles);
const [open, setOpen] = useState(false);
const [selected, setSelected] = useState<string[]>([]);
const { config: enhanceConfig, use: enhanceUse } = enhance.getFieldsState();
if (handler) {
handler.current = {
open: () => setOpen(true),
close: () => setOpen(false),
};
}
console.log("render");
useEffect(() => {
if (open) mutate();
}, [open]);
useEffect(() => {
setSelected([...(data?.valid || []), ...enhanceUse]);
}, [data?.valid, enhanceUse]);
const handleChange = (item: string) => {
if (!item) return;
setSelected((old) =>
old.includes(item) ? old.filter((e) => e !== item) : [...old, item]
);
};
const handleSave = async () => {
setOpen(false);
const oldSet = new Set([...(data?.valid || []), ...enhanceUse]);
const curSet = new Set(selected.concat([...oldSet]));
if (curSet.size === oldSet.size) return;
try {
await changeProfileValid([...new Set(selected)]);
mutate();
} catch (err: any) {
onError(err);
}
};
return (
<Dialog open={open} onClose={() => setOpen(false)}>
<DialogTitle>{t("Clash Field")}</DialogTitle>
<DialogContent
sx={{
pb: 0,
width: 320,
height: 300,
overflowY: "auto",
userSelect: "text",
}}
>
{useFields.map((item) => {
const inSelect = selected.includes(item);
const inConfig = enhanceConfig.includes(item);
const inConfigUse = enhanceUse.includes(item);
const inValid = data?.valid?.includes(item);
return (
<Stack key={item} mb={0.5} direction="row" alignItems="center">
<Checkbox
checked={inSelect}
size="small"
sx={{ p: 0.5 }}
onChange={() => handleChange(item)}
/>
<Typography width="100%">{item}</Typography>
{inConfigUse && !inValid && <InfoIcon />}
{!inSelect && inConfig && <WarnIcon />}
</Stack>
);
})}
<Divider sx={{ my: 0.5 }} />
{handleFields.map((item) => (
<Stack key={item} mb={0.5} direction="row" alignItems="center">
<Checkbox defaultChecked disabled size="small" sx={{ p: 0.5 }} />
<Typography>{item}</Typography>
</Stack>
))}
</DialogContent>
<DialogActions>
<Button variant="outlined" onClick={() => setOpen(false)}>
{t("Back")}
</Button>
<Button variant="contained" onClick={handleSave}>
{t("Save")}
</Button>
</DialogActions>
</Dialog>
);
};
function WarnIcon() {
return (
<Tooltip title="The field exists in the config but not enabled.">
<InfoRounded color="warning" sx={{ cursor: "pointer", opacity: 0.5 }} />
</Tooltip>
);
}
function InfoIcon() {
return (
<Tooltip title="This field is provided by Merge Profile.">
<BuildCircleRounded
color="info"
sx={{ cursor: "pointer", opacity: 0.5 }}
/>
</Tooltip>
);
}
export default ClashFieldViewer;

View File

@ -19,6 +19,7 @@ import Notice from "../base/base-notice";
import GuardState from "./mods/guard-state";
import CoreSwitch from "./mods/core-switch";
import WebUIViewer from "./mods/web-ui-viewer";
import ClashFieldViewer from "./mods/clash-field-viewer";
interface Props {
onError: (err: Error) => void;
@ -40,6 +41,7 @@ const SettingClash = ({ onError }: Props) => {
const setGlobalClashPort = useSetRecoilState(atomClashPort);
const webUIHandler = useModalHandler();
const fieldHandler = useModalHandler();
const onSwitchFormat = (_e: any, value: boolean) => value;
const onChangeData = (patch: Partial<ApiType.ConfigData>) => {
@ -73,6 +75,7 @@ const SettingClash = ({ onError }: Props) => {
return (
<SettingList title={t("Clash Setting")}>
<WebUIViewer handler={webUIHandler} onError={onError} />
<ClashFieldViewer handler={fieldHandler} onError={onError} />
<SettingItem label={t("Allow Lan")}>
<GuardState
@ -100,17 +103,6 @@ const SettingClash = ({ onError }: Props) => {
</GuardState>
</SettingItem>
<SettingItem label={t("Web UI")}>
<IconButton
color="inherit"
size="small"
sx={{ my: "2px" }}
onClick={() => webUIHandler.current.open()}
>
<ArrowForward />
</IconButton>
</SettingItem>
<SettingItem label={t("Log Level")}>
<GuardState
value={logLevel ?? "info"}
@ -146,6 +138,28 @@ const SettingClash = ({ onError }: Props) => {
</GuardState>
</SettingItem>
<SettingItem label={t("Web UI")}>
<IconButton
color="inherit"
size="small"
sx={{ my: "2px" }}
onClick={() => webUIHandler.current.open()}
>
<ArrowForward />
</IconButton>
</SettingItem>
<SettingItem label={t("Clash Field")}>
<IconButton
color="inherit"
size="small"
sx={{ my: "2px" }}
onClick={() => fieldHandler.current.open()}
>
<ArrowForward />
</IconButton>
</SettingItem>
<SettingItem label={t("Clash Core")} extra={<CoreSwitch />}>
<Typography sx={{ py: "7px" }}>{clashVer}</Typography>
</SettingItem>

View File

@ -59,6 +59,7 @@
"theme.light": "浅色",
"theme.dark": "深色",
"theme.system": "系统",
"Clash Field": "Clash 字段",
"Back": "返回",
"Save": "保存",

View File

@ -12,6 +12,7 @@ export const HANDLE_FIELDS = [
"mode",
"log-level",
"ipv6",
"secret",
"external-controller",
];
@ -131,6 +132,12 @@ class Enhance {
private listenMap: Map<string, EListener>;
private resultMap: Map<string, EStatus>;
// record current config fields
private fieldsState = {
config: [] as string[],
use: [] as string[],
};
constructor() {
this.listenMap = new Map();
this.resultMap = new Map();
@ -148,6 +155,11 @@ class Enhance {
return this.resultMap.get(uid);
}
// get the running field state
getFieldsState() {
return this.fieldsState;
}
async enhanceHandler(event: Event<unknown>) {
const payload = event.payload as CmdType.EnhancedPayload;
@ -220,6 +232,10 @@ class Enhance {
pdata = ignoreCase(pdata);
// save the fields state
this.fieldsState.config = Object.keys(pdata);
this.fieldsState.use = [...useList];
// filter the data
const filterData: typeof pdata = {};
Object.keys(pdata).forEach((key: any) => {

View File

@ -2,7 +2,7 @@
type TData = Record<string, any>;
export default function ignoreCase(data: TData): TData {
if (!data) return data;
if (!data) return {};
const newData = {} as TData;