From a43dab80571f4c21540aca6db231941fb7420a68 Mon Sep 17 00:00:00 2001
From: GyDi <segydi@foxmail.com>
Date: Sat, 5 Mar 2022 22:54:39 +0800
Subject: [PATCH] feat: profile enhanced ui

---
 src/components/profile/profile-edit.tsx |   7 +-
 src/components/profile/profile-item.tsx |  35 ++--
 src/components/profile/profile-more.tsx | 222 ++++++++++++++++++++++++
 src/pages/profiles.tsx                  |  73 +++++---
 4 files changed, 293 insertions(+), 44 deletions(-)
 create mode 100644 src/components/profile/profile-more.tsx

diff --git a/src/components/profile/profile-edit.tsx b/src/components/profile/profile-edit.tsx
index cda39d2..ae5856d 100644
--- a/src/components/profile/profile-edit.tsx
+++ b/src/components/profile/profile-edit.tsx
@@ -50,11 +50,8 @@ const ProfileEdit = (props: Props) => {
   } as const;
 
   const type =
-    form.type ?? form.url
-      ? "remote"
-      : form.file?.endsWith("js")
-      ? "script"
-      : "local";
+    form.type ||
+    (form.url ? "remote" : form.file?.endsWith("js") ? "script" : "local");
 
   return (
     <Dialog open={open} onClose={onClose}>
diff --git a/src/components/profile/profile-item.tsx b/src/components/profile/profile-item.tsx
index 238974d..af4dc53 100644
--- a/src/components/profile/profile-item.tsx
+++ b/src/components/profile/profile-item.tsx
@@ -1,7 +1,7 @@
 import dayjs from "dayjs";
 import { useLockFn } from "ahooks";
 import { useSWRConfig } from "swr";
-import { useEffect, useState, MouseEvent } from "react";
+import { useEffect, useState } from "react";
 import {
   alpha,
   Box,
@@ -84,7 +84,7 @@ const ProfileItem = (props: Props) => {
     try {
       await viewProfile(itemData.uid);
     } catch (err: any) {
-      Notice.error(err.toString());
+      Notice.error(err?.message || err.toString());
     }
   };
 
@@ -109,7 +109,6 @@ const ProfileItem = (props: Props) => {
 
   const onDelete = useLockFn(async () => {
     setAnchorEl(null);
-
     try {
       await deleteProfile(itemData.uid);
       mutate("getProfiles");
@@ -118,13 +117,6 @@ const ProfileItem = (props: Props) => {
     }
   });
 
-  const handleContextMenu = (event: MouseEvent<HTMLDivElement, MouseEvent>) => {
-    const { clientX, clientY } = event;
-    setPosition({ top: clientY, left: clientX });
-    setAnchorEl(event.currentTarget);
-    event.preventDefault();
-  };
-
   const boxStyle = {
     height: 26,
     display: "flex",
@@ -178,7 +170,12 @@ const ProfileItem = (props: Props) => {
           return { bgcolor, color, "& h2": { color: h2color } };
         }}
         onClick={() => onSelect(false)}
-        onContextMenu={handleContextMenu as any}
+        onContextMenu={(event) => {
+          const { clientX, clientY } = event;
+          setPosition({ top: clientY, left: clientX });
+          setAnchorEl(event.currentTarget);
+          event.preventDefault();
+        }}
       >
         <Box display="flex" justifyContent="space-between">
           <Typography
@@ -263,6 +260,10 @@ const ProfileItem = (props: Props) => {
         onClose={() => setAnchorEl(null)}
         anchorPosition={position}
         anchorReference="anchorPosition"
+        onContextMenu={(e) => {
+          setAnchorEl(null);
+          e.preventDefault();
+        }}
       >
         {(hasUrl ? urlModeMenu : fileModeMenu).map((item) => (
           <MenuItem
@@ -275,11 +276,13 @@ const ProfileItem = (props: Props) => {
         ))}
       </Menu>
 
-      <ProfileEdit
-        open={editOpen}
-        itemData={itemData}
-        onClose={() => setEditOpen(false)}
-      />
+      {editOpen && (
+        <ProfileEdit
+          open={editOpen}
+          itemData={itemData}
+          onClose={() => setEditOpen(false)}
+        />
+      )}
     </>
   );
 };
diff --git a/src/components/profile/profile-more.tsx b/src/components/profile/profile-more.tsx
new file mode 100644
index 0000000..367ac5a
--- /dev/null
+++ b/src/components/profile/profile-more.tsx
@@ -0,0 +1,222 @@
+import dayjs from "dayjs";
+import { useLockFn } from "ahooks";
+import { useSWRConfig } from "swr";
+import { useState } from "react";
+import {
+  alpha,
+  Box,
+  Chip,
+  styled,
+  Typography,
+  MenuItem,
+  Menu,
+} from "@mui/material";
+import { CmdType } from "../../services/types";
+import { deleteProfile, viewProfile } from "../../services/cmds";
+import relativeTime from "dayjs/plugin/relativeTime";
+import ProfileEdit from "./profile-edit";
+import Notice from "../base/base-notice";
+
+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",
+}));
+
+interface Props {
+  selected: boolean;
+  itemData: CmdType.ProfileItem;
+  onEnable: () => void;
+  onDisable: () => void;
+  onMoveTop: () => void;
+  onMoveEnd: () => void;
+}
+
+// profile enhanced item
+const ProfileMore = (props: Props) => {
+  const {
+    selected,
+    itemData,
+    onEnable,
+    onDisable,
+    onMoveTop,
+    onMoveEnd,
+  } = props;
+
+  const { type } = itemData;
+  const { mutate } = useSWRConfig();
+  const [anchorEl, setAnchorEl] = useState<any>(null);
+  const [position, setPosition] = useState({ left: 0, top: 0 });
+  const [editOpen, setEditOpen] = useState(false);
+
+  const onEdit = () => {
+    setAnchorEl(null);
+    setEditOpen(true);
+  };
+
+  const onView = async () => {
+    setAnchorEl(null);
+    try {
+      await viewProfile(itemData.uid);
+    } catch (err: any) {
+      Notice.error(err?.message || err.toString());
+    }
+  };
+
+  const onDelete = useLockFn(async () => {
+    setAnchorEl(null);
+    try {
+      await deleteProfile(itemData.uid);
+      mutate("getProfiles");
+    } catch (err: any) {
+      Notice.error(err?.message || err.toString());
+    }
+  });
+
+  const enableMenu = [
+    { label: "Disable", handler: onDisable },
+    { label: "Edit", handler: onEdit },
+    { label: "View File", handler: onView },
+    { label: "To Top", handler: onMoveTop },
+    { label: "To End", handler: onMoveEnd },
+    { label: "Delete", handler: onDelete },
+  ];
+
+  const disableMenu = [
+    { label: "Enable", handler: onEnable },
+    { label: "Edit", handler: onEdit },
+    { label: "View File", handler: onView },
+    { label: "Delete", handler: onDelete },
+  ];
+
+  const boxStyle = {
+    height: 26,
+    display: "flex",
+    alignItems: "center",
+    justifyContent: "space-between",
+    lineHeight: 1,
+  };
+
+  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={(event) => {
+          const { clientX, clientY } = event;
+          setPosition({ top: clientY, left: clientX });
+          setAnchorEl(event.currentTarget);
+          event.preventDefault();
+        }}
+      >
+        <Box display="flex" justifyContent="space-between" alignItems="center">
+          <Typography
+            width="calc(100% - 52px)"
+            variant="h6"
+            component="h2"
+            noWrap
+            title={itemData.name}
+          >
+            {itemData.name}
+          </Typography>
+
+          <Chip
+            label={type}
+            color="primary"
+            size="small"
+            variant="outlined"
+            sx={{ textTransform: "capitalize" }}
+          />
+        </Box>
+
+        <Box sx={boxStyle}>
+          <Typography
+            noWrap
+            title={itemData.desc}
+            sx={{ width: "calc(100% - 75px)" }}
+          >
+            {itemData.desc}
+          </Typography>
+
+          <Typography
+            component="span"
+            title="updated time"
+            style={{ fontSize: 14 }}
+          >
+            {parseExpire(itemData.updated)}
+          </Typography>
+        </Box>
+      </Wrapper>
+
+      <Menu
+        open={!!anchorEl}
+        anchorEl={anchorEl}
+        onClose={() => setAnchorEl(null)}
+        anchorPosition={position}
+        anchorReference="anchorPosition"
+        onContextMenu={(e) => {
+          setAnchorEl(null);
+          e.preventDefault();
+        }}
+      >
+        {(selected ? enableMenu : disableMenu).map((item) => (
+          <MenuItem
+            key={item.label}
+            onClick={item.handler}
+            sx={{ minWidth: 133 }}
+          >
+            {item.label}
+          </MenuItem>
+        ))}
+      </Menu>
+
+      {editOpen && (
+        <ProfileEdit
+          open={editOpen}
+          itemData={itemData}
+          onClose={() => setEditOpen(false)}
+        />
+      )}
+    </>
+  );
+};
+
+function parseExpire(expire?: number) {
+  if (!expire) return "-";
+  return dayjs(expire * 1000).format("YYYY-MM-DD");
+}
+
+export default ProfileMore;
diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx
index b7ee9f9..1557d59 100644
--- a/src/pages/profiles.tsx
+++ b/src/pages/profiles.tsx
@@ -1,7 +1,7 @@
 import useSWR, { useSWRConfig } from "swr";
