feat: clash field viewer wip
This commit is contained in:
parent
35de2334fb
commit
066b08040a
169
src/components/setting/mods/clash-field-viewer.tsx
Normal file
169
src/components/setting/mods/clash-field-viewer.tsx
Normal 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;
|
@ -19,6 +19,7 @@ import Notice from "../base/base-notice";
|
|||||||
import GuardState from "./mods/guard-state";
|
import GuardState from "./mods/guard-state";
|
||||||
import CoreSwitch from "./mods/core-switch";
|
import CoreSwitch from "./mods/core-switch";
|
||||||
import WebUIViewer from "./mods/web-ui-viewer";
|
import WebUIViewer from "./mods/web-ui-viewer";
|
||||||
|
import ClashFieldViewer from "./mods/clash-field-viewer";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onError: (err: Error) => void;
|
onError: (err: Error) => void;
|
||||||
@ -40,6 +41,7 @@ const SettingClash = ({ onError }: Props) => {
|
|||||||
const setGlobalClashPort = useSetRecoilState(atomClashPort);
|
const setGlobalClashPort = useSetRecoilState(atomClashPort);
|
||||||
|
|
||||||
const webUIHandler = useModalHandler();
|
const webUIHandler = useModalHandler();
|
||||||
|
const fieldHandler = useModalHandler();
|
||||||
|
|
||||||
const onSwitchFormat = (_e: any, value: boolean) => value;
|
const onSwitchFormat = (_e: any, value: boolean) => value;
|
||||||
const onChangeData = (patch: Partial<ApiType.ConfigData>) => {
|
const onChangeData = (patch: Partial<ApiType.ConfigData>) => {
|
||||||
@ -73,6 +75,7 @@ const SettingClash = ({ onError }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<SettingList title={t("Clash Setting")}>
|
<SettingList title={t("Clash Setting")}>
|
||||||
<WebUIViewer handler={webUIHandler} onError={onError} />
|
<WebUIViewer handler={webUIHandler} onError={onError} />
|
||||||
|
<ClashFieldViewer handler={fieldHandler} onError={onError} />
|
||||||
|
|
||||||
<SettingItem label={t("Allow Lan")}>
|
<SettingItem label={t("Allow Lan")}>
|
||||||
<GuardState
|
<GuardState
|
||||||
@ -100,17 +103,6 @@ const SettingClash = ({ onError }: Props) => {
|
|||||||
</GuardState>
|
</GuardState>
|
||||||
</SettingItem>
|
</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")}>
|
<SettingItem label={t("Log Level")}>
|
||||||
<GuardState
|
<GuardState
|
||||||
value={logLevel ?? "info"}
|
value={logLevel ?? "info"}
|
||||||
@ -146,6 +138,28 @@ const SettingClash = ({ onError }: Props) => {
|
|||||||
</GuardState>
|
</GuardState>
|
||||||
</SettingItem>
|
</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 />}>
|
<SettingItem label={t("Clash Core")} extra={<CoreSwitch />}>
|
||||||
<Typography sx={{ py: "7px" }}>{clashVer}</Typography>
|
<Typography sx={{ py: "7px" }}>{clashVer}</Typography>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
@ -59,6 +59,7 @@
|
|||||||
"theme.light": "浅色",
|
"theme.light": "浅色",
|
||||||
"theme.dark": "深色",
|
"theme.dark": "深色",
|
||||||
"theme.system": "系统",
|
"theme.system": "系统",
|
||||||
|
"Clash Field": "Clash 字段",
|
||||||
|
|
||||||
"Back": "返回",
|
"Back": "返回",
|
||||||
"Save": "保存",
|
"Save": "保存",
|
||||||
|
@ -12,6 +12,7 @@ export const HANDLE_FIELDS = [
|
|||||||
"mode",
|
"mode",
|
||||||
"log-level",
|
"log-level",
|
||||||
"ipv6",
|
"ipv6",
|
||||||
|
"secret",
|
||||||
"external-controller",
|
"external-controller",
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -131,6 +132,12 @@ class Enhance {
|
|||||||
private listenMap: Map<string, EListener>;
|
private listenMap: Map<string, EListener>;
|
||||||
private resultMap: Map<string, EStatus>;
|
private resultMap: Map<string, EStatus>;
|
||||||
|
|
||||||
|
// record current config fields
|
||||||
|
private fieldsState = {
|
||||||
|
config: [] as string[],
|
||||||
|
use: [] as string[],
|
||||||
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.listenMap = new Map();
|
this.listenMap = new Map();
|
||||||
this.resultMap = new Map();
|
this.resultMap = new Map();
|
||||||
@ -148,6 +155,11 @@ class Enhance {
|
|||||||
return this.resultMap.get(uid);
|
return this.resultMap.get(uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get the running field state
|
||||||
|
getFieldsState() {
|
||||||
|
return this.fieldsState;
|
||||||
|
}
|
||||||
|
|
||||||
async enhanceHandler(event: Event<unknown>) {
|
async enhanceHandler(event: Event<unknown>) {
|
||||||
const payload = event.payload as CmdType.EnhancedPayload;
|
const payload = event.payload as CmdType.EnhancedPayload;
|
||||||
|
|
||||||
@ -220,6 +232,10 @@ class Enhance {
|
|||||||
|
|
||||||
pdata = ignoreCase(pdata);
|
pdata = ignoreCase(pdata);
|
||||||
|
|
||||||
|
// save the fields state
|
||||||
|
this.fieldsState.config = Object.keys(pdata);
|
||||||
|
this.fieldsState.use = [...useList];
|
||||||
|
|
||||||
// filter the data
|
// filter the data
|
||||||
const filterData: typeof pdata = {};
|
const filterData: typeof pdata = {};
|
||||||
Object.keys(pdata).forEach((key: any) => {
|
Object.keys(pdata).forEach((key: any) => {
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
type TData = Record<string, any>;
|
type TData = Record<string, any>;
|
||||||
|
|
||||||
export default function ignoreCase(data: TData): TData {
|
export default function ignoreCase(data: TData): TData {
|
||||||
if (!data) return data;
|
if (!data) return {};
|
||||||
|
|
||||||
const newData = {} as TData;
|
const newData = {} as TData;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user