refactor: ts path alias

This commit is contained in:
GyDi
2022-08-06 02:35:11 +08:00
parent c699bae99c
commit 786ea17f95
44 changed files with 140 additions and 155 deletions

View File

@@ -0,0 +1,79 @@
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useRecoilValue } from "recoil";
import {
Button,
Chip,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
} from "@mui/material";
import { InfoRounded } from "@mui/icons-material";
import { atomThemeMode } from "../../../services/states";
import { getRunningConfig } 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";
import "monaco-editor/esm/vs/editor/contrib/folding/browser/folding.js";
import { editor } from "monaco-editor/esm/vs/editor/editor.api";
const ConfigViewer = () => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const editorRef = useRef<any>();
const instanceRef = useRef<editor.IStandaloneCodeEditor | null>(null);
const themeMode = useRecoilValue(atomThemeMode);
useEffect(() => {
if (!open) return;
getRunningConfig().then((data) => {
const dom = editorRef.current;
if (!dom) return;
if (instanceRef.current) instanceRef.current.dispose();
instanceRef.current = editor.create(editorRef.current, {
value: data ?? "# Error\n",
language: "yaml",
theme: themeMode === "light" ? "vs" : "vs-dark",
minimap: { enabled: false },
readOnly: true,
});
});
return () => {
if (instanceRef.current) {
instanceRef.current.dispose();
instanceRef.current = null;
}
};
}, [open]);
return (
<>
<Dialog open={open} onClose={() => setOpen(false)}>
<DialogTitle>
{t("Running Config")} <Chip label="ReadOnly" size="small" />
</DialogTitle>
<DialogContent sx={{ width: 520, pb: 1 }}>
<div style={{ width: "100%", height: "420px" }} ref={editorRef} />
</DialogContent>
<DialogActions>
<Button onClick={() => setOpen(false)}>{t("Back")}</Button>
</DialogActions>
</Dialog>
<InfoRounded
fontSize="small"
style={{ cursor: "pointer", opacity: 0.75 }}
onClick={() => setOpen(true)}
/>
</>
);
};
export default ConfigViewer;

View File

@@ -0,0 +1,79 @@
import useSWR, { useSWRConfig } from "swr";
import { useState } from "react";
import { useLockFn } from "ahooks";
import { Menu, MenuItem } from "@mui/material";
import { Settings } from "@mui/icons-material";
import { changeClashCore, getVergeConfig } from "@/services/cmds";
import { getVersion } from "@/services/api";
import Notice from "@/components/base/base-notice";
const VALID_CORE = [
{ name: "Clash", core: "clash" },
{ name: "Clash Meta", core: "clash-meta" },
];
const CoreSwitch = () => {
const { mutate } = useSWRConfig();
const { data: vergeConfig } = useSWR("getVergeConfig", getVergeConfig);
const [anchorEl, setAnchorEl] = useState<any>(null);
const [position, setPosition] = useState({ left: 0, top: 0 });
const { clash_core = "clash" } = vergeConfig ?? {};
const onCoreChange = useLockFn(async (core: string) => {
if (core === clash_core) return;
try {
await changeClashCore(core);
mutate("getVergeConfig");
mutate("getClashConfig");
mutate("getVersion", getVersion());
setAnchorEl(null);
Notice.success(`Successfully switch to ${core}`, 1000);
} catch (err: any) {
Notice.error(err?.message || err.toString());
}
});
return (
<>
<Settings
fontSize="small"
style={{ cursor: "pointer", opacity: 0.75 }}
onClick={(event) => {
const { clientX, clientY } = event;
setPosition({ top: clientY, left: clientX });
setAnchorEl(event.currentTarget);
}}
/>
<Menu
open={!!anchorEl}
anchorEl={anchorEl}
onClose={() => setAnchorEl(null)}
anchorPosition={position}
anchorReference="anchorPosition"
transitionDuration={225}
onContextMenu={(e) => {
setAnchorEl(null);
e.preventDefault();
}}
>
{VALID_CORE.map((each) => (
<MenuItem
key={each.core}
sx={{ minWidth: 125 }}
selected={each.core === clash_core}
onClick={() => onCoreChange(each.core)}
>
{each.name}
</MenuItem>
))}
</Menu>
</>
);
};
export default CoreSwitch;

View File