-import { useEffect, useState } from "react";
+import { useEffect, useMemo, useState } from "react";
 import { useLockFn } from "ahooks";
-import { Box, Button, Grid, TextField, Typography } from "@mui/material";
+import { Box, Button, Grid, TextField } from "@mui/material";
 import {
   getProfiles,
   selectProfile,
@@ -13,21 +13,34 @@ import Notice from "../components/base/base-notice";
 import BasePage from "../components/base/base-page";
 import ProfileNew from "../components/profile/profile-new";
 import ProfileItem from "../components/profile/profile-item";
+import ProfileMore from "../components/profile/profile-more";
 
 const ProfilePage = () => {
+  const { mutate } = useSWRConfig();
+
   const [url, setUrl] = useState("");
   const [disabled, setDisabled] = useState(false);
-
-  const { mutate } = useSWRConfig();
-  const { data: profiles = {} } = useSWR("getProfiles", getProfiles);
   const [dialogOpen, setDialogOpen] = useState(false);
 
+  const { data: profiles = {} } = useSWR("getProfiles", getProfiles);
+
+  const { regularItems, enhanceItems } = useMemo(() => {
+    const { items = [] } = profiles;
+    const regularItems = items.filter((i) =>
+      ["local", "remote"].includes(i.type!)
+    );
+    const enhanceItems = items.filter((i) =>
+      ["merge", "script"].includes(i.type!)
+    );
+
+    return { regularItems, enhanceItems };
+  }, [profiles]);
+
   useEffect(() => {
     if (profiles.current == null) return;
-    if (!profiles.items) profiles.items = [];
 
     const current = profiles.current;
-    const profile = profiles.items.find((p) => p.uid === current);
+    const profile = regularItems.find((p) => p.uid === current);
     if (!profile) return;
 
     setTimeout(async () => {
@@ -62,7 +75,7 @@ const ProfilePage = () => {
       // update proxies cache
       if (hasChange) mutate("getProxies", getProxies());
     }, 100);
-  }, [profiles]);
+  }, [profiles, regularItems]);
 
   const onImport = async () => {
     if (!url) return;
@@ -89,22 +102,27 @@ const ProfilePage = () => {
     }
   };
 
-  const onSelect = useLockFn(async (current: string, force: boolean) => {
-    if (!force && current === profiles.current) return;
+  const onSelect = useLockFn(async (uid: string, force: boolean) => {
+    if (!force && uid === profiles.current) return;
 
     try {
-      await selectProfile(current);
-      mutate("getProfiles", { ...profiles, current: current }, true);
+      await selectProfile(uid);
+      mutate("getProfiles", { ...profiles, current: uid }, true);
     } catch (err: any) {
-      err && Notice.error(err.message || err.toString());
+      Notice.error(err?.message || err.toString());
     }
   });
 
+  const onEnhanceEnable = useLockFn(async (uid: string) => {});
+  const onEnhanceDisable = useLockFn(async (uid: string) => {});
+  const onMoveTop = useLockFn(async (uid: string) => {});
+  const onMoveEnd = useLockFn(async (uid: string) => {});
+
   return (
     <BasePage title="Profiles">
-      <Box sx={{ display: "flex", mb: 3 }}>
+      <Box sx={{ display: "flex", mb: 2.5 }}>
         <TextField
-          id="profile_url"
+          id="clas_verge_profile_url"
           name="profile_url"
           label="Profile URL"
           size="small"
@@ -126,8 +144,8 @@ const ProfilePage = () => {
         </Button>
       </Box>
 
-      <Grid container spacing={3}>
-        {profiles?.items?.map((item) => (
+      <Grid container spacing={2}>
+        {regularItems.map((item) => (
           <Grid item xs={12} sm={6} key={item.file}>
             <ProfileItem
               selected={profiles.current === item.uid}
@@ -138,13 +156,22 @@ const ProfilePage = () => {
         ))}
       </Grid>
 
-      <ProfileNew open={dialogOpen} onClose={() => setDialogOpen(false)} />
+      <Grid container spacing={2} sx={{ mt: 3 }}>
+        {enhanceItems.map((item) => (
+          <Grid item xs={12} sm={6} key={item.file}>
+            <ProfileMore
+              selected={!!profiles.chain?.includes(item.uid)}
+              itemData={item}
+              onEnable={() => onEnhanceEnable(item.uid)}
+              onDisable={() => onEnhanceDisable(item.uid)}
+              onMoveTop={() => onMoveTop(item.uid)}
+              onMoveEnd={() => onMoveEnd(item.uid)}
+            />
+          </Grid>
+        ))}
+      </Grid>
 
-      <header data-windrag style={{ marginTop: 20, userSelect: "none" }}>
-        <Typography variant="h5" component="h2" data-windrag>
-          Enhanced
-        </Typography>
-      </header>
+      <ProfileNew open={dialogOpen} onClose={() => setDialogOpen(false)} />
     </BasePage>
   );
 };