|
@@ -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>
|
|
|
);
|