@@ -0,0 +1,87 @@
import { cloneElement, isValidElement, ReactNode, useRef } from "react";
import noop from "@/utils/noop";
interface Props<Value> {
value?: Value;
valueProps?: string;
onChangeProps?: string;
waitTime?: number;
onChange?: (value: Value) => void;
onFormat?: (...args: any[]) => Value;
onGuard?: (value: Value, oldValue: Value) => Promise<void>;
onCatch?: (error: Error) => void;
children: ReactNode;
}
function GuardState<T>(props: Props<T>) {
const {
value,
children,
valueProps = "value",
onChangeProps = "onChange",
waitTime = 0, // debounce wait time default 0
onGuard = noop,
onCatch = noop,
onChange = noop,
onFormat = (v: T) => v,
} = props;
const lockRef = useRef(false);
const saveRef = useRef(value);
const lastRef = useRef(0);
const timeRef = useRef<any>();
if (!isValidElement(children)) {
return children as any;
}
const childProps = { ...children.props };
childProps[valueProps] = value;
childProps[onChangeProps] = async (...args: any[]) => {
// 多次操作无效
if (lockRef.current) return;
lockRef.current = true;
try {
const newValue = (onFormat as any)(...args);
// 先在ui上响应操作
onChange(newValue);
const now = Date.now();
// save the old value
if (waitTime <= 0 || now - lastRef.current >= waitTime) {
saveRef.current = value;
}
lastRef.current = now;
if (waitTime <= 0) {
await onGuard(newValue, value!);
} else {
// debounce guard
clearTimeout(timeRef.current);
timeRef.current = setTimeout(async () => {
try {
await onGuard(newValue, saveRef.current!);
} catch (err: any) {
// 状态回退
onChange(saveRef.current!);
onCatch(err);
}
}, waitTime);
}
} catch (err: any) {
// 状态回退
onChange(saveRef.current!);
onCatch(err);
}
lockRef.current = false;
};
return cloneElement(children, childProps);
}
export default GuardState;

View File

@@ -0,0 +1,52 @@
import { styled, Switch } from "@mui/material";
// todo: deprecated
// From: https://mui.com/components/switches/
const PaletteSwitch = styled(Switch)(({ theme }) => ({
width: 62,
height: 34,
padding: 7,
"& .MuiSwitch-switchBase": {
margin: 1,
padding: 0,
transform: "translateX(6px)",
"&.Mui-checked": {
color: "#fff",
transform: "translateX(22px)",
"& .MuiSwitch-thumb:before": {
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="${encodeURIComponent(
"#fff"
)}" d="M4.2 2.5l-.7 1.8-1.8.7 1.8.7.7 1.8.6-1.8L6.7 5l-1.9-.7-.6-1.8zm15 8.3a6.7 6.7 0 11-6.6-6.6 5.8 5.8 0 006.6 6.6z"/></svg>')`,
},
"& + .MuiSwitch-track": {
opacity: 1,
backgroundColor: theme.palette.mode === "dark" ? "#8796A5" : "#aab4be",
},
},
},
"& .MuiSwitch-thumb": {
backgroundColor: theme.palette.mode === "dark" ? "#003892" : "#001e3c",
width: 32,
height: 32,
"&:before": {
content: "''",
position: "absolute",
width: "100%",
height: "100%",
left: 0,
top: 0,
backgroundRepeat: "no-repeat",
backgroundPosition: "center",
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="${encodeURIComponent(
"#fff"
)}" d="M9.305 1.667V3.75h1.389V1.667h-1.39zm-4.707 1.95l-.982.982L5.09 6.072l.982-.982-1.473-1.473zm10.802 0L13.927 5.09l.982.982 1.473-1.473-.982-.982zM10 5.139a4.872 4.872 0 00-4.862 4.86A4.872 4.872 0 0010 14.862 4.872 4.872 0 0014.86 10 4.872 4.872 0 0010 5.139zm0 1.389A3.462 3.462 0 0113.471 10a3.462 3.462 0 01-3.473 3.472A3.462 3.462 0 016.527 10 3.462 3.462 0 0110 6.528zM1.665 9.305v1.39h2.083v-1.39H1.666zm14.583 0v1.39h2.084v-1.39h-2.084zM5.09 13.928L3.616 15.4l.982.982 1.473-1.473-.982-.982zm9.82 0l-.982.982 1.473 1.473.982-.982-1.473-1.473zM9.305 16.25v2.083h1.389V16.25h-1.39z"/></svg>')`,
},
},
"& .MuiSwitch-track": {
opacity: 1,
backgroundColor: theme.palette.mode === "dark" ? "#8796A5" : "#aab4be",
borderRadius: 20 / 2,
},
}));
export default PaletteSwitch;

View File

