import dayjs from "dayjs"; import { mutate } from "swr"; import { useEffect, useState } from "react"; import { useLockFn } from "ahooks"; import { useRecoilState } from "recoil"; import { useTranslation } from "react-i18next"; import { Box, Typography, LinearProgress, IconButton, keyframes, MenuItem, Menu, } from "@mui/material"; 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"; const round = keyframes` from { transform: rotate(0deg); } to { transform: rotate(360deg); } `; interface Props { selected: boolean; itemData: IProfileItem; onSelect: (force: boolean) => void; } export const ProfileItem = (props: Props) => { const { selected, itemData, onSelect } = props; const { t } = useTranslation(); const [anchorEl, setAnchorEl] = useState(null); const [position, setPosition] = useState({ left: 0, top: 0 }); const [loadingCache, setLoadingCache] = useRecoilState(atomLoadingCache); const { uid, name = "Profile", extra, updated = 0 } = itemData; // local file mode // remote file mode const hasUrl = !!itemData.url; const hasExtra = !!extra; // only subscription url has extra info const { upload = 0, download = 0, total = 0 } = extra ?? {}; const from = parseUrl(itemData.url); const expire = parseExpire(extra?.expire); const progress = Math.round(((download + upload) * 100) / (total + 0.1)); const loading = loadingCache[itemData.uid] ?? false; // interval update from now field 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]); const [editOpen, setEditOpen] = useState(false); const [fileOpen, setFileOpen] = useState(false); const onEditInfo = () => { setAnchorEl(null); setEditOpen(true); }; const onEditFile = () => { setAnchorEl(null); setFileOpen(true); }; const onForceSelect = () => { setAnchorEl(null); onSelect(true); }; 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) => { setAnchorEl(null); setLoadingCache((cache) => ({ ...cache, [itemData.uid]: true })); const option: Partial = {}; 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; } } try { await updateProfile(itemData.uid, option); mutate("getProfiles"); } catch (err: any) { const errmsg = err?.message || err.toString(); Notice.error( errmsg.replace(/error sending request for url (\S+?): /, "") ); } finally { setLoadingCache((cache) => ({ ...cache, [itemData.uid]: false })); } }); const onDelete = useLockFn(async () => { setAnchorEl(null); try { await deleteProfile(itemData.uid); mutate("getProfiles"); } catch (err: any) { Notice.error(err?.message || err.toString()); } }); const urlModeMenu = [ { label: "Select", handler: onForceSelect }, { 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) }, { label: "Delete", handler: onDelete }, ]; const fileModeMenu = [ { label: "Select", handler: onForceSelect }, { label: "Edit Info", handler: onEditInfo }, { label: "Edit File", handler: onEditFile }, { label: "Open File", handler: onOpenFile }, { label: "Delete", handler: onDelete }, ]; const boxStyle = { height: 26, display: "flex", alignItems: "center", justifyContent: "space-between", }; return ( <> onSelect(false)} onContextMenu={(event) => { const { clientX, clientY } = event; setPosition({ top: clientY, left: clientX }); setAnchorEl(event.currentTarget); event.preventDefault(); }} > {name} {/* only if has url can it be updated */} {hasUrl && ( { e.stopPropagation(); onUpdate(1); }} > )} {/* the second line show url's info or description */} {hasUrl ? ( <> {from} {updated > 0 ? dayjs(updated * 1000).fromNow() : ""} ) : ( {itemData.desc} )} {/* the third line show extra info or last updated time */} {hasExtra ? ( {parseTraffic(upload + download)} / {parseTraffic(total)} {expire} ) : ( {parseExpire(updated)} )} setAnchorEl(null)} anchorPosition={position} anchorReference="anchorPosition" transitionDuration={225} MenuListProps={{ sx: { py: 0.5 } }} onContextMenu={(e) => { setAnchorEl(null); e.preventDefault(); }} > {(hasUrl ? urlModeMenu : fileModeMenu).map((item) => ( {t(item.label)} ))} setEditOpen(false)} /> setFileOpen(false)} /> ); }; 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"); }