clash-verge/src/components/profile-item.tsx
2022-01-17 02:28:23 +08:00

242 lines
6.3 KiB
TypeScript

import React, { useRef, useState } from "react";
import dayjs from "dayjs";
import {
alpha,
Box,
styled,
Typography,
LinearProgress,
IconButton,
keyframes,
MenuItem,
Menu,
} from "@mui/material";
import { useSWRConfig } from "swr";
import { RefreshRounded } from "@mui/icons-material";
import { CmdType } from "../services/types";
import { updateProfile, deleteProfile, editProfile } from "../services/cmds";
import Notice from "./notice";
import parseTraffic from "../utils/parse-traffic";
import relativeTime from "dayjs/plugin/relativeTime";
dayjs.extend(relativeTime);
const Wrapper = styled(Box)(({ theme }) => ({
width: "100%",
display: "block",
cursor: "pointer",
textAlign: "left",
borderRadius: theme.shape.borderRadius,
boxShadow: theme.shadows[2],
padding: "8px 16px",
boxSizing: "border-box",
}));
const round = keyframes`
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
`;
interface Props {
index: number;
selected: boolean;
itemData: CmdType.ProfileItem;
onSelect: (force: boolean) => void;
}
const ProfileItem: React.FC<Props> = (props) => {
const { index, selected, itemData, onSelect } = props;
const { mutate } = useSWRConfig();
const [loading, setLoading] = useState(false);
const [anchorEl, setAnchorEl] = useState<any>(null);
const [position, setPosition] = useState({ left: 0, top: 0 });
const { name = "Profile", extra, updated = 0 } = itemData;
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 fromnow = updated > 0 ? dayjs(updated * 1000).fromNow() : "";
const onEdit = async () => {
setAnchorEl(null);
try {
await editProfile(index);
} catch (err: any) {
Notice.error(err.toString());
}
};
const onForceSelect = () => {
setAnchorEl(null);
onSelect(true);
};
const onUpdateWrapper = (withProxy: boolean) => async () => {
setAnchorEl(null);
if (loading) return;
setLoading(true);
try {
await updateProfile(index, withProxy);
mutate("getProfiles");
} catch (err: any) {
Notice.error(err.toString());
} finally {
setLoading(false);
}
};
const deleteRef = useRef(false);
const onDelete = async () => {
setAnchorEl(null);
if (deleteRef.current) return;
deleteRef.current = true;
try {
await deleteProfile(index);
mutate("getProfiles");
} catch (err: any) {
Notice.error(err.toString());
} finally {
deleteRef.current = false;
}
};
const handleContextMenu = (
event: React.MouseEvent<HTMLDivElement, MouseEvent>
) => {
const { clientX, clientY } = event;
setPosition({ top: clientY, left: clientX });
setAnchorEl(event.currentTarget);
event.preventDefault();
};
return (
<>
<Wrapper
sx={({ palette }) => {
const { mode, primary, text, grey } = palette;
const key = `${mode}-${selected}`;
const bgcolor = {
"light-true": alpha(primary.main, 0.15),
"light-false": palette.background.paper,
"dark-true": alpha(primary.main, 0.35),
"dark-false": alpha(grey[700], 0.35),
}[key]!;
const color = {
"light-true": text.secondary,
"light-false": text.secondary,
"dark-true": alpha(text.secondary, 0.6),
"dark-false": alpha(text.secondary, 0.6),
}[key]!;
const h2color = {
"light-true": primary.main,
"light-false": text.primary,
"dark-true": primary.light,
"dark-false": text.primary,
}[key]!;
return { bgcolor, color, "& h2": { color: h2color } };
}}
onClick={() => onSelect(false)}
onContextMenu={handleContextMenu}
>
<Box display="flex" justifyContent="space-between">
<Typography
width="calc(100% - 40px)"
variant="h6"
component="h2"
noWrap
title={name}
>
{name}
</Typography>
<IconButton
sx={{
width: 30,
height: 30,
animation: loading ? `1s linear infinite ${round}` : "none",
}}
color="inherit"
disabled={loading}
onClick={(e) => {
e.stopPropagation();
onUpdateWrapper(false)();
}}
>
<RefreshRounded />
</IconButton>
</Box>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography noWrap title={`From: ${from}`}>
{from}
</Typography>
<Typography
noWrap
flex="1 0 auto"
fontSize={14}
textAlign="right"
title="updated time"
>
{fromnow}
</Typography>
</Box>
<Box
sx={{
my: 0.5,
fontSize: 14,
display: "flex",
justifyContent: "space-between",
}}
>
<span title="used / total">
{parseTraffic(upload + download)} / {parseTraffic(total)}
</span>
<span title="expire time">{expire}</span>
</Box>
<LinearProgress
variant="determinate"
value={progress}
color="inherit"
/>
</Wrapper>
<Menu
open={!!anchorEl}
anchorEl={anchorEl}
onClose={() => setAnchorEl(null)}
anchorPosition={position}
anchorReference="anchorPosition"
>
<MenuItem onClick={onForceSelect}>Select</MenuItem>
<MenuItem onClick={onEdit}>Edit(VScode)</MenuItem>
<MenuItem onClick={onUpdateWrapper(false)}>Update</MenuItem>
<MenuItem onClick={onUpdateWrapper(true)}>Update(Proxy)</MenuItem>
<MenuItem onClick={onDelete}>Delete</MenuItem>
</Menu>
</>
);
};
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");
}
export default ProfileItem;