@@ -0,0 +1,117 @@
import useSWR, { useSWRConfig } from "swr";
import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next";
import {
Button,
Dialog,
DialogContent,
DialogTitle,
Stack,
Typography,
} from "@mui/material";
import {
checkService,
installService,
uninstallService,
patchVergeConfig,
} from "@/services/cmds";
import Notice from "@/components/base/base-notice";
import noop from "@/utils/noop";
interface Props {
open: boolean;
enable: boolean;
onClose: () => void;
onError?: (err: Error) => void;
}
const ServiceMode = (props: Props) => {
const { open, enable, onClose, onError = noop } = props;
const { t } = useTranslation();
const { mutate } = useSWRConfig();
const { data: status } = useSWR("checkService", checkService, {
revalidateIfStale: true,
shouldRetryOnError: false,
});
const state = status != null ? status : "pending";
const onInstall = useLockFn(async () => {
try {
await installService();
mutate("checkService");
onClose();
Notice.success("Service installed successfully");
} catch (err: any) {
mutate("checkService");
onError(err);
}
});
const onUninstall = useLockFn(async () => {
try {
if (state === "active" && enable) {
await patchVergeConfig({ enable_service_mode: false });
}
await uninstallService();
mutate("checkService");
onClose();
Notice.success("Service uninstalled successfully");
} catch (err: any) {
mutate("checkService");
onError(err);
}
});
// fix unhandle error of the service mode
const onDisable = useLockFn(async () => {
await patchVergeConfig({ enable_service_mode: false });
mutate("checkService");
onClose();
});
return (
<Dialog open={open} onClose={onClose}>
<DialogTitle>{t("Service Mode")}</DialogTitle>
<DialogContent sx={{ width: 360, userSelect: "text" }}>
<Typography>Current State: {state}</Typography>
{(state === "unknown" || state === "uninstall") && (
<Typography>
Infomation: Please make sure the Clash Verge Service is installed
and enabled
</Typography>
)}
<Stack
direction="row"
spacing={1}
sx={{ mt: 4, justifyContent: "flex-end" }}
>
{state === "uninstall" && enable && (
<Button variant="contained" onClick={onDisable}>
Disable Service Mode
</Button>
)}
{state === "uninstall" && (
<Button variant="contained" onClick={onInstall}>
Install
</Button>
)}
{(state === "active" || state === "installed") && (
<Button variant="outlined" onClick={onUninstall}>
Uninstall
</Button>
)}
</Stack>
</DialogContent>
</Dialog>
);
};
export default ServiceMode;

View File

@@ -0,0 +1,56 @@
import { useEffect, useState } from "react";
import { InfoRounded } from "@mui/icons-material";
import { ClickAwayListener, Tooltip } from "@mui/material";
import { getSystemProxy } from "@/services/cmds";
const SysproxyTooltip = () => {
const [open, setOpen] = useState(false);
const [info, setInfo] = useState<any>({});
const onShow = async () => {
const data = await getSystemProxy();
setInfo(data ?? {});
setOpen(true);
};
useEffect(() => {
if (!open) return;
const timer = setTimeout(() => setOpen(false), 2000);
return () => clearTimeout(timer);
}, [open]);
// todo: add error info
const showTitle = (
<div>
<div>Enable: {(!!info.enable).toString()}</div>
<div>Server: {info.server}</div>
<div>Bypass: {info.bypass}</div>
</div>
);
return (
<ClickAwayListener onClickAway={() => setOpen(false)}>
<Tooltip
PopperProps={{
disablePortal: true,
}}
onClose={() => setOpen(false)}
open={open}
disableFocusListener
disableHoverListener
disableTouchListener
placement="top"
title={showTitle}
arrow
>
<InfoRounded
fontSize="small"
style={{ cursor: "pointer", opacity: 0.75 }}
onClick={onShow}
/>
</Tooltip>
</ClickAwayListener>
);
};
export default SysproxyTooltip;

View File

@@ -0,0 +1,33 @@
import { useTranslation } from "react-i18next";
import { Button, ButtonGroup } from "@mui/material";
type ThemeValue = CmdType.VergeConfig["theme_mode"];
interface Props {
value?: ThemeValue;
onChange?: (value: ThemeValue) => void;
}
const ThemeModeSwitch = (props: Props) => {
const { value, onChange } = props;
const { t } = useTranslation();
const modes = ["light", "dark", "system"] as const;
return (
<ButtonGroup size="small">
{modes.map((mode) => (
<Button
key={mode}
variant={mode === value ? "contained" : "outlined"}
onClick={() => onChange?.(mode)}
sx={{ textTransform: "capitalize" }}
>
{t(`theme.${mode}`)}
</Button>
))}
</ButtonGroup>
);
};
export default ThemeModeSwitch;