mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2024-11-16 11:42:21 +08:00
feat: profile page ui
This commit is contained in:
parent
17f724748f
commit
ab34044196
|
@ -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 (
|
|
||||||
<Grid container spacing={{ xs: 2, lg: 3 }}>
|
|
||||||
{items.map((item) => (
|
|
||||||
<Grid item xs={12} sm={6} md={4} lg={3} key={item.file}>
|
|
||||||
<ProfileMore
|
|
||||||
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)}
|
|
||||||
onMoveTop={() => onMoveTop(item.uid)}
|
|
||||||
onMoveEnd={() => onMoveEnd(item.uid)}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -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 (
|
|
||||||
<Dialog open={open} onClose={onClose}>
|
|
||||||
<DialogTitle sx={{ pb: 0.5 }}>{t("Edit Info")}</DialogTitle>
|
|
||||||
|
|
||||||
<DialogContent sx={{ width: 336, pb: 1, userSelect: "text" }}>
|
|
||||||
<TextField
|
|
||||||
{...textFieldProps}
|
|
||||||
disabled
|
|
||||||
label={t("Type")}
|
|
||||||
value={type}
|
|
||||||
sx={{ input: { textTransform: "capitalize" } }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
{...textFieldProps}
|
|
||||||
autoFocus
|
|
||||||
label={t("Name")}
|
|
||||||
value={form.name}
|
|
||||||
onChange={(e) => setForm({ name: e.target.value })}
|
|
||||||
onKeyDown={(e) => e.key === "Enter" && onUpdate()}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
{...textFieldProps}
|
|
||||||
label={t("Descriptions")}
|
|
||||||
value={form.desc}
|
|
||||||
onChange={(e) => setForm({ desc: e.target.value })}
|
|
||||||
onKeyDown={(e) => e.key === "Enter" && onUpdate()}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{type === "remote" && (
|
|
||||||
<TextField
|
|
||||||
{...textFieldProps}
|
|
||||||
label={t("Subscription URL")}
|
|
||||||
value={form.url}
|
|
||||||
onChange={(e) => setForm({ url: e.target.value })}
|
|
||||||
onKeyDown={(e) => e.key === "Enter" && onUpdate()}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(type === "remote" || type === "local") && (
|
|
||||||
<TextField
|
|
||||||
{...textFieldProps}
|
|
||||||
label={t("Update Interval(mins)")}
|
|
||||||
value={option.update_interval}
|
|
||||||
onChange={(e) => {
|
|
||||||
const str = e.target.value?.replace(/\D/, "");
|
|
||||||
setOption({ update_interval: !!str ? +str : undefined });
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) => e.key === "Enter" && onUpdate()}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Collapse
|
|
||||||
in={type === "remote" && showOpt}
|
|
||||||
timeout="auto"
|
|
||||||
unmountOnExit
|
|
||||||
>
|
|
||||||
<TextField
|
|
||||||
{...textFieldProps}
|
|
||||||
label="User Agent"
|
|
||||||
value={option.user_agent}
|
|
||||||
placeholder={`clash-verge/v${version}`}
|
|
||||||
onChange={(e) => setOption({ user_agent: e.target.value })}
|
|
||||||
onKeyDown={(e) => e.key === "Enter" && onUpdate()}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormControlLabel
|
|
||||||
label={t("Use System Proxy")}
|
|
||||||
labelPlacement="start"
|
|
||||||
sx={{ ml: 0, my: 1 }}
|
|
||||||
control={
|
|
||||||
<Switch
|
|
||||||
color="primary"
|
|
||||||
checked={option.with_proxy ?? false}
|
|
||||||
onChange={(_e, c) =>
|
|
||||||
setOption((o) => ({
|
|
||||||
self_proxy: c ? false : o.self_proxy ?? false,
|
|
||||||
with_proxy: c,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormControlLabel
|
|
||||||
label={t("Use Clash Proxy")}
|
|
||||||
labelPlacement="start"
|
|
||||||
sx={{ ml: 0, my: 1 }}
|
|
||||||
control={
|
|
||||||
<Switch
|
|
||||||
color="primary"
|
|
||||||
checked={option.self_proxy ?? false}
|
|
||||||
onChange={(_e, c) =>
|
|
||||||
setOption((o) => ({
|
|
||||||
with_proxy: c ? false : o.with_proxy ?? false,
|
|
||||||
self_proxy: c,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Collapse>
|
|
||||||
</DialogContent>
|
|
||||||
|
|
||||||
<DialogActions sx={{ px: 2, pb: 2, position: "relative" }}>
|
|
||||||
{form.type === "remote" && (
|
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
color="inherit"
|
|
||||||
sx={{ position: "absolute", left: 18 }}
|
|
||||||
onClick={() => setShowOpt((o) => !o)}
|
|
||||||
>
|
|
||||||
<Settings />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button onClick={onClose} variant="outlined">
|
|
||||||
{t("Cancel")}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={onUpdate} variant="contained">
|
|
||||||
{t("Save")}
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -17,7 +17,6 @@ import { RefreshRounded } from "@mui/icons-material";
|
||||||
import { atomLoadingCache } from "@/services/states";
|
import { atomLoadingCache } from "@/services/states";
|
||||||
import { updateProfile, deleteProfile, viewProfile } from "@/services/cmds";
|
import { updateProfile, deleteProfile, viewProfile } from "@/services/cmds";
|
||||||
import { Notice } from "@/components/base";
|
import { Notice } from "@/components/base";
|
||||||
import { InfoViewer } from "./info-viewer";
|
|
||||||
import { EditorViewer } from "./editor-viewer";
|
import { EditorViewer } from "./editor-viewer";
|
||||||
import { ProfileBox } from "./profile-box";
|
import { ProfileBox } from "./profile-box";
|
||||||
import parseTraffic from "@/utils/parse-traffic";
|
import parseTraffic from "@/utils/parse-traffic";
|
||||||
|
@ -31,10 +30,11 @@ interface Props {
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
itemData: IProfileItem;
|
itemData: IProfileItem;
|
||||||
onSelect: (force: boolean) => void;
|
onSelect: (force: boolean) => void;
|
||||||
|
onEdit: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProfileItem = (props: Props) => {
|
export const ProfileItem = (props: Props) => {
|
||||||
const { selected, itemData, onSelect } = props;
|
const { selected, itemData, onSelect, onEdit } = props;
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [anchorEl, setAnchorEl] = useState<any>(null);
|
const [anchorEl, setAnchorEl] = useState<any>(null);
|
||||||
|
@ -55,7 +55,7 @@ export const ProfileItem = (props: Props) => {
|
||||||
|
|
||||||
const loading = loadingCache[itemData.uid] ?? false;
|
const loading = loadingCache[itemData.uid] ?? false;
|
||||||
|
|
||||||
// interval update from now field
|
// interval update fromNow field
|
||||||
const [, setRefresh] = useState({});
|
const [, setRefresh] = useState({});
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasUrl) return;
|
if (!hasUrl) return;
|
||||||
|
@ -83,12 +83,11 @@ export const ProfileItem = (props: Props) => {
|
||||||
};
|
};
|
||||||
}, [hasUrl, updated]);
|
}, [hasUrl, updated]);
|
||||||
|
|
||||||
const [editOpen, setEditOpen] = useState(false);
|
|
||||||
const [fileOpen, setFileOpen] = useState(false);
|
const [fileOpen, setFileOpen] = useState(false);
|
||||||
|
|
||||||
const onEditInfo = () => {
|
const onEditInfo = () => {
|
||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
setEditOpen(true);
|
onEdit();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onEditFile = () => {
|
const onEditFile = () => {
|
||||||
|
@ -298,12 +297,6 @@ export const ProfileItem = (props: Props) => {
|
||||||
))}
|
))}
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
||||||
<InfoViewer
|
|
||||||
open={editOpen}
|
|
||||||
itemData={itemData}
|
|
||||||
onClose={() => setEditOpen(false)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EditorViewer
|
<EditorViewer
|
||||||
uid={uid}
|
uid={uid}
|
||||||
open={fileOpen}
|
open={fileOpen}
|
||||||
|
|
|
@ -14,7 +14,6 @@ import {
|
||||||
import { FeaturedPlayListRounded } from "@mui/icons-material";
|
import { FeaturedPlayListRounded } from "@mui/icons-material";
|
||||||
import { viewProfile } from "@/services/cmds";
|
import { viewProfile } from "@/services/cmds";
|
||||||
import { Notice } from "@/components/base";
|
import { Notice } from "@/components/base";
|
||||||
import { InfoViewer } from "./info-viewer";
|
|
||||||
import { EditorViewer } from "./editor-viewer";
|
import { EditorViewer } from "./editor-viewer";
|
||||||
import { ProfileBox } from "./profile-box";
|
import { ProfileBox } from "./profile-box";
|
||||||
import { LogViewer } from "./log-viewer";
|
import { LogViewer } from "./log-viewer";
|
||||||
|
@ -29,6 +28,7 @@ interface Props {
|
||||||
onMoveTop: () => void;
|
onMoveTop: () => void;
|
||||||
onMoveEnd: () => void;
|
onMoveEnd: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
|
onEdit: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// profile enhanced item
|
// profile enhanced item
|
||||||
|
@ -43,19 +43,19 @@ export const ProfileMore = (props: Props) => {
|
||||||
onMoveTop,
|
onMoveTop,
|
||||||
onMoveEnd,
|
onMoveEnd,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
onEdit,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { uid, type } = itemData;
|
const { uid, type } = itemData;
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const [anchorEl, setAnchorEl] = useState<any>(null);
|
const [anchorEl, setAnchorEl] = useState<any>(null);
|
||||||
const [position, setPosition] = useState({ left: 0, top: 0 });
|
const [position, setPosition] = useState({ left: 0, top: 0 });
|
||||||
const [editOpen, setEditOpen] = useState(false);
|
|
||||||
const [fileOpen, setFileOpen] = useState(false);
|
const [fileOpen, setFileOpen] = useState(false);
|
||||||
const [logOpen, setLogOpen] = useState(false);
|
const [logOpen, setLogOpen] = useState(false);
|
||||||
|
|
||||||
const onEditInfo = () => {
|
const onEditInfo = () => {
|
||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
setEditOpen(true);
|
onEdit();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onEditFile = () => {
|
const onEditFile = () => {
|
||||||
|
@ -219,12 +219,6 @@ export const ProfileMore = (props: Props) => {
|
||||||
))}
|
))}
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
||||||
<InfoViewer
|
|
||||||
open={editOpen}
|
|
||||||
itemData={itemData}
|
|
||||||
onClose={() => setEditOpen(false)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EditorViewer
|
<EditorViewer
|
||||||
uid={uid}
|
uid={uid}
|
||||||
open={fileOpen}
|
open={fileOpen}
|
||||||
|
|
|
@ -1,212 +0,0 @@
|
||||||
import { useRef, useState } from "react";
|
|
||||||
import { mutate } from "swr";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useLockFn, useSetState } from "ahooks";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Collapse,
|
|
||||||
Dialog,
|
|
||||||
DialogActions,
|
|
||||||
DialogContent,
|
|
||||||
DialogTitle,
|
|
||||||
FormControl,
|
|
||||||
FormControlLabel,
|
|
||||||
IconButton,
|
|
||||||
InputLabel,
|
|
||||||
MenuItem,
|
|
||||||
Select,
|
|
||||||
Switch,
|
|
||||||
TextField,
|
|
||||||
} from "@mui/material";
|
|
||||||
import { Settings } from "@mui/icons-material";
|
|
||||||
import { createProfile } from "@/services/cmds";
|
|
||||||
import { Notice } from "@/components/base";
|
|
||||||
import { FileInput } from "./file-input";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
open: boolean;
|
|
||||||
onClose: () => 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<string | null>(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 (
|
|
||||||
<Dialog open={open} onClose={onClose}>
|
|
||||||
<DialogTitle sx={{ pb: 0.5 }}>{t("Create Profile")}</DialogTitle>
|
|
||||||
|
|
||||||
<DialogContent sx={{ width: 336, pb: 1 }}>
|
|
||||||
<FormControl size="small" fullWidth sx={{ mt: 2, mb: 1 }}>
|
|
||||||
<InputLabel>Type</InputLabel>
|
|
||||||
<Select
|
|
||||||
autoFocus
|
|
||||||
label={t("Type")}
|
|
||||||
value={form.type}
|
|
||||||
onChange={(e) => setForm({ type: e.target.value })}
|
|
||||||
>
|
|
||||||
<MenuItem value="remote">Remote</MenuItem>
|
|
||||||
<MenuItem value="local">Local</MenuItem>
|
|
||||||
<MenuItem value="script">Script</MenuItem>
|
|
||||||
<MenuItem value="merge">Merge</MenuItem>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
{...textFieldProps}
|
|
||||||
label={t("Name")}
|
|
||||||
autoComplete="off"
|
|
||||||
value={form.name}
|
|
||||||
onChange={(e) => setForm({ name: e.target.value })}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
{...textFieldProps}
|
|
||||||
label={t("Descriptions")}
|
|
||||||
autoComplete="off"
|
|
||||||
value={form.desc}
|
|
||||||
onChange={(e) => setForm({ desc: e.target.value })}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{form.type === "remote" && (
|
|
||||||
<TextField
|
|
||||||
{...textFieldProps}
|
|
||||||
label={t("Subscription URL")}
|
|
||||||
autoComplete="off"
|
|
||||||
value={form.url}
|
|
||||||
onChange={(e) => setForm({ url: e.target.value })}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{form.type === "local" && (
|
|
||||||
<FileInput onChange={(val) => (fileDataRef.current = val)} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Collapse
|
|
||||||
in={form.type === "remote" && showOpt}
|
|
||||||
timeout="auto"
|
|
||||||
unmountOnExit
|
|
||||||
>
|
|
||||||
<TextField
|
|
||||||
{...textFieldProps}
|
|
||||||
label="User Agent"
|
|
||||||
autoComplete="off"
|
|
||||||
value={option.user_agent}
|
|
||||||
onChange={(e) => setOption({ user_agent: e.target.value })}
|
|
||||||
/>
|
|
||||||
<FormControlLabel
|
|
||||||
label={t("Use System Proxy")}
|
|
||||||
labelPlacement="start"
|
|
||||||
sx={{ ml: 0, my: 1 }}
|
|
||||||
control={
|
|
||||||
<Switch
|
|
||||||
color="primary"
|
|
||||||
checked={option.with_proxy}
|
|
||||||
onChange={(_e, c) =>
|
|
||||||
setOption((o) => ({
|
|
||||||
self_proxy: c ? false : o.self_proxy,
|
|
||||||
with_proxy: c,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<FormControlLabel
|
|
||||||
label={t("Use Clash Proxy")}
|
|
||||||
labelPlacement="start"
|
|
||||||
sx={{ ml: 0, my: 1 }}
|
|
||||||
control={
|
|
||||||
<Switch
|
|
||||||
color="primary"
|
|
||||||
checked={option.self_proxy}
|
|
||||||
onChange={(_e, c) =>
|
|
||||||
setOption((o) => ({
|
|
||||||
with_proxy: c ? false : o.with_proxy,
|
|
||||||
self_proxy: c,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Collapse>
|
|
||||||
</DialogContent>
|
|
||||||
|
|
||||||
<DialogActions sx={{ px: 2, pb: 2, position: "relative" }}>
|
|
||||||
{form.type === "remote" && (
|
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
color="inherit"
|
|
||||||
sx={{ position: "absolute", left: 18 }}
|
|
||||||
onClick={() => setShowOpt((o) => !o)}
|
|
||||||
>
|
|
||||||
<Settings />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button onClick={onClose} variant="outlined">
|
|
||||||
{t("Cancel")}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={onCreate} variant="contained">
|
|
||||||
{t("Save")}
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
274
src/components/profile/profile-viewer.tsx
Normal file
274
src/components/profile/profile-viewer.tsx
Normal file
|
@ -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<ProfileViewerRef, Props>(
|
||||||
|
(props, ref) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [openType, setOpenType] = useState<"new" | "edit">("new");
|
||||||
|
|
||||||
|
// file input
|
||||||
|
const fileDataRef = useRef<string | null>(null);
|
||||||
|
|
||||||
|
const { control, watch, register, ...formIns } = useForm<IProfileItem>({
|
||||||
|
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 (
|
||||||
|
<BaseDialog
|
||||||
|
open={open}
|
||||||
|
title={openType === "new" ? t("Create Profile") : t("Edit Profile")}
|
||||||
|
contentSx={{ width: 375, pb: 0, maxHeight: 320 }}
|
||||||
|
okBtn={t("Save")}
|
||||||
|
cancelBtn={t("Cancel")}
|
||||||
|
onClose={handleClose}
|
||||||
|
onCancel={handleClose}
|
||||||
|
onOk={handleOk}
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="type"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormControl size="small" fullWidth sx={{ mt: 1, mb: 1 }}>
|
||||||
|
<InputLabel>{t("Type")}</InputLabel>
|
||||||
|
<Select {...field} autoFocus label={t("Type")}>
|
||||||
|
<MenuItem value="remote">Remote</MenuItem>
|
||||||
|
<MenuItem value="local">Local</MenuItem>
|
||||||
|
<MenuItem value="script">Script</MenuItem>
|
||||||
|
<MenuItem value="merge">Merge</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="name"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<TextField {...text} {...field} label={t("Name")} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="desc"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<TextField {...text} {...field} label={t("Descriptions")} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isRemote && (
|
||||||
|
<>
|
||||||
|
<Controller
|
||||||
|
name="url"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<TextField {...text} {...field} label={t("Subscription URL")} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="option.user_agent"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<TextField
|
||||||
|
{...text}
|
||||||
|
{...field}
|
||||||
|
placeholder={`clash-verge/v${version}`}
|
||||||
|
label="User Agent"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(isRemote || isLocal) && (
|
||||||
|
<Controller
|
||||||
|
name="option.update_interval"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<TextField
|
||||||
|
{...text}
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => {
|
||||||
|
e.target.value = e.target.value
|
||||||
|
?.replace(/\D/, "")
|
||||||
|
.slice(0, 10);
|
||||||
|
field.onChange(e);
|
||||||
|
}}
|
||||||
|
label={t("Update Interval")}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">mins</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isLocal && openType === "new" && (
|
||||||
|
<FileInput onChange={(val) => (fileDataRef.current = val)} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isRemote && (
|
||||||
|
<>
|
||||||
|
<Controller
|
||||||
|
name="option.with_proxy"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<StyledBox>
|
||||||
|
<InputLabel>{t("Use System Proxy")}</InputLabel>
|
||||||
|
<Switch checked={field.value} {...field} color="primary" />
|
||||||
|
</StyledBox>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="option.self_proxy"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<StyledBox>
|
||||||
|
<InputLabel>{t("Use Clash Proxy")}</InputLabel>
|
||||||
|
<Switch checked={field.value} {...field} color="primary" />
|
||||||
|
</StyledBox>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</BaseDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const StyledBox = styled(Box)(() => ({
|
||||||
|
margin: "8px 0 8px 8px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}));
|
|
@ -6,17 +6,20 @@ import {
|
||||||
} from "@/services/cmds";
|
} from "@/services/cmds";
|
||||||
|
|
||||||
export const useProfiles = () => {
|
export const useProfiles = () => {
|
||||||
const { data: profiles, mutate } = useSWR("getProfiles", getProfiles);
|
const { data: profiles, mutate: mutateProfiles } = useSWR(
|
||||||
|
"getProfiles",
|
||||||
|
getProfiles
|
||||||
|
);
|
||||||
|
|
||||||
const patchProfiles = async (value: Partial<IProfilesConfig>) => {
|
const patchProfiles = async (value: Partial<IProfilesConfig>) => {
|
||||||
await patchProfilesConfig(value);
|
await patchProfilesConfig(value);
|
||||||
mutate();
|
mutateProfiles();
|
||||||
};
|
};
|
||||||
|
|
||||||
const patchCurrent = async (value: Partial<IProfileItem>) => {
|
const patchCurrent = async (value: Partial<IProfileItem>) => {
|
||||||
if (profiles?.current) {
|
if (profiles?.current) {
|
||||||
await patchProfile(profiles.current, value);
|
await patchProfile(profiles.current, value);
|
||||||
mutate();
|
mutateProfiles();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -25,5 +28,6 @@ export const useProfiles = () => {
|
||||||
current: profiles?.items?.find((p) => p.uid === profiles.current),
|
current: profiles?.items?.find((p) => p.uid === profiles.current),
|
||||||
patchProfiles,
|
patchProfiles,
|
||||||
patchCurrent,
|
patchCurrent,
|
||||||
|
mutateProfiles,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -50,7 +50,7 @@
|
||||||
"Name": "Name",
|
"Name": "Name",
|
||||||
"Descriptions": "Descriptions",
|
"Descriptions": "Descriptions",
|
||||||
"Subscription URL": "Subscription URL",
|
"Subscription URL": "Subscription URL",
|
||||||
"Update Interval(mins)": "Update Interval(mins)",
|
"Update Interval": "Update Interval",
|
||||||
|
|
||||||
"Settings": "Settings",
|
"Settings": "Settings",
|
||||||
"Clash Setting": "Clash Setting",
|
"Clash Setting": "Clash Setting",
|
||||||
|
|
|
@ -50,7 +50,9 @@
|
||||||
"Name": "名称",
|
"Name": "名称",
|
||||||
"Descriptions": "描述",
|
"Descriptions": "描述",
|
||||||
"Subscription URL": "订阅链接",
|
"Subscription URL": "订阅链接",
|
||||||
"Update Interval(mins)": "更新间隔(分钟)",
|
"Update Interval": "更新间隔",
|
||||||
|
"Use System Proxy": "使用系统代理更新",
|
||||||
|
"Use Clash Proxy": "使用Clash代理更新",
|
||||||
|
|
||||||
"Settings": "设置",
|
"Settings": "设置",
|
||||||
"Clash Setting": "Clash 设置",
|
"Clash Setting": "Clash 设置",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import useSWR, { mutate } from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useSetRecoilState } from "recoil";
|
import { useSetRecoilState } from "recoil";
|
||||||
import { Box, Button, Grid, IconButton, Stack, TextField } from "@mui/material";
|
import { Box, Button, Grid, IconButton, Stack, TextField } from "@mui/material";
|
||||||
import { CachedRounded } from "@mui/icons-material";
|
import { CachedRounded } from "@mui/icons-material";
|
||||||
|
@ -8,27 +8,39 @@ import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
getProfiles,
|
getProfiles,
|
||||||
patchProfile,
|
patchProfile,
|
||||||
patchProfilesConfig,
|
|
||||||
importProfile,
|
importProfile,
|
||||||
enhanceProfiles,
|
enhanceProfiles,
|
||||||
|
getRuntimeLogs,
|
||||||
|
deleteProfile,
|
||||||
} from "@/services/cmds";
|
} from "@/services/cmds";
|
||||||
import { closeAllConnections, getProxies, updateProxy } from "@/services/api";
|
import { closeAllConnections, getProxies, updateProxy } from "@/services/api";
|
||||||
import { atomCurrentProfile } from "@/services/states";
|
import { atomCurrentProfile } from "@/services/states";
|
||||||
import { BasePage, Notice } from "@/components/base";
|
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 { 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 ProfilePage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [url, setUrl] = useState("");
|
const [url, setUrl] = useState("");
|
||||||
const [disabled, setDisabled] = useState(false);
|
const [disabled, setDisabled] = useState(false);
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
|
||||||
|
|
||||||
const setCurrentProfile = useSetRecoilState(atomCurrentProfile);
|
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<ProfileViewerRef>(null);
|
||||||
|
|
||||||
// distinguish type
|
// distinguish type
|
||||||
const { regularItems, enhanceItems } = useMemo(() => {
|
const { regularItems, enhanceItems } = useMemo(() => {
|
||||||
|
@ -40,9 +52,7 @@ const ProfilePage = () => {
|
||||||
|
|
||||||
const regularItems = items.filter((i) => type1.includes(i.type!));
|
const regularItems = items.filter((i) => type1.includes(i.type!));
|
||||||
const restItems = items.filter((i) => type2.includes(i.type!));
|
const restItems = items.filter((i) => type2.includes(i.type!));
|
||||||
|
|
||||||
const restMap = Object.fromEntries(restItems.map((i) => [i.uid, i]));
|
const restMap = Object.fromEntries(restItems.map((i) => [i.uid, i]));
|
||||||
|
|
||||||
const enhanceItems = chain
|
const enhanceItems = chain
|
||||||
.map((i) => restMap[i]!)
|
.map((i) => restMap[i]!)
|
||||||
.concat(restItems.filter((i) => !chain.includes(i.uid)));
|
.concat(restItems.filter((i) => !chain.includes(i.uid)));
|
||||||
|
@ -75,8 +85,9 @@ const ProfilePage = () => {
|
||||||
|
|
||||||
const { global, groups } = proxiesData;
|
const { global, groups } = proxiesData;
|
||||||
[global, ...groups].forEach((group) => {
|
[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 (!now || selectedMap[name] === now) return;
|
||||||
if (selectedMap[name] == null) {
|
if (selectedMap[name] == null) {
|
||||||
selectedMap[name] = now!;
|
selectedMap[name] = now!;
|
||||||
|
@ -114,13 +125,13 @@ const ProfilePage = () => {
|
||||||
|
|
||||||
if (!newProfiles.current && remoteItem) {
|
if (!newProfiles.current && remoteItem) {
|
||||||
const current = remoteItem.uid;
|
const current = remoteItem.uid;
|
||||||
patchProfilesConfig({ current });
|
patchProfiles({ current });
|
||||||
mutate("getProfiles", { ...newProfiles, current }, true);
|
mutateProfiles();
|
||||||
mutate("getRuntimeLogs");
|
mutateLogs();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch {
|
} catch (err: any) {
|
||||||
Notice.error("Failed to import profile.");
|
Notice.error(err.message || err.toString());
|
||||||
} finally {
|
} finally {
|
||||||
setDisabled(false);
|
setDisabled(false);
|
||||||
}
|
}
|
||||||
|
@ -128,12 +139,10 @@ const ProfilePage = () => {
|
||||||
|
|
||||||
const onSelect = useLockFn(async (current: string, force: boolean) => {
|
const onSelect = useLockFn(async (current: string, force: boolean) => {
|
||||||
if (!force && current === profiles.current) return;
|
if (!force && current === profiles.current) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await patchProfilesConfig({ current });
|
await patchProfiles({ current });
|
||||||
setCurrentProfile(current);
|
setCurrentProfile(current);
|
||||||
mutate("getProfiles", { ...profiles, current: current }, true);
|
mutateLogs();
|
||||||
mutate("getRuntimeLogs");
|
|
||||||
closeAllConnections();
|
closeAllConnections();
|
||||||
Notice.success("Refresh clash config", 1000);
|
Notice.success("Refresh clash config", 1000);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
@ -144,13 +153,52 @@ const ProfilePage = () => {
|
||||||
const onEnhance = useLockFn(async () => {
|
const onEnhance = useLockFn(async () => {
|
||||||
try {
|
try {
|
||||||
await enhanceProfiles();
|
await enhanceProfiles();
|
||||||
mutate("getRuntimeLogs");
|
mutateLogs();
|
||||||
// Notice.success("Refresh clash config", 1000);
|
Notice.success("Refresh clash config", 1000);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
Notice.error(err.message || err.toString(), 3000);
|
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 (
|
return (
|
||||||
<BasePage
|
<BasePage
|
||||||
title={t("Profiles")}
|
title={t("Profiles")}
|
||||||
|
@ -191,7 +239,7 @@ const ProfilePage = () => {
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => setDialogOpen(true)}
|
onClick={viewerRef.current?.create}
|
||||||
>
|
>
|
||||||
{t("New")}
|
{t("New")}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -205,6 +253,7 @@ const ProfilePage = () => {
|
||||||
selected={profiles.current === item.uid}
|
selected={profiles.current === item.uid}
|
||||||
itemData={item}
|
itemData={item}
|
||||||
onSelect={(f) => onSelect(item.uid, f)}
|
onSelect={(f) => onSelect(item.uid, f)}
|
||||||
|
onEdit={() => viewerRef.current?.edit(item)}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
))}
|
))}
|
||||||
|
@ -212,10 +261,27 @@ const ProfilePage = () => {
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{enhanceItems.length > 0 && (
|
{enhanceItems.length > 0 && (
|
||||||
<EnhancedMode items={enhanceItems} chain={profiles.chain || []} />
|
<Grid container spacing={{ xs: 2, lg: 3 }}>
|
||||||
|
{enhanceItems.map((item) => (
|
||||||
|
<Grid item xs={12} sm={6} md={4} lg={3} key={item.file}>
|
||||||
|
<ProfileMore
|
||||||
|
selected={!!chain.includes(item.uid)}
|
||||||
|
itemData={item}
|
||||||
|
enableNum={chain.length || 0}
|
||||||
|
logInfo={chainLogs[item.uid]}
|
||||||
|
onEnable={() => onEnable(item.uid)}
|
||||||
|
onDisable={() => onDisable(item.uid)}
|
||||||
|
onDelete={() => onDelete(item.uid)}
|
||||||
|
onMoveTop={() => onMoveTop(item.uid)}
|
||||||
|
onMoveEnd={() => onMoveEnd(item.uid)}
|
||||||
|
onEdit={() => viewerRef.current?.edit(item)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ProfileNew open={dialogOpen} onClose={() => setDialogOpen(false)} />
|
<ProfileViewer ref={viewerRef} onChange={() => mutateProfiles()} />
|
||||||
</BasePage>
|
</BasePage>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
4
src/services/types.d.ts
vendored
4
src/services/types.d.ts
vendored
|
@ -91,8 +91,6 @@ interface IConnections {
|
||||||
* Some interface for command
|
* Some interface for command
|
||||||
*/
|
*/
|
||||||
|
|
||||||
type IProfileType = "local" | "remote" | "merge" | "script";
|
|
||||||
|
|
||||||
interface IClashInfo {
|
interface IClashInfo {
|
||||||
// status: string;
|
// status: string;
|
||||||
port?: number; // clash mixed port
|
port?: number; // clash mixed port
|
||||||
|
@ -102,7 +100,7 @@ interface IClashInfo {
|
||||||
|
|
||||||
interface IProfileItem {
|
interface IProfileItem {
|
||||||
uid: string;
|
uid: string;
|
||||||
type?: IProfileType | string;
|
type?: "local" | "remote" | "merge" | "script";
|
||||||
name?: string;
|
name?: string;
|
||||||
desc?: string;
|
desc?: string;
|
||||||
file?: string;
|
file?: string;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user