Taio_O 5 месяцев назад
Родитель
Сommit
6b38d343ab
3 измененных файлов с 458 добавлено и 11 удалено
  1. 160 3
      src/assets/styles/layout.scss
  2. 2 2
      src/pages/profiles.tsx
  3. 296 6
      src/pages/quick.tsx

+ 160 - 3
src/assets/styles/layout.scss

@@ -101,6 +101,7 @@
       justify-content: end;
       box-sizing: border-box;
       z-index: 2;
+
       .the-dragbar {
         margin-top: 5px;
         app-region: drag;
@@ -157,29 +158,36 @@
     }
   }
 }
+
 .quickCon {
+  height: 240px;
   display: flex;
   justify-content: center;
-  padding: 20px 0;
+  padding-top: 40px;
   margin: 0 auto;
+  cursor: pointer;
 
   .quickCon1 {
     display: flex;
     justify-content: center;
     align-items: center;
-    background-color: #ededed;
+    background-color: #ececec;
     border-radius: 9999px;
     width: 200px;
     height: 200px;
+    transition: all 0.3s ease-in-out;
+    animation: con1 3s infinite;
 
     .quickCon2 {
       display: flex;
       justify-content: center;
       align-items: center;
-      background-color: #e3e3e3;
+      background-color: #e0dede;
       border-radius: 9999px;
       width: 160px;
       height: 160px;
+      transition: all 0.3s ease-in-out;
+      animation: con2 3s infinite;
 
       .quick {
         display: flex;
@@ -190,7 +198,156 @@
         border-radius: 9999px;
         width: 120px;
         height: 120px;
+        transition: all 0.3s ease-in-out;
+
+        &:hover {
+          background-color: rgb(98, 98, 98);
+        }
+      }
+    }
+  }
+
+  .aquickCon1 {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    background-color: #f3f4f5;
+    border-radius: 9999px;
+    width: 210px;
+    height: 210px;
+    transition: all 0.3s ease-in-out;
+    animation: acon1 3s infinite;
+
+    .aquickCon2 {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      background-color: #d3dff1;
+      border-radius: 9999px;
+      width: 160px;
+      height: 160px;
+      transition: all 0.3s ease-in-out;
+      animation: acon2 3s infinite;
+
+      .aquick {
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        color: #ffffff;
+        background-color: #007aff;
+        border-radius: 9999px;
+        width: 120px;
+        height: 120px;
+        transition: all 0.3s ease-in-out;
+
+        &:hover {
+          background-color: #3898ff;
+        }
       }
     }
   }
 }
+
+@keyframes con1 {
+  0% {
+    width: 160px;
+    height: 160px;
+    background-color: transparent;
+  }
+
+  10% {
+    width: 160px;
+    height: 160px;
+    background-color: #f3f4f5;
+  }
+
+  60% {
+    width: 210px;
+    height: 210px;
+    background-color: rgb(240, 240, 240);
+  }
+
+  100% {
+    width: 160px;
+    height: 160px;
+    background-color: transparent;
+  }
+}
+
+@keyframes con2 {
+  0% {
+    width: 120px;
+    height: 120px;
+    background-color: transparent;
+  }
+
+  10% {
+    width: 120px;
+    height: 120px;
+    background-color: #bdbdbd;
+  }
+
+  60% {
+    width: 180px;
+    height: 180px;
+    background-color: rgb(240, 240, 240);
+  }
+
+  100% {
+    width: 120px;
+    height: 120px;
+    background-color: transparent;
+  }
+}
+
+@keyframes acon1 {
+  0% {
+    width: 160px;
+    height: 160px;
+    background-color: transparent;
+  }
+
+  10% {
+    width: 160px;
+    height: 160px;
+    background-color: #ececec;
+  }
+
+  60% {
+    width: 210px;
+    height: 210px;
+    background-color: rgb(240, 240, 240);
+  }
+
+  100% {
+    width: 160px;
+    height: 160px;
+    background-color: transparent;
+  }
+}
+
+@keyframes acon2 {
+  0% {
+    width: 120px;
+    height: 120px;
+    background-color: transparent;
+  }
+
+  10% {
+    width: 120px;
+    height: 120px;
+    background-color: #9bc3ff;
+  }
+
+  60% {
+    width: 180px;
+    height: 180px;
+    background-color: rgb(240, 240, 240);
+  }
+
+  100% {
+    width: 120px;
+    height: 120px;
+    background-color: transparent;
+  }
+}

+ 2 - 2
src/pages/profiles.tsx

@@ -391,7 +391,7 @@ const ProfilePage = () => {
             </Grid>
           </Box>
         </DndContext>
-        <Divider
+        {/* <Divider
           variant="middle"
           flexItem
           sx={{ width: `calc(100% - 32px)`, borderColor: dividercolor }}
@@ -420,7 +420,7 @@ const ProfilePage = () => {
               />
             </Grid>
           </Grid>
-        </Box>
+        </Box> */}
       </Box>
 
       <ProfileViewer ref={viewerRef} onChange={() => mutateProfiles()} />

+ 296 - 6
src/pages/quick.tsx

@@ -1,7 +1,7 @@
 import { useState, useMemo, useRef } from "react";
 import { useTranslation } from "react-i18next";
 import { Virtuoso } from "react-virtuoso";
-import { getRules, getClashConfig } from "@/services/api";
+import { getRules, getClashConfig, closeAllConnections } from "@/services/api";
 import { BaseEmpty, BasePage, Notice } from "@/components/base";
 import RuleItem from "@/components/rule/rule-item";
 import { ProviderButton } from "@/components/rule/provider-button";
@@ -35,6 +35,30 @@ import {
 import useSWR, { mutate } from "swr";
 import { useProfiles } from "@/hooks/use-profiles";
 import { ProxyGroups } from "@/components/proxy/proxy-groups";
+import { useVerge } from "@/hooks/use-verge";
+import { useLockFn } from "ahooks";
+import getSystem from "@/utils/get-system";
+import {
+  installService,
+  uninstallService,
+  checkService,
+} from "@/services/cmds";
+import {
+  DndContext,
+  closestCenter,
+  KeyboardSensor,
+  PointerSensor,
+  useSensor,
+  useSensors,
+  DragEndEvent,
+} from "@dnd-kit/core";
+import {
+  SortableContext,
+  sortableKeyboardCoordinates,
+} from "@dnd-kit/sortable";
+import { ProfileItem } from "@/components/profile/profile-item";
+import { ProfileMore } from "@/components/profile/profile-more";
+import { useSetLoadingCache, useThemeMode } from "@/services/states";
 
 const QuickPage = () => {
   const { t } = useTranslation();
@@ -107,9 +131,219 @@ const QuickPage = () => {
   );
   const curMode = clashConfig?.mode?.toLowerCase();
 
+  const sensors = useSensors(
+    useSensor(PointerSensor),
+    useSensor(KeyboardSensor, {
+      coordinateGetter: sortableKeyboardCoordinates,
+    })
+  );
+
+  const onDragEnd = async (event: DragEndEvent) => {
+    const { active, over } = event;
+    if (over) {
+      if (active.id !== over.id) {
+        await reorderProfile(active.id.toString(), over.id.toString());
+        mutateProfiles();
+      }
+    }
+  };
+
+  const profileItems = useMemo(() => {
+    const items = profiles.items || [];
+
+    const type1 = ["local", "remote"];
+
+    const profileItems = items.filter((i) => i && type1.includes(i.type!));
+
+    return profileItems;
+  }, [profiles]);
+
+  const currentActivatings = () => {
+    return [...new Set([profiles.current ?? ""])].filter(Boolean);
+  };
+
+  const onSelect = useLockFn(async (current: string, force: boolean) => {
+    if (!force && current === profiles.current) return;
+    // 避免大多数情况下loading态闪烁
+    const reset = setTimeout(() => {
+      setActivatings([...currentActivatings(), current]);
+    }, 100);
+    try {
+      await patchProfiles({ current });
+      await mutateLogs();
+      closeAllConnections();
+      activateSelected().then(() => {
+        Notice.success(t("Profile Switched"), 1000);
+      });
+    } catch (err: any) {
+      Notice.error(err?.message || err.toString(), 4000);
+    } finally {
+      clearTimeout(reset);
+      setActivatings([]);
+    }
+  });
+
+  const onEnhance = useLockFn(async () => {
+    setActivatings(currentActivatings());
+    try {
+      await enhanceProfiles();
+      mutateLogs();
+      Notice.success(t("Profile Reactivated"), 1000);
+    } catch (err: any) {
+      Notice.error(err.message || err.toString(), 3000);
+    } finally {
+      setActivatings([]);
+    }
+  });
+
+  const onDelete = useLockFn(async (uid: string) => {
+    const current = profiles.current === uid;
+    try {
+      setActivatings([...(current ? currentActivatings() : []), uid]);
+      await deleteProfile(uid);
+      mutateProfiles();
+      mutateLogs();
+      current && (await onEnhance());
+    } catch (err: any) {
+      Notice.error(err?.message || err.toString());
+    } finally {
+      setActivatings([]);
+    }
+  });
+
+  const mode = useThemeMode();
+  const islight = mode === "light" ? true : false;
+  const dividercolor = islight
+    ? "rgba(0, 0, 0, 0.06)"
+    : "rgba(255, 255, 255, 0.06)";
+
+  const { verge, mutateVerge, patchVerge } = useVerge();
+  const onChangeData = (patch: Partial<IVergeConfig>) => {
+    mutateVerge({ ...verge, ...patch }, false);
+  };
+
+  const { data: serviceStatus, mutate: themutate } = useSWR(
+    "checkService",
+    checkService,
+    {
+      revalidateIfStale: false,
+      shouldRetryOnError: false,
+      focusThrottleInterval: 36e5, // 1 hour
+    }
+  );
+
+  const isWindows = getSystem() === "windows";
+  const isActive = status === "active";
+  const isInstalled = status === "installed";
+  const isUninstall = status === "uninstall" || status === "unknown";
+  const [serviceLoading, setServiceLoading] = useState(false);
+  const [openInstall, setOpenInstall] = useState(false);
+  const [openUninstall, setOpenUninstall] = useState(false);
+  const [uninstallServiceLoaing, setUninstallServiceLoading] = useState(false);
+
+  // const mutate = 'active';
+
+  async function install(passwd: string) {
+    try {
+      setOpenInstall(false);
+      await installService(passwd);
+      await themutate();
+      setTimeout(() => {
+        themutate();
+      }, 2000);
+      Notice.success(t("Service Installed Successfully"));
+      setServiceLoading(false);
+    } catch (err: any) {
+      await themutate();
+      setTimeout(() => {
+        themutate();
+      }, 2000);
+      Notice.error(err.message || err.toString());
+      setServiceLoading(false);
+    }
+  }
+  const onInstallOrEnableService = useLockFn(async () => {
+    setServiceLoading(true);
+    if (isUninstall) {
+      // install service
+      if (isWindows) {
+        await install("");
+      } else {
+        setOpenInstall(true);
+      }
+    } else {
+      try {
+        // enable or disable service
+        await patchVerge({ enable_service_mode: !isActive });
+        onChangeData({ enable_service_mode: !isActive });
+        await themutate();
+        setTimeout(() => {
+          themutate();
+        }, 2000);
+        setServiceLoading(false);
+      } catch (err: any) {
+        await themutate();
+        Notice.error(err.message || err.toString());
+        setServiceLoading(false);
+      }
+    }
+  });
+
+  async function uninstall(passwd: string) {
+    try {
+      setOpenUninstall(false);
+      await uninstallService(passwd);
+      await themutate();
+      setTimeout(() => {
+        themutate();
+      }, 2000);
+      Notice.success(t("Service Uninstalled Successfully"));
+      setUninstallServiceLoading(false);
+    } catch (err: any) {
+      await themutate();
+      setTimeout(() => {
+        themutate();
+      }, 2000);
+      Notice.error(err.message || err.toString());
+      setUninstallServiceLoading(false);
+    }
+  }
+  const onUninstallService = useLockFn(async () => {
+    setUninstallServiceLoading(true);
+    if (isWindows) {
+      await uninstall("");
+    } else {
+      setOpenUninstall(true);
+    }
+  });
+
+  const {
+    enable_tun_mode,
+    enable_auto_launch,
+    enable_silent_start,
+    enable_system_proxy,
+    enable_service_mode,
+  } = verge ?? {};
+
+  const link = () => {
+    onInstallOrEnableService();
+    onChangeData({ enable_tun_mode: true });
+    patchVerge({ enable_tun_mode: true });
+    onChangeData({ enable_system_proxy: true });
+    patchVerge({ enable_system_proxy: true });
+  };
+
+  const cancelink = async () => {
+    await patchVerge({ enable_service_mode: !isActive });
+    onChangeData({ enable_service_mode: !isActive });
+    onChangeData({ enable_tun_mode: false });
+    patchVerge({ enable_tun_mode: false });
+    onChangeData({ enable_system_proxy: false });
+    patchVerge({ enable_system_proxy: false });
+  };
+
   return (
     <BasePage
-      full
       title={t("Quick")}
       contentStyle={{ height: "100%" }}
       header={
@@ -119,11 +353,23 @@ const QuickPage = () => {
       }
     >
       <div className="quickCon">
-        <div className="quickCon1">
-          <div className="quickCon2">
-            <div className="quick">一键连接</div>
+        {enable_system_proxy && enable_tun_mode && enable_service_mode ? (
+          <div className="aquickCon1">
+            <div className="aquickCon2">
+              <div className="aquick" onClick={cancelink}>
+                取消连接
+              </div>
+            </div>
           </div>
-        </div>
+        ) : (
+          <div className="quickCon1">
+            <div className="quickCon2">
+              <div className="quick" onClick={link}>
+                一键连接
+              </div>
+            </div>
+          </div>
+        )}
       </div>
 
       <Stack
@@ -185,6 +431,50 @@ const QuickPage = () => {
           {t("New")}
         </Button>
       </Stack>
+      <Box
+        sx={{
+          pt: 1,
+          mb: 0.5,
+          pl: "10px",
+          mr: "10px",
+          overflowY: "auto",
+        }}
+      >
+        <DndContext
+          sensors={sensors}
+          collisionDetection={closestCenter}
+          onDragEnd={onDragEnd}
+        >
+          <Box sx={{ mb: 1.5 }}>
+            <Grid container spacing={{ xs: 1, lg: 1 }}>
+              <SortableContext
+                items={profileItems.map((x) => {
+                  return x.uid;
+                })}
+              >
+                {profileItems.map((item) => (
+                  <Grid item xs={12} sm={6} md={4} lg={3} key={item.file}>
+                    <ProfileItem
+                      id={item.uid}
+                      selected={profiles.current === item.uid}
+                      activating={activatings.includes(item.uid)}
+                      itemData={item}
+                      onSelect={(f) => onSelect(item.uid, f)}
+                      onEdit={() => viewerRef.current?.edit(item)}
+                      onSave={async (prev, curr) => {
+                        if (prev !== curr && profiles.current === item.uid) {
+                          await onEnhance();
+                        }
+                      }}
+                      onDelete={() => onDelete(item.uid)}
+                    />
+                  </Grid>
+                ))}
+              </SortableContext>
+            </Grid>
+          </Box>
+        </DndContext>
+      </Box>
       <ProxyGroups mode={curMode!} />
     </BasePage>
   );