Files
clash-verge-rev/src/components/profile/profile-item.tsx
T

416 lines
11 KiB
TypeScript
Raw Normal View History

2021-12-17 00:44:51 +08:00
import dayjs from "dayjs";
2022-08-14 23:10:19 +08:00
import { mutate } from "swr";
2022-05-07 14:43:08 +08:00
import { useEffect, useState } from "react";
2022-03-05 19:04:20 +08:00
import { useLockFn } from "ahooks";
2022-03-21 11:40:27 +08:00
import { useRecoilState } from "recoil";
2022-03-18 14:59:23 +08:00
import { useTranslation } from "react-i18next";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
2021-12-17 00:44:51 +08:00
import {
Box,
Typography,
LinearProgress,
IconButton,
2021-12-26 14:08:25 +08:00
keyframes,
2022-01-08 16:52:18 +08:00
MenuItem,
Menu,
2023-08-05 22:07:30 +08:00
CircularProgress,
2021-12-17 00:44:51 +08:00
} from "@mui/material";
import { RefreshRounded, DragIndicator } from "@mui/icons-material";
2022-08-06 02:35:11 +08:00
import { atomLoadingCache } from "@/services/states";
import { updateProfile, deleteProfile, viewProfile } from "@/services/cmds";
2022-11-20 22:03:55 +08:00
import { Notice } from "@/components/base";
2022-11-20 22:37:34 +08:00
import { EditorViewer } from "./editor-viewer";
import { ProfileBox } from "./profile-box";
2022-08-06 02:35:11 +08:00
import parseTraffic from "@/utils/parse-traffic";
2024-03-21 11:39:01 +08:00
import { ConfirmViewer } from "./confirm-viewer";
2021-12-26 14:08:25 +08:00
const round = keyframes`
2022-01-16 18:30:25 +08:00
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
2021-12-26 14:08:25 +08:00
`;
2021-12-17 00:44:51 +08:00
interface Props {
id: string;
2021-12-17 00:44:51 +08:00
selected: boolean;
2023-08-05 22:07:30 +08:00
activating: boolean;
2022-11-19 17:22:29 +08:00
itemData: IProfileItem;
2022-01-17 02:28:23 +08:00
onSelect: (force: boolean) => void;
2022-11-28 22:29:58 +08:00
onEdit: () => void;
2021-12-17 00:44:51 +08:00
}
2022-11-20 22:37:34 +08:00
export const ProfileItem = (props: Props) => {
2023-08-05 22:07:30 +08:00
const { selected, activating, itemData, onSelect, onEdit } = props;
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id: props.id });
2021-12-26 14:08:25 +08:00
2022-03-18 14:59:23 +08:00
const { t } = useTranslation();
2022-01-08 16:52:18 +08:00
const [anchorEl, setAnchorEl] = useState<any>(null);
const [position, setPosition] = useState({ left: 0, top: 0 });
2022-03-21 11:40:27 +08:00
const [loadingCache, setLoadingCache] = useRecoilState(atomLoadingCache);
2021-12-17 00:44:51 +08:00
2022-03-27 00:58:17 +08:00
const { uid, name = "Profile", extra, updated = 0 } = itemData;
2021-12-17 00:44:51 +08:00
2022-02-19 00:23:47 +08:00
// local file mode
// remote file mode
const hasUrl = !!itemData.url;
const hasExtra = !!extra; // only subscription url has extra info
const hasHome = !!itemData.home; // only subscription url has home page
2022-02-09 02:08:27 +08:00
2022-05-07 14:43:08 +08:00
const { upload = 0, download = 0, total = 0 } = extra ?? {};
const from = parseUrl(itemData.url);
2024-03-10 21:37:52 +08:00
const description = itemData.desc;
2022-05-07 14:43:08 +08:00
const expire = parseExpire(extra?.expire);
const progress = Math.round(((download + upload) * 100) / (total + 0.1));
2022-03-21 11:40:27 +08:00
const loading = loadingCache[itemData.uid] ?? false;
2022-03-02 01:58:16 +08:00
2022-11-28 22:29:58 +08:00
// interval update fromNow field
2022-05-07 14:43:08 +08:00
const [, setRefresh] = useState({});
useEffect(() => {
if (!hasUrl) return;
let timer: any = null;
const handler = () => {
const now = Date.now();
const lastUpdate = updated * 1000;
// 大于一天的不管
if (now - lastUpdate >= 24 * 36e5) return;
const wait = now - lastUpdate >= 36e5 ? 30e5 : 5e4;
timer = setTimeout(() => {
setRefresh({});
handler();
}, wait);
};
handler();
return () => {
if (timer) clearTimeout(timer);
};
}, [hasUrl, updated]);
2022-03-27 00:58:17 +08:00
const [fileOpen, setFileOpen] = useState(false);
2024-03-21 11:39:01 +08:00
const [confirmOpen, setConfirmOpen] = useState(false);
2022-03-27 00:58:17 +08:00
const onOpenHome = () => {
setAnchorEl(null);
window.open(itemData.home); // use built-in browser
};
2022-03-27 00:58:17 +08:00
const onEditInfo = () => {
2022-03-02 01:58:16 +08:00
setAnchorEl(null);
2022-11-28 22:29:58 +08:00
onEdit();
2022-03-02 01:58:16 +08:00
};
2022-03-27 00:58:17 +08:00
const onEditFile = () => {
2022-01-17 02:16:17 +08:00
setAnchorEl(null);
2022-03-27 00:58:17 +08:00
setFileOpen(true);
2022-01-17 02:16:17 +08:00
};
2022-01-17 02:28:23 +08:00
const onForceSelect = () => {
setAnchorEl(null);
onSelect(true);
};
2022-03-27 00:58:17 +08:00
const onOpenFile = useLockFn(async () => {
setAnchorEl(null);
try {
await viewProfile(itemData.uid);
} catch (err: any) {
Notice.error(err?.message || err.toString());
}
});
/// 0 不使用任何代理
/// 1 使用订阅好的代理
/// 2 至少使用一个代理,根据订阅,如果没订阅,默认使用系统代理
const onUpdate = useLockFn(async (type: 0 | 1 | 2) => {
2022-01-08 16:52:18 +08:00
setAnchorEl(null);
2022-03-21 11:40:27 +08:00
setLoadingCache((cache) => ({ ...cache, [itemData.uid]: true }));
2022-11-19 17:22:29 +08:00
const option: Partial<IProfileOption> = {};
if (type === 0) {
option.with_proxy = false;
option.self_proxy = false;
} else if (type === 1) {
// nothing
} else if (type === 2) {
if (itemData.option?.self_proxy) {
option.with_proxy = false;
option.self_proxy = true;
} else {
option.with_proxy = true;
option.self_proxy = false;
}
}
2021-12-26 14:08:25 +08:00
try {
await updateProfile(itemData.uid, option);
2021-12-26 14:08:25 +08:00
mutate("getProfiles");
2022-01-16 18:30:25 +08:00
} catch (err: any) {
2022-06-15 02:42:04 +08:00
const errmsg = err?.message || err.toString();
Notice.error(
errmsg.replace(/error sending request for url (\S+?): /, "")
);
2022-03-21 11:40:27 +08:00
} finally {
setLoadingCache((cache) => ({ ...cache, [itemData.uid]: false }));
2021-12-26 14:08:25 +08:00
}
2022-03-21 11:40:27 +08:00
});
2021-12-26 14:08:25 +08:00
2022-02-26 17:39:36 +08:00
const onDelete = useLockFn(async () => {
2022-01-08 16:52:18 +08:00
setAnchorEl(null);
try {
2022-03-01 08:58:47 +08:00
await deleteProfile(itemData.uid);
2022-01-08 16:52:18 +08:00
mutate("getProfiles");
2022-01-16 18:30:25 +08:00
} catch (err: any) {
2022-03-02 01:58:16 +08:00
Notice.error(err?.message || err.toString());
2022-01-08 16:52:18 +08:00
}
2022-02-26 17:39:36 +08:00
});
2022-01-08 16:52:18 +08:00
const urlModeMenu = (
hasHome ? [{ label: "Home", handler: onOpenHome }] : []
).concat([
2022-02-10 01:42:52 +08:00
{ label: "Select", handler: onForceSelect },
2022-03-27 00:58:17 +08:00
{ label: "Edit Info", handler: onEditInfo },
{ label: "Edit File", handler: onEditFile },
{ label: "Open File", handler: onOpenFile },
{ label: "Update", handler: () => onUpdate(0) },
{ label: "Update(Proxy)", handler: () => onUpdate(2) },
2024-03-21 11:39:01 +08:00
{
label: "Delete",
handler: () => {
2024-03-21 12:55:22 +08:00
setAnchorEl(null);
2024-03-21 11:39:01 +08:00
setConfirmOpen(true);
},
},
]);
2022-02-10 01:42:52 +08:00
const fileModeMenu = [
{ label: "Select", handler: onForceSelect },
2022-03-27 00:58:17 +08:00
{ label: "Edit Info", handler: onEditInfo },
{ label: "Edit File", handler: onEditFile },
{ label: "Open File", handler: onOpenFile },
2024-03-21 11:39:01 +08:00
{
label: "Delete",
handler: () => {
2024-03-21 12:55:22 +08:00
setAnchorEl(null);
2024-03-21 11:39:01 +08:00
setConfirmOpen(true);
},
},
2022-02-10 01:42:52 +08:00
];
2022-08-14 23:10:19 +08:00
const boxStyle = {
height: 26,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
};
2021-12-17 00:44:51 +08:00
return (
<Box
sx={{
transform: CSS.Transform.toString(transform),
transition,
}}
>
2022-08-14 23:10:19 +08:00
<ProfileBox
aria-selected={selected}
2022-01-17 02:28:23 +08:00
onClick={() => onSelect(false)}
2022-03-05 22:54:39 +08:00
onContextMenu={(event) => {
const { clientX, clientY } = event;
setPosition({ top: clientY, left: clientX });
setAnchorEl(event.currentTarget);
event.preventDefault();
}}
2021-12-17 00:44:51 +08:00
>
2023-08-05 22:07:30 +08:00
{activating && (
<Box
sx={{
position: "absolute",
display: "flex",
justifyContent: "center",
alignItems: "center",
top: 10,
left: 10,
right: 10,
bottom: 2,
zIndex: 10,
backdropFilter: "blur(2px)",
}}
>
<CircularProgress size={20} />
</Box>
)}
2022-08-14 23:10:19 +08:00
<Box position="relative">
<Box sx={{ display: "flex", justifyContent: "start" }}>
<Box
ref={setNodeRef}
sx={{ display: "flex", margin: "auto 0" }}
{...attributes}
{...listeners}
>
2024-03-09 21:37:21 +08:00
<DragIndicator
sx={[
{ cursor: "move", marginLeft: "-6px" },
({ palette: { text } }) => {
return { color: text.primary };
},
]}
/>
</Box>
<Typography
width="calc(100% - 36px)"
2024-03-13 10:53:39 +08:00
sx={{ fontSize: "18px", fontWeight: "600", lineHeight: "26px" }}
variant="h6"
component="h2"
noWrap
title={name}
>
{name}
</Typography>
</Box>
2021-12-17 00:44:51 +08:00
2022-02-19 00:23:47 +08:00
{/* only if has url can it be updated */}
{hasUrl && (
2022-02-09 02:08:27 +08:00
<IconButton
sx={{
2022-08-14 23:10:19 +08:00
position: "absolute",
p: "3px",
top: -1,
right: -5,
2022-02-09 02:08:27 +08:00
animation: loading ? `1s linear infinite ${round}` : "none",
}}
2022-08-14 23:10:19 +08:00
size="small"
2022-02-09 02:08:27 +08:00
color="inherit"
disabled={loading}
onClick={(e) => {
e.stopPropagation();
onUpdate(1);
2022-02-09 02:08:27 +08:00
}}
>
2022-08-14 23:10:19 +08:00
<RefreshRounded color="inherit" />
2022-02-09 02:08:27 +08:00
</IconButton>
)}
2022-01-08 16:52:18 +08:00
</Box>
2022-02-19 00:23:47 +08:00
{/* the second line show url's info or description */}
2022-08-14 23:10:19 +08:00
<Box sx={boxStyle}>
2024-03-10 21:37:52 +08:00
{
2022-08-14 23:10:19 +08:00
<>
2024-03-10 21:37:52 +08:00
{description ? (
2024-03-13 10:53:39 +08:00
<Typography
noWrap
title={description}
sx={{ fontSize: "14px" }}
>
2024-03-10 21:37:52 +08:00
{description}
</Typography>
) : (
hasUrl && (
<Typography noWrap title={`From ${from}`}>
{from}
</Typography>
)
)}
{hasUrl && (
<Typography
noWrap
flex="1 0 auto"
fontSize={14}
textAlign="right"
title={`Updated Time: ${parseExpire(updated)}`}
>
{updated > 0 ? dayjs(updated * 1000).fromNow() : ""}
</Typography>
)}
2022-08-14 23:10:19 +08:00
</>
2024-03-10 21:37:52 +08:00
}
2022-08-14 23:10:19 +08:00
</Box>
2022-02-19 00:23:47 +08:00
{/* the third line show extra info or last updated time */}
{hasExtra ? (
<Box sx={{ ...boxStyle, fontSize: 14 }}>
2022-08-14 23:10:19 +08:00
<span title="Used / Total">
2022-02-19 00:23:47 +08:00
{parseTraffic(upload + download)} / {parseTraffic(total)}
</span>
2022-08-14 23:10:19 +08:00
<span title="Expire Time">{expire}</span>
2022-02-19 00:23:47 +08:00
</Box>
) : (
2024-03-13 10:53:39 +08:00
<Box sx={{ ...boxStyle, fontSize: 12, justifyContent: "flex-end" }}>
2022-08-14 23:10:19 +08:00
<span title="Updated Time">{parseExpire(updated)}</span>
2022-02-19 00:23:47 +08:00
</Box>
2022-02-09 02:08:27 +08:00
)}
2024-03-09 21:37:21 +08:00
<LinearProgress variant="determinate" value={progress} />
2022-08-14 23:10:19 +08:00
</ProfileBox>
2022-01-08 16:52:18 +08:00
<Menu
open={!!anchorEl}
anchorEl={anchorEl}
onClose={() => setAnchorEl(null)}
anchorPosition={position}
anchorReference="anchorPosition"
2022-03-27 00:58:17 +08:00
transitionDuration={225}
2022-11-09 15:53:42 +08:00
MenuListProps={{ sx: { py: 0.5 } }}
2022-03-05 22:54:39 +08:00
onContextMenu={(e) => {
setAnchorEl(null);
e.preventDefault();
}}
2022-01-08 16:52:18 +08:00
>
2022-02-19 00:23:47 +08:00
{(hasUrl ? urlModeMenu : fileModeMenu).map((item) => (
<MenuItem
key={item.label}
onClick={item.handler}
2024-03-21 11:39:01 +08:00
sx={[
{
minWidth: 120,
},
(theme) => {
return {
color:
item.label === "Delete"
? theme.palette.error.main
: undefined,
};
},
]}
2022-11-09 15:53:42 +08:00
dense
2022-02-19 00:23:47 +08:00
>
2022-03-18 14:59:23 +08:00
{t(item.label)}
2022-02-10 01:42:52 +08:00
</MenuItem>
))}
2022-01-08 16:52:18 +08:00
</Menu>
2022-03-02 01:58:16 +08:00
2022-11-20 22:37:34 +08:00
<EditorViewer
2022-08-14 23:10:19 +08:00
uid={uid}
open={fileOpen}
mode="yaml"
onClose={() => setFileOpen(false)}
/>
2024-03-21 11:39:01 +08:00
<ConfirmViewer
title="Confirm deletion"
message="This operation is not reversible"
open={confirmOpen}
onClose={() => setConfirmOpen(false)}
onConfirm={() => {
onDelete();
setConfirmOpen(false);
}}
/>
</Box>
2021-12-17 00:44:51 +08:00
);
};
function parseUrl(url?: string) {
if (!url) return "";
const regex = /https?:\/\/(.+?)\//;
const result = url.match(regex);
return result ? result[1] : "local file";
}
function parseExpire(expire?: number) {
if (!expire) return "-";
return dayjs(expire * 1000).format("YYYY-MM-DD");
}