feat: refactor

This commit is contained in:
GyDi
2022-08-12 03:20:55 +08:00
parent 178fd8e828
commit 7f6dac4271
22 changed files with 320 additions and 778 deletions

View File

@@ -1,29 +1,13 @@
import useSWR from "swr";
import { useState } from "react";
import { useLockFn } from "ahooks";
import {
Box,
Divider,
Grid,
IconButton,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
Stack,
} from "@mui/material";
import {
AddchartRounded,
CheckRounded,
MenuRounded,
RestartAltRounded,
} from "@mui/icons-material";
import { Box, Grid, IconButton, Stack } from "@mui/material";
import { RestartAltRounded } from "@mui/icons-material";
import {
getProfiles,
deleteProfile,
enhanceProfiles,
changeProfileChain,
changeProfileValid,
getRuntimeLogs,
} from "@/services/cmds";
import ProfileMore from "./profile-more";
import Notice from "../base/base-notice";
@@ -36,10 +20,8 @@ interface Props {
const EnhancedMode = (props: Props) => {
const { items, chain } = props;
const { data, mutate } = useSWR("getProfiles", getProfiles);
const valid = data?.valid || [];
const [anchorEl, setAnchorEl] = useState<any>(null);
const { mutate: mutateProfiles } = useSWR("getProfiles", getProfiles);
const { data: chainLogs = {} } = useSWR("getRuntimeLogs", getRuntimeLogs);
// handler
const onEnhance = useLockFn(async () => {
@@ -56,7 +38,7 @@ const EnhancedMode = (props: Props) => {
const newChain = [...chain, uid];
await changeProfileChain(newChain);
mutate((conf = {}) => ({ ...conf, chain: newChain }), true);
mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true);
});
const onEnhanceDisable = useLockFn(async (uid: string) => {
@@ -64,14 +46,14 @@ const EnhancedMode = (props: Props) => {
const newChain = chain.filter((i) => i !== uid);
await changeProfileChain(newChain);
mutate((conf = {}) => ({ ...conf, chain: newChain }), true);
mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true);
});
const onEnhanceDelete = useLockFn(async (uid: string) => {
try {
await onEnhanceDisable(uid);
await deleteProfile(uid);
mutate();
mutateProfiles();
} catch (err: any) {
Notice.error(err?.message || err.toString());
}
@@ -82,7 +64,7 @@ const EnhancedMode = (props: Props) => {
const newChain = [uid].concat(chain.filter((i) => i !== uid));
await changeProfileChain(newChain);
mutate((conf = {}) => ({ ...conf, chain: newChain }), true);
mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true);
});
const onMoveEnd = useLockFn(async (uid: string) => {
@@ -90,20 +72,7 @@ const EnhancedMode = (props: Props) => {
const newChain = chain.filter((i) => i !== uid).concat([uid]);
await changeProfileChain(newChain);
mutate((conf = {}) => ({ ...conf, chain: newChain }), true);
});
// update valid list
const onToggleValid = useLockFn(async (key: string) => {
try {
const newValid = valid.includes(key)
? valid.filter((i) => i !== key)
: valid.concat(key);
await changeProfileValid(newValid);
mutate();
} catch (err: any) {
Notice.error(err.message || err.toString());
}
mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true);
});
return (
@@ -123,72 +92,6 @@ const EnhancedMode = (props: Props) => {
>
<RestartAltRounded />
</IconButton>
<IconButton
size="small"
color="inherit"
id="profile-use-button"
title="enable clash fields"
aria-controls={!!anchorEl ? "profile-use-menu" : undefined}
aria-haspopup="true"
aria-expanded={!!anchorEl ? "true" : undefined}
onClick={(e) => setAnchorEl(e.currentTarget)}
>
<MenuRounded />
</IconButton>
<Menu
id="profile-use-menu"
open={!!anchorEl}
anchorEl={anchorEl}
onClose={() => setAnchorEl(null)}
transitionDuration={225}
MenuListProps={{
dense: true,
"aria-labelledby": "profile-use-button",
}}
onContextMenu={(e) => {
setAnchorEl(null);
e.preventDefault();
}}
>
<MenuItem>
<ListItemIcon color="inherit">
<AddchartRounded />
</ListItemIcon>
Use Clash Fields
</MenuItem>
<Divider />
{[
"tun",
"dns",
"hosts",
"script",
"profile",
"payload",
"interface-name",
"routing-mark",
].map((key) => {
const has = valid.includes(key);
return (
<MenuItem
key={key}
sx={{ width: 180 }}
onClick={() => onToggleValid(key)}
>
{has && (
<ListItemIcon color="inherit">
<CheckRounded />
</ListItemIcon>
)}
<ListItemText inset={!has}>{key}</ListItemText>
</MenuItem>
);
})}
</Menu>
</Stack>
<Grid container spacing={2}>
@@ -198,6 +101,7 @@ const EnhancedMode = (props: Props) => {
selected={!!chain.includes(item.uid)}
itemData={item}
enableNum={chain.length}
logInfo={chainLogs[item.uid]}
onEnable={() => onEnhanceEnable(item.uid)}
onDisable={() => onEnhanceDisable(item.uid)}
onDelete={() => onEnhanceDelete(item.uid)}

View File

@@ -12,7 +12,6 @@ import {
Menu,
} from "@mui/material";
import { viewProfile } from "@/services/cmds";
import enhance from "@/services/enhance";
import ProfileEdit from "./profile-edit";
import FileEditor from "./file-editor";
import Notice from "../base/base-notice";
@@ -32,6 +31,7 @@ interface Props {
selected: boolean;
itemData: CmdType.ProfileItem;
enableNum: number;
logInfo?: [string, string][];
onEnable: () => void;
onDisable: () => void;
onMoveTop: () => void;
@@ -45,6 +45,7 @@ const ProfileMore = (props: Props) => {
selected,
itemData,
enableNum,
logInfo = [],
onEnable,
onDisable,
onMoveTop,
@@ -59,13 +60,13 @@ const ProfileMore = (props: Props) => {
const [position, setPosition] = useState({ left: 0, top: 0 });
const [editOpen, setEditOpen] = useState(false);
const [fileOpen, setFileOpen] = useState(false);
const [status, setStatus] = useState(enhance.status(uid));
// const [status, setStatus] = useState(enhance.status(uid));
// unlisten when unmount
useEffect(() => enhance.listen(uid, setStatus), [uid]);
// useEffect(() => enhance.listen(uid, setStatus), [uid]);
// error during enhanced mode
const hasError = selected && status?.status === "error";
const hasError = !!logInfo.find((e) => e[0] === "exception"); // selected && status?.status === "error";
const onEditInfo = () => {
setAnchorEl(null);
@@ -188,9 +189,11 @@ const ProfileMore = (props: Props) => {
noWrap
color="error"
sx={{ width: "calc(100% - 75px)" }}
title={status.message}
// title={status.message}
title="error"
>
{status.message}
{/* {status.message} */}
error
</Typography>
) : (
<Typography

View File

@@ -13,14 +13,18 @@ import {
Tooltip,
Typography,
} from "@mui/material";
import { BuildCircleRounded, InfoRounded } from "@mui/icons-material";
import { changeProfileValid, getProfiles } from "@/services/cmds";
import { InfoRounded } from "@mui/icons-material";
import {
changeProfileValid,
getProfiles,
getRuntimeExists,
} from "@/services/cmds";
import { ModalHandler } from "@/hooks/use-modal-handler";
import enhance, {
DEFAULT_FIELDS,
import {
HANDLE_FIELDS,
USE_FLAG_FIELDS,
} from "@/services/enhance";
DEFAULT_FIELDS,
OTHERS_FIELDS,
} from "@/utils/clash-fields";
import Notice from "@/components/base/base-notice";
interface Props {
@@ -36,19 +40,21 @@ const fieldSorter = (a: string, b: string) => {
return 0;
};
const useFields = [...USE_FLAG_FIELDS].sort(fieldSorter);
const otherFields = [...OTHERS_FIELDS].sort(fieldSorter);
const handleFields = [...HANDLE_FIELDS, ...DEFAULT_FIELDS].sort(fieldSorter);
const ClashFieldViewer = ({ handler }: Props) => {
const { t } = useTranslation();
const { data, mutate } = useSWR("getProfiles", getProfiles);
const { data: existsKeys = [] } = useSWR(
"getRuntimeExists",
getRuntimeExists
);
const [open, setOpen] = useState(false);
const [selected, setSelected] = useState<string[]>([]);
const { config: enhanceConfig, use: enhanceUse } = enhance.getFieldsState();
if (handler) {
handler.current = {
open: () => setOpen(true),
@@ -61,8 +67,8 @@ const ClashFieldViewer = ({ handler }: Props) => {
}, [open]);
useEffect(() => {
setSelected([...(data?.valid || []), ...enhanceUse]);
}, [data?.valid, enhanceUse]);
setSelected(data?.valid || []);
}, [data?.valid]);
const handleChange = (item: string) => {
if (!item) return;
@@ -75,7 +81,7 @@ const ClashFieldViewer = ({ handler }: Props) => {
const handleSave = async () => {
setOpen(false);
const oldSet = new Set([...(data?.valid || []), ...enhanceUse]);
const oldSet = new Set(data?.valid || []);
const curSet = new Set(selected);
const joinSet = new Set(selected.concat([...oldSet]));
@@ -103,10 +109,9 @@ const ClashFieldViewer = ({ handler }: Props) => {
userSelect: "text",
}}
>
{useFields.map((item) => {
{otherFields.map((item) => {
const inSelect = selected.includes(item);
const inConfig = enhanceConfig.includes(item);
const inConfigUse = enhanceUse.includes(item);
const inConfig = existsKeys.includes(item);
const inValid = data?.valid?.includes(item);
return (
@@ -119,8 +124,7 @@ const ClashFieldViewer = ({ handler }: Props) => {
/>
<Typography width="100%">{item}</Typography>
{inConfigUse && !inValid && <InfoIcon />}
{!inSelect && inConfig && <WarnIcon />}
{!inSelect && inConfig && !inValid && <WarnIcon />}
</Stack>
);
})}
@@ -159,15 +163,4 @@ function WarnIcon() {
);
}
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

@@ -10,8 +10,8 @@ import {
DialogTitle,
} from "@mui/material";
import { InfoRounded } from "@mui/icons-material";
import { atomThemeMode } from "../../../services/states";
import { getRunningConfig } from "../../../services/cmds";
import { atomThemeMode } from "@/services/states";
import { getRuntimeYaml } from "@/services/cmds";
import "monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution.js";
import "monaco-editor/esm/vs/basic-languages/yaml/yaml.contribution.js";
@@ -29,7 +29,7 @@ const ConfigViewer = () => {
useEffect(() => {
if (!open) return;
getRunningConfig().then((data) => {
getRuntimeYaml().then((data) => {
const dom = editorRef.current;
if (!dom) return;
@@ -56,7 +56,7 @@ const ConfigViewer = () => {
<>
<Dialog open={open} onClose={() => setOpen(false)}>
<DialogTitle>
{t("Running Config")} <Chip label="ReadOnly" size="small" />
{t("Runtime Config")} <Chip label="ReadOnly" size="small" />
</DialogTitle>
<DialogContent sx={{ width: 520, pb: 1 }}>

View File

@@ -7,11 +7,8 @@ import ReactDOM from "react-dom";
import { RecoilRoot } from "recoil";
import { BrowserRouter } from "react-router-dom";
import Layout from "./pages/_layout";
import enhance from "./services/enhance";
import "./services/i18n";
enhance.setup();
ReactDOM.render(
<React.StrictMode>
<RecoilRoot>

View File

@@ -69,8 +69,20 @@ export async function getClashInfo() {
return invoke<CmdType.ClashInfo | null>("get_clash_info");
}
export async function getRunningConfig() {
return invoke<string | null>("get_running_config");
export async function getRuntimeConfig() {
return invoke<any | null>("get_runtime_config");
}
export async function getRuntimeYaml() {
return invoke<string | null>("get_runtime_yaml");
}
export async function getRuntimeExists() {
return invoke<string[]>("get_runtime_exists");
}
export async function getRuntimeLogs() {
return invoke<Record<string, [string, string][]>>("get_runtime_logs");
}
export async function patchClashConfig(payload: Partial<ApiType.ConfigData>) {

View File

@@ -1,259 +0,0 @@
import { emit, listen, Event } from "@tauri-apps/api/event";
import { appWindow } from "@tauri-apps/api/window";
import ignoreCase from "@/utils/ignore-case";
export const HANDLE_FIELDS = [
"port",
"mixed-port",
"allow-lan",
"mode",
"log-level",
"ipv6",
"secret",
"external-controller",
];
export const DEFAULT_FIELDS = [
"rules",
"proxies",
"proxy-groups",
"proxy-providers",
"rule-providers",
] as const;
export const USE_FLAG_FIELDS = [
"tun",
"dns",
"ebpf",
"hosts",
"script",
"profile",
"payload",
"auto-redir",
"experimental",
"interface-name",
"routing-mark",
"socks-port",
"redir-port",
"tproxy-port",
"iptables",
"external-ui",
"bind-address",
"authentication",
"sniffer", // meta
"geodata-mode", // meta
"tcp-concurrent", // meta
] as const;
/**
* process the merge mode
*/
function toMerge(merge: CmdType.ProfileMerge, data: CmdType.ProfileData) {
if (!merge) return { data, use: [] };
const {
use,
"prepend-rules": preRules,
"append-rules": postRules,
"prepend-proxies": preProxies,
"append-proxies": postProxies,
"prepend-proxy-groups": preProxyGroups,
"append-proxy-groups": postProxyGroups,
...mergeConfig
} = merge;
[...DEFAULT_FIELDS, ...USE_FLAG_FIELDS].forEach((key) => {
// the value should not be null
if (mergeConfig[key] != null) {
data[key] = mergeConfig[key];
}
});
// init
if (!data.rules) data.rules = [];
if (!data.proxies) data.proxies = [];
if (!data["proxy-groups"]) data["proxy-groups"] = [];
// rules
if (Array.isArray(preRules)) {
data.rules.unshift(...preRules);
}
if (Array.isArray(postRules)) {
data.rules.push(...postRules);
}
// proxies
if (Array.isArray(preProxies)) {
data.proxies.unshift(...preProxies);
}
if (Array.isArray(postProxies)) {
data.proxies.push(...postProxies);
}
// proxy-groups
if (Array.isArray(preProxyGroups)) {
data["proxy-groups"].unshift(...preProxyGroups);
}
if (Array.isArray(postProxyGroups)) {
data["proxy-groups"].push(...postProxyGroups);
}
return { data, use: Array.isArray(use) ? use : [] };
}
/**
* process the script mode
*/
function toScript(
script: string,
data: CmdType.ProfileData
): Promise<CmdType.ProfileData> {
if (!script) {
throw new Error("miss the main function");
}
const paramsName = `__verge${Math.floor(Math.random() * 1000)}`;
const code = `'use strict';${script};return main(${paramsName});`;
const func = new Function(paramsName, code);
return func(data);
}
export type EStatus = { status: "ok" | "error"; message?: string };
export type EListener = (status: EStatus) => void;
export type EUnlistener = () => void;
/**
* The service helps to
* implement enhanced profiles
*/
class Enhance {
private isSetup = false;
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();
}
// setup some listener
// for the enhanced running status
listen(uid: string, cb: EListener): EUnlistener {
this.listenMap.set(uid, cb);
return () => this.listenMap.delete(uid);
}
// get the running status
status(uid: string): EStatus | undefined {
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;
const result = await this.runner(payload).catch((err: any) => ({
data: null,
status: "error",
error: err.message,
}));
emit(payload.callback, JSON.stringify(result)).catch(console.error);
}
// setup the handler
setup() {
if (this.isSetup) return;
this.isSetup = true;
listen("script-handler", async (event) => {
await this.enhanceHandler(event);
});
listen("script-handler-close", async (event) => {
await this.enhanceHandler(event);
appWindow.close();
});
}
// enhanced mode runner
private async runner(payload: CmdType.EnhancedPayload) {
const chain = payload.chain || [];
const valid = payload.valid || [];
if (!Array.isArray(chain)) throw new Error("unhandle error");
let pdata = payload.current || {};
let useList = valid;
for (const each of chain) {
const { uid, type = "" } = each.item;
try {
// process script
if (type === "script") {
// support async main function
pdata = await toScript(each.script!, ignoreCase(pdata));
}
// process merge
else if (type === "merge") {
const temp = toMerge(each.merge!, ignoreCase(pdata));
pdata = temp.data;
useList = useList.concat(temp.use || []);
}
// invalid type
else {
throw new Error(`invalid enhanced profile type "${type}"`);
}
this.exec(uid, { status: "ok" });
} catch (err: any) {
console.error(err);
this.exec(uid, {
status: "error",
message: err.message || err.toString(),
});
}
}
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) => {
if (
DEFAULT_FIELDS.includes(key) ||
(USE_FLAG_FIELDS.includes(key) && useList.includes(key))
) {
filterData[key] = pdata[key];
}
});
return { data: filterData, status: "ok" };
}
// exec the listener
private exec(uid: string, status: EStatus) {
this.resultMap.set(uid, status);
this.listenMap.get(uid)?.(status);
}
}
export default new Enhance();

42
src/utils/clash-fields.ts Normal file
View File

@@ -0,0 +1,42 @@
export const HANDLE_FIELDS = [
"port",
"socks-port",
"mixed-port",
"allow-lan",
"mode",
"log-level",
"ipv6",
"secret",
"external-controller",
];
export const DEFAULT_FIELDS = [
"rules",
"proxies",
"proxy-groups",
"proxy-providers",
"rule-providers",
] as const;
export const OTHERS_FIELDS = [
"tun",
"dns",
"ebpf",
"hosts",
"script",
"profile",
"payload",
"auto-redir",
"experimental",
"interface-name",
"routing-mark",
"redir-port",
"tproxy-port",
"iptables",
"external-ui",
"bind-address",
"authentication",
"sniffer", // meta
"geodata-mode", // meta
"tcp-concurrent", // meta
] as const;