diff --git a/src/components/profile/enhanced.tsx b/src/components/profile/enhanced.tsx
deleted file mode 100644
index 6c20eb8..0000000
--- a/src/components/profile/enhanced.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import useSWR from "swr";
-import { useLockFn } from "ahooks";
-import { Grid } from "@mui/material";
-import {
- getProfiles,
- deleteProfile,
- patchProfilesConfig,
- getRuntimeLogs,
-} from "@/services/cmds";
-import { Notice } from "@/components/base";
-import { ProfileMore } from "./profile-more";
-
-interface Props {
- items: IProfileItem[];
- chain: string[];
-}
-
-export const EnhancedMode = (props: Props) => {
- const { items, chain } = props;
-
- const { mutate: mutateProfiles } = useSWR("getProfiles", getProfiles);
- const { data: chainLogs = {}, mutate: mutateLogs } = useSWR(
- "getRuntimeLogs",
- getRuntimeLogs
- );
-
- const onEnhanceEnable = useLockFn(async (uid: string) => {
- if (chain.includes(uid)) return;
-
- const newChain = [...chain, uid];
- await patchProfilesConfig({ chain: newChain });
- mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true);
- mutateLogs();
- });
-
- const onEnhanceDisable = useLockFn(async (uid: string) => {
- if (!chain.includes(uid)) return;
-
- const newChain = chain.filter((i) => i !== uid);
- await patchProfilesConfig({ chain: newChain });
- mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true);
- mutateLogs();
- });
-
- const onEnhanceDelete = useLockFn(async (uid: string) => {
- try {
- await onEnhanceDisable(uid);
- await deleteProfile(uid);
- mutateProfiles();
- mutateLogs();
- } catch (err: any) {
- Notice.error(err?.message || err.toString());
- }
- });
-
- const onMoveTop = useLockFn(async (uid: string) => {
- if (!chain.includes(uid)) return;
-
- const newChain = [uid].concat(chain.filter((i) => i !== uid));
- await patchProfilesConfig({ chain: newChain });
- mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true);
- mutateLogs();
- });
-
- const onMoveEnd = useLockFn(async (uid: string) => {
- if (!chain.includes(uid)) return;
-
- const newChain = chain.filter((i) => i !== uid).concat([uid]);
- await patchProfilesConfig({ chain: newChain });
- mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true);
- mutateLogs();
- });
-
- return (
-
- {items.map((item) => (
-
- onEnhanceEnable(item.uid)}
- onDisable={() => onEnhanceDisable(item.uid)}
- onDelete={() => onEnhanceDelete(item.uid)}
- onMoveTop={() => onMoveTop(item.uid)}
- onMoveEnd={() => onMoveEnd(item.uid)}
- />
-
- ))}
-
- );
-};
diff --git a/src/components/profile/info-viewer.tsx b/src/components/profile/info-viewer.tsx
deleted file mode 100644
index ffeb3af..0000000
--- a/src/components/profile/info-viewer.tsx
+++ /dev/null
@@ -1,211 +0,0 @@
-import { mutate } from "swr";
-import { useEffect, useState } from "react";
-import { useLockFn, useSetState } from "ahooks";
-import { useTranslation } from "react-i18next";
-import {
- Button,
- Collapse,
- Dialog,
- DialogActions,
- DialogContent,
- DialogTitle,
- FormControlLabel,
- IconButton,
- Switch,
- TextField,
-} from "@mui/material";
-import { Settings } from "@mui/icons-material";
-import { patchProfile } from "@/services/cmds";
-import { version } from "@root/package.json";
-import { Notice } from "@/components/base";
-
-interface Props {
- open: boolean;
- itemData: IProfileItem;
- onClose: () => void;
-}
-
-// edit the profile item
-// remote / local file / merge / script
-export const InfoViewer = (props: Props) => {
- const { open, itemData, onClose } = props;
-
- const { t } = useTranslation();
- const [form, setForm] = useSetState({ ...itemData });
- const [option, setOption] = useSetState(itemData.option ?? {});
- const [showOpt, setShowOpt] = useState(!!itemData.option);
-
- useEffect(() => {
- if (itemData) {
- const { option } = itemData;
- setForm({ ...itemData });
- setOption(option ?? {});
- setShowOpt(
- itemData.type === "remote" &&
- (!!option?.user_agent ||
- !!option?.update_interval ||
- !!option?.self_proxy ||
- !!option?.with_proxy)
- );
- }
- }, [itemData]);
-
- const onUpdate = useLockFn(async () => {
- try {
- const { uid } = itemData;
- const { name, desc, url } = form;
- const option_ =
- itemData.type === "remote" || itemData.type === "local"
- ? option
- : undefined;
-
- if (itemData.type === "remote" && !url) {
- throw new Error("Remote URL should not be null");
- }
-
- await patchProfile(uid, { uid, name, desc, url, option: option_ });
- mutate("getProfiles");
- onClose();
- } catch (err: any) {
- Notice.error(err?.message || err.toString());
- }
- });
-
- const textFieldProps = {
- fullWidth: true,
- size: "small",
- margin: "normal",
- variant: "outlined",
- } as const;
-
- const type =
- form.type ||
- (form.url ? "remote" : form.file?.endsWith(".js") ? "script" : "local");
-
- return (
-
- );
-};
diff --git a/src/components/profile/profile-item.tsx b/src/components/profile/profile-item.tsx
index 2840017..b370307 100644
--- a/src/components/profile/profile-item.tsx
+++ b/src/components/profile/profile-item.tsx
@@ -17,7 +17,6 @@ import { RefreshRounded } from "@mui/icons-material";
import { atomLoadingCache } from "@/services/states";
import { updateProfile, deleteProfile, viewProfile } from "@/services/cmds";
import { Notice } from "@/components/base";
-import { InfoViewer } from "./info-viewer";
import { EditorViewer } from "./editor-viewer";
import { ProfileBox } from "./profile-box";
import parseTraffic from "@/utils/parse-traffic";
@@ -31,10 +30,11 @@ interface Props {
selected: boolean;
itemData: IProfileItem;
onSelect: (force: boolean) => void;
+ onEdit: () => void;
}
export const ProfileItem = (props: Props) => {
- const { selected, itemData, onSelect } = props;
+ const { selected, itemData, onSelect, onEdit } = props;
const { t } = useTranslation();
const [anchorEl, setAnchorEl] = useState(null);
@@ -55,7 +55,7 @@ export const ProfileItem = (props: Props) => {
const loading = loadingCache[itemData.uid] ?? false;
- // interval update from now field
+ // interval update fromNow field
const [, setRefresh] = useState({});
useEffect(() => {
if (!hasUrl) return;
@@ -83,12 +83,11 @@ export const ProfileItem = (props: Props) => {
};
}, [hasUrl, updated]);
- const [editOpen, setEditOpen] = useState(false);
const [fileOpen, setFileOpen] = useState(false);
const onEditInfo = () => {
setAnchorEl(null);
- setEditOpen(true);
+ onEdit();
};
const onEditFile = () => {
@@ -298,12 +297,6 @@ export const ProfileItem = (props: Props) => {
))}
- setEditOpen(false)}
- />
-
void;
onMoveEnd: () => void;
onDelete: () => void;
+ onEdit: () => void;
}
// profile enhanced item
@@ -43,19 +43,19 @@ export const ProfileMore = (props: Props) => {
onMoveTop,
onMoveEnd,
onDelete,
+ onEdit,
} = props;
const { uid, type } = itemData;
const { t, i18n } = useTranslation();
const [anchorEl, setAnchorEl] = useState(null);
const [position, setPosition] = useState({ left: 0, top: 0 });
- const [editOpen, setEditOpen] = useState(false);
const [fileOpen, setFileOpen] = useState(false);
const [logOpen, setLogOpen] = useState(false);
const onEditInfo = () => {
setAnchorEl(null);
- setEditOpen(true);
+ onEdit();
};
const onEditFile = () => {
@@ -219,12 +219,6 @@ export const ProfileMore = (props: Props) => {
))}
- setEditOpen(false)}
- />
-
void;
-}
-
-// create a new profile
-// remote / local file / merge / script
-export const ProfileNew = (props: Props) => {
- const { open, onClose } = props;
-
- const { t } = useTranslation();
- const [form, setForm] = useSetState({
- type: "remote",
- name: "",
- desc: "",
- url: "",
- });
-
- const [showOpt, setShowOpt] = useState(false);
- // can add more option
- const [option, setOption] = useSetState({
- user_agent: "",
- with_proxy: false,
- self_proxy: false,
- });
- // file input
- const fileDataRef = useRef(null);
-
- const onCreate = useLockFn(async () => {
- if (!form.type) {
- Notice.error("`Type` should not be null");
- return;
- }
-
- try {
- const name = form.name || `${form.type} file`;
-
- if (form.type === "remote" && !form.url) {
- throw new Error("The URL should not be null");
- }
-
- const option_ = form.type === "remote" ? option : undefined;
- const item = { ...form, name, option: option_ };
- const fileData = form.type === "local" ? fileDataRef.current : null;
-
- await createProfile(item, fileData);
-
- setForm({ type: "remote", name: "", desc: "", url: "" });
- setOption({ user_agent: "" });
- setShowOpt(false);
- fileDataRef.current = null;
-
- mutate("getProfiles");
- onClose();
- } catch (err: any) {
- Notice.error(err.message || err.toString());
- }
- });
-
- const textFieldProps = {
- fullWidth: true,
- size: "small",
- margin: "normal",
- variant: "outlined",
- } as const;
-
- return (
-
- );
-};
diff --git a/src/components/profile/profile-viewer.tsx b/src/components/profile/profile-viewer.tsx
new file mode 100644
index 0000000..563dbca
--- /dev/null
+++ b/src/components/profile/profile-viewer.tsx
@@ -0,0 +1,274 @@
+import {
+ forwardRef,
+ useEffect,
+ useImperativeHandle,
+ useRef,
+ useState,
+} from "react";
+import { useLockFn } from "ahooks";
+import { useTranslation } from "react-i18next";
+import { useForm, Controller } from "react-hook-form";
+import {
+ Box,
+ FormControl,
+ InputAdornment,
+ InputLabel,
+ MenuItem,
+ Select,
+ Switch,
+ styled,
+ TextField,
+} from "@mui/material";
+import { createProfile, patchProfile } from "@/services/cmds";
+import { BaseDialog, Notice } from "@/components/base";
+import { version } from "@root/package.json";
+import { FileInput } from "./file-input";
+
+interface Props {
+ onChange: () => void;
+}
+
+export interface ProfileViewerRef {
+ create: () => void;
+ edit: (item: IProfileItem) => void;
+}
+
+// create or edit the profile
+// remote / local / merge / script
+export const ProfileViewer = forwardRef(
+ (props, ref) => {
+ const { t } = useTranslation();
+ const [open, setOpen] = useState(false);
+ const [openType, setOpenType] = useState<"new" | "edit">("new");
+
+ // file input
+ const fileDataRef = useRef(null);
+
+ const { control, watch, register, ...formIns } = useForm({
+ defaultValues: {
+ type: "remote",
+ name: "Remote File",
+ desc: "",
+ url: "",
+ option: {
+ // user_agent: "",
+ with_proxy: false,
+ self_proxy: false,
+ },
+ },
+ });
+
+ useImperativeHandle(ref, () => ({
+ create: () => {
+ setOpenType("new");
+ setOpen(true);
+ },
+ edit: (item) => {
+ if (item) {
+ Object.entries(item).forEach(([key, value]) => {
+ formIns.setValue(key as any, value);
+ });
+ }
+ setOpenType("edit");
+ setOpen(true);
+ },
+ }));
+
+ const selfProxy = watch("option.self_proxy");
+ const withProxy = watch("option.with_proxy");
+
+ useEffect(() => {
+ if (selfProxy) formIns.setValue("option.with_proxy", false);
+ }, [selfProxy]);
+
+ useEffect(() => {
+ if (withProxy) formIns.setValue("option.self_proxy", false);
+ }, [withProxy]);
+
+ const handleOk = useLockFn(
+ formIns.handleSubmit(async (form) => {
+ try {
+ if (!form.type) throw new Error("`Type` should not be null");
+ if (form.type === "remote" && !form.url) {
+ throw new Error("The URL should not be null");
+ }
+ if (form.type !== "remote" && form.type !== "local") {
+ delete form.option;
+ }
+ if (form.option?.update_interval) {
+ form.option.update_interval = +form.option.update_interval;
+ }
+ const name = form.name || `${form.type} file`;
+ const item = { ...form, name };
+
+ // 创建
+ if (openType === "new") {
+ await createProfile(item, fileDataRef.current);
+ }
+ // 编辑
+ else {
+ if (!form.uid) throw new Error("UID not found");
+ await patchProfile(form.uid, item);
+ }
+ setOpen(false);
+ setTimeout(() => formIns.reset(), 500);
+ fileDataRef.current = null;
+ props.onChange();
+ } catch (err: any) {
+ Notice.error(err.message);
+ }
+ })
+ );
+
+ const handleClose = () => {
+ setOpen(false);
+ fileDataRef.current = null;
+ setTimeout(() => formIns.reset(), 500);
+ };
+
+ const text = {
+ fullWidth: true,
+ size: "small",
+ margin: "normal",
+ variant: "outlined",
+ autoComplete: "off",
+ autoCorrect: "off",
+ } as const;
+
+ const formType = watch("type");
+ const isRemote = formType === "remote";
+ const isLocal = formType === "local";
+
+ return (
+
+ (
+
+ {t("Type")}
+
+
+ )}
+ />
+
+ (
+
+ )}
+ />
+
+ (
+
+ )}
+ />
+
+ {isRemote && (
+ <>
+ (
+
+ )}
+ />
+
+ (
+
+ )}
+ />
+ >
+ )}
+
+ {(isRemote || isLocal) && (
+ (
+ {
+ e.target.value = e.target.value
+ ?.replace(/\D/, "")
+ .slice(0, 10);
+ field.onChange(e);
+ }}
+ label={t("Update Interval")}
+ InputProps={{
+ endAdornment: (
+ mins
+ ),
+ }}
+ />
+ )}
+ />
+ )}
+
+ {isLocal && openType === "new" && (
+ (fileDataRef.current = val)} />
+ )}
+
+ {isRemote && (
+ <>
+ (
+
+ {t("Use System Proxy")}
+
+
+ )}
+ />
+
+ (
+
+ {t("Use Clash Proxy")}
+
+
+ )}
+ />
+ >
+ )}
+
+ );
+ }
+);
+
+const StyledBox = styled(Box)(() => ({
+ margin: "8px 0 8px 8px",
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "space-between",
+}));
diff --git a/src/hooks/use-profiles.ts b/src/hooks/use-profiles.ts
index 17b2886..9b6c5fe 100644
--- a/src/hooks/use-profiles.ts
+++ b/src/hooks/use-profiles.ts
@@ -6,17 +6,20 @@ import {
} from "@/services/cmds";
export const useProfiles = () => {
- const { data: profiles, mutate } = useSWR("getProfiles", getProfiles);
+ const { data: profiles, mutate: mutateProfiles } = useSWR(
+ "getProfiles",
+ getProfiles
+ );
const patchProfiles = async (value: Partial) => {
await patchProfilesConfig(value);
- mutate();
+ mutateProfiles();
};
const patchCurrent = async (value: Partial) => {
if (profiles?.current) {
await patchProfile(profiles.current, value);
- mutate();
+ mutateProfiles();
}
};
@@ -25,5 +28,6 @@ export const useProfiles = () => {
current: profiles?.items?.find((p) => p.uid === profiles.current),
patchProfiles,
patchCurrent,
+ mutateProfiles,
};
};
diff --git a/src/locales/en.json b/src/locales/en.json
index e6e7774..9c139c9 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -50,7 +50,7 @@
"Name": "Name",
"Descriptions": "Descriptions",
"Subscription URL": "Subscription URL",
- "Update Interval(mins)": "Update Interval(mins)",
+ "Update Interval": "Update Interval",
"Settings": "Settings",
"Clash Setting": "Clash Setting",
diff --git a/src/locales/zh.json b/src/locales/zh.json
index eb9d627..d836e5c 100644
--- a/src/locales/zh.json
+++ b/src/locales/zh.json
@@ -50,7 +50,9 @@
"Name": "名称",
"Descriptions": "描述",
"Subscription URL": "订阅链接",
- "Update Interval(mins)": "更新间隔(分钟)",
+ "Update Interval": "更新间隔",
+ "Use System Proxy": "使用系统代理更新",
+ "Use Clash Proxy": "使用Clash代理更新",
"Settings": "设置",
"Clash Setting": "Clash 设置",
diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx
index 8e618f0..ff4ab8b 100644
--- a/src/pages/profiles.tsx
+++ b/src/pages/profiles.tsx
@@ -1,6 +1,6 @@
import useSWR, { mutate } from "swr";
import { useLockFn } from "ahooks";
-import { useEffect, useMemo, useState } from "react";
+import { useEffect, useMemo, useRef, useState } from "react";
import { useSetRecoilState } from "recoil";
import { Box, Button, Grid, IconButton, Stack, TextField } from "@mui/material";
import { CachedRounded } from "@mui/icons-material";
@@ -8,27 +8,39 @@ import { useTranslation } from "react-i18next";
import {
getProfiles,
patchProfile,
- patchProfilesConfig,
importProfile,
enhanceProfiles,
+ getRuntimeLogs,
+ deleteProfile,
} from "@/services/cmds";
import { closeAllConnections, getProxies, updateProxy } from "@/services/api";
import { atomCurrentProfile } from "@/services/states";
import { BasePage, Notice } from "@/components/base";
-import { ProfileNew } from "@/components/profile/profile-new";
+import {
+ ProfileViewer,
+ ProfileViewerRef,
+} from "@/components/profile/profile-viewer";
import { ProfileItem } from "@/components/profile/profile-item";
-import { EnhancedMode } from "@/components/profile/enhanced";
+import { ProfileMore } from "@/components/profile/profile-more";
+import { useProfiles } from "@/hooks/use-profiles";
const ProfilePage = () => {
const { t } = useTranslation();
const [url, setUrl] = useState("");
const [disabled, setDisabled] = useState(false);
- const [dialogOpen, setDialogOpen] = useState(false);
const setCurrentProfile = useSetRecoilState(atomCurrentProfile);
- const { data: profiles = {} } = useSWR("getProfiles", getProfiles);
+ const { profiles = {}, patchProfiles, mutateProfiles } = useProfiles();
+
+ const { data: chainLogs = {}, mutate: mutateLogs } = useSWR(
+ "getRuntimeLogs",
+ getRuntimeLogs
+ );
+
+ const chain = profiles.chain || [];
+ const viewerRef = useRef(null);
// distinguish type
const { regularItems, enhanceItems } = useMemo(() => {
@@ -40,9 +52,7 @@ const ProfilePage = () => {
const regularItems = items.filter((i) => type1.includes(i.type!));
const restItems = items.filter((i) => type2.includes(i.type!));
-
const restMap = Object.fromEntries(restItems.map((i) => [i.uid, i]));
-
const enhanceItems = chain
.map((i) => restMap[i]!)
.concat(restItems.filter((i) => !chain.includes(i.uid)));
@@ -75,8 +85,9 @@ const ProfilePage = () => {
const { global, groups } = proxiesData;
[global, ...groups].forEach((group) => {
- const { name, now } = group;
+ const { type, name, now } = group;
+ if (type !== "Selector" && type !== "Fallback") return;
if (!now || selectedMap[name] === now) return;
if (selectedMap[name] == null) {
selectedMap[name] = now!;
@@ -114,13 +125,13 @@ const ProfilePage = () => {
if (!newProfiles.current && remoteItem) {
const current = remoteItem.uid;
- patchProfilesConfig({ current });
- mutate("getProfiles", { ...newProfiles, current }, true);
- mutate("getRuntimeLogs");
+ patchProfiles({ current });
+ mutateProfiles();
+ mutateLogs();
}
});
- } catch {
- Notice.error("Failed to import profile.");
+ } catch (err: any) {
+ Notice.error(err.message || err.toString());
} finally {
setDisabled(false);
}
@@ -128,12 +139,10 @@ const ProfilePage = () => {
const onSelect = useLockFn(async (current: string, force: boolean) => {
if (!force && current === profiles.current) return;
-
try {
- await patchProfilesConfig({ current });
+ await patchProfiles({ current });
setCurrentProfile(current);
- mutate("getProfiles", { ...profiles, current: current }, true);
- mutate("getRuntimeLogs");
+ mutateLogs();
closeAllConnections();
Notice.success("Refresh clash config", 1000);
} catch (err: any) {
@@ -144,13 +153,52 @@ const ProfilePage = () => {
const onEnhance = useLockFn(async () => {
try {
await enhanceProfiles();
- mutate("getRuntimeLogs");
- // Notice.success("Refresh clash config", 1000);
+ mutateLogs();
+ Notice.success("Refresh clash config", 1000);
} catch (err: any) {
Notice.error(err.message || err.toString(), 3000);
}
});
+ const onEnable = useLockFn(async (uid: string) => {
+ if (chain.includes(uid)) return;
+ const newChain = [...chain, uid];
+ await patchProfiles({ chain: newChain });
+ mutateLogs();
+ });
+
+ const onDisable = useLockFn(async (uid: string) => {
+ if (!chain.includes(uid)) return;
+ const newChain = chain.filter((i) => i !== uid);
+ await patchProfiles({ chain: newChain });
+ mutateLogs();
+ });
+
+ const onDelete = useLockFn(async (uid: string) => {
+ try {
+ await onDisable(uid);
+ await deleteProfile(uid);
+ mutateProfiles();
+ mutateLogs();
+ } catch (err: any) {
+ Notice.error(err?.message || err.toString());
+ }
+ });
+
+ const onMoveTop = useLockFn(async (uid: string) => {
+ if (!chain.includes(uid)) return;
+ const newChain = [uid].concat(chain.filter((i) => i !== uid));
+ await patchProfiles({ chain: newChain });
+ mutateLogs();
+ });
+
+ const onMoveEnd = useLockFn(async (uid: string) => {
+ if (!chain.includes(uid)) return;
+ const newChain = chain.filter((i) => i !== uid).concat([uid]);
+ await patchProfiles({ chain: newChain });
+ mutateLogs();
+ });
+
return (
{
@@ -205,6 +253,7 @@ const ProfilePage = () => {
selected={profiles.current === item.uid}
itemData={item}
onSelect={(f) => onSelect(item.uid, f)}
+ onEdit={() => viewerRef.current?.edit(item)}
/>
))}
@@ -212,10 +261,27 @@ const ProfilePage = () => {
{enhanceItems.length > 0 && (
-
+
+ {enhanceItems.map((item) => (
+
+ onEnable(item.uid)}
+ onDisable={() => onDisable(item.uid)}
+ onDelete={() => onDelete(item.uid)}
+ onMoveTop={() => onMoveTop(item.uid)}
+ onMoveEnd={() => onMoveEnd(item.uid)}
+ onEdit={() => viewerRef.current?.edit(item)}
+ />
+
+ ))}
+
)}
- setDialogOpen(false)} />
+ mutateProfiles()} />
);
};
diff --git a/src/services/types.d.ts b/src/services/types.d.ts
index 17fb5bf..35fb437 100644
--- a/src/services/types.d.ts
+++ b/src/services/types.d.ts
@@ -91,8 +91,6 @@ interface IConnections {
* Some interface for command
*/
-type IProfileType = "local" | "remote" | "merge" | "script";
-
interface IClashInfo {
// status: string;
port?: number; // clash mixed port
@@ -102,7 +100,7 @@ interface IClashInfo {
interface IProfileItem {
uid: string;
- type?: IProfileType | string;
+ type?: "local" | "remote" | "merge" | "script";
name?: string;
desc?: string;
file?: string;