123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536 |
- import { useState, useMemo, useRef, useEffect } from "react";
- import { useTranslation } from "react-i18next";
- import { Virtuoso } from "react-virtuoso";
- import {
- getRules,
- getClashConfig,
- closeAllConnections,
- updateConfigs,
- } from "@/services/api";
- import { BaseEmpty, BasePage, Notice } from "@/components/base";
- import RuleItem from "@/components/rule/rule-item";
- import { ProviderButton } from "@/components/rule/provider-button";
- import { useCustomTheme } from "@/components/layout/use-custom-theme";
- import { BaseSearchBox } from "@/components/base/base-search-box";
- import {
- Box,
- Button,
- Grid,
- IconButton,
- Stack,
- ButtonGroup,
- } from "@mui/material";
- import { BaseStyledTextField } from "@/components/base/base-styled-text-field";
- import { readText } from "@tauri-apps/api/clipboard";
- import {
- ClearRounded,
- ContentPasteRounded,
- LocalFireDepartmentRounded,
- RefreshRounded,
- TextSnippetOutlined,
- } from "@mui/icons-material";
- import { LoadingButton } from "@mui/lab";
- import {
- ProfileViewer,
- ProfileViewerRef,
- } from "@/components/profile/profile-viewer";
- import {
- getProfiles,
- importProfile,
- enhanceProfiles,
- getRuntimeLogs,
- deleteProfile,
- updateProfile,
- reorderProfile,
- createProfile,
- } from "@/services/cmds";
- 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,
- patchClashConfig,
- } 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();
- const { data = [] } = useSWR("getRules", getRules);
- const { theme } = useCustomTheme();
- const isDark = theme.palette.mode === "dark";
- const [match, setMatch] = useState(() => (_: string) => true);
- const rules = useMemo(() => {
- return data.filter((item) => match(item.payload));
- }, [data, match]);
- const [url, setUrl] = useState("");
- const [disabled, setDisabled] = useState(false);
- const [activatings, setActivatings] = useState<string[]>([]);
- const [loading, setLoading] = useState(false);
- const onCopyLink = async () => {
- const text = await readText();
- if (text) setUrl(text);
- };
- const {
- profiles = {},
- activateSelected,
- patchProfiles,
- mutateProfiles,
- } = useProfiles();
- const { data: chainLogs = {}, mutate: mutateLogs } = useSWR(
- "getRuntimeLogs",
- getRuntimeLogs
- );
- const onImport = async () => {
- if (!url) return;
- setLoading(true);
- try {
- await importProfile(url);
- Notice.success(t("Profile Imported Successfully"));
- setUrl("");
- setLoading(false);
- getProfiles().then(async (newProfiles) => {
- mutate("getProfiles", newProfiles);
- const remoteItem = newProfiles.items?.find((e) => e.type === "remote");
- if (newProfiles.current && remoteItem) {
- const current = remoteItem.uid;
- await patchProfiles({ current });
- mutateLogs();
- setTimeout(() => activateSelected(), 2000);
- }
- });
- } catch (err: any) {
- Notice.error(err.message || err.toString());
- setLoading(false);
- } finally {
- setDisabled(false);
- setLoading(false);
- }
- };
- const viewerRef = useRef<ProfileViewerRef>(null);
- const { data: clashConfig, mutate: mutateClash } = useSWR(
- "getClashConfig",
- getClashConfig
- );
- 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 isEmpty = profileItems.length === 0;
- 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 = serviceStatus === "active";
- const isInstalled = serviceStatus === "installed";
- const isUninstall =
- serviceStatus === "uninstall" || serviceStatus === "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 [result, setResult] = useState(false);
- useEffect(() => {
- if (data) {
- const { enable_tun_mode, enable_system_proxy, enable_service_mode } =
- verge ?? {};
- // 当 enable_tun_mode, enable_system_proxy, enable_service_mode 同为 true 时,result 为 true
- // 否则,result 为 false
- // setResult(enable_tun_mode && enable_system_proxy && enable_service_mode);
- setResult(
- !!(enable_tun_mode && enable_system_proxy && enable_service_mode)
- );
- }
- }, [data]);
- const link = async () => {
- if (!isEmpty) {
- onInstallOrEnableService();
- await patchVerge({ enable_service_mode: true });
- onChangeData({ enable_service_mode: true });
- onChangeData({ enable_tun_mode: true });
- patchVerge({ enable_tun_mode: true });
- onChangeData({ enable_system_proxy: true });
- patchVerge({ enable_system_proxy: true });
- setResult(true);
- } else {
- Notice.error(t("Profiles Null"));
- }
- };
- const cancelink = async () => {
- await patchVerge({ enable_service_mode: false });
- onChangeData({ enable_service_mode: false });
- onChangeData({ enable_tun_mode: false });
- patchVerge({ enable_tun_mode: false });
- onChangeData({ enable_system_proxy: false });
- patchVerge({ enable_system_proxy: false });
- setResult(false);
- };
- const modeList = ["rule", "global", "direct"];
- const onChangeMode = useLockFn(async (mode: string) => {
- // 断开连接
- if (mode !== curMode && verge?.auto_close_connection) {
- closeAllConnections();
- }
- await updateConfigs({ mode });
- await patchClashConfig({ mode });
- mutateClash();
- });
- useEffect(() => {
- if (curMode && !modeList.includes(curMode)) {
- onChangeMode("rule");
- }
- }, [curMode]);
- return (
- <BasePage
- title={t("Quick")}
- contentStyle={{ height: "100%" }}
- header={
- <Box display="flex" alignItems="center" gap={1}>
- <ProviderButton />
- <ButtonGroup size="small">
- {modeList.map((mode) => (
- <Button
- key={mode}
- variant={mode === curMode ? "contained" : "outlined"}
- onClick={() => onChangeMode(mode)}
- sx={{ textTransform: "capitalize" }}
- >
- {t(mode)}
- </Button>
- ))}
- </ButtonGroup>
- </Box>
- }
- >
- <div className="quickCon">
- {result ? (
- <div className="aquickCon1">
- <div className="aquickCon2">
- <div className="aquick" onClick={cancelink}>
- {t("Close Connection")}
- </div>
- </div>
- </div>
- ) : (
- <div className="quickCon1">
- <div className="quickCon2">
- <div className="quick" onClick={link}>
- {t("Quick Connection")}
- </div>
- </div>
- </div>
- )}
- </div>
- <Stack
- direction="row"
- spacing={1}
- sx={{
- pt: 1,
- mb: 0.5,
- mx: "10px",
- height: "36px",
- display: "flex",
- alignItems: "center",
- }}
- >
- <BaseStyledTextField
- value={url}
- variant="outlined"
- onChange={(e) => setUrl(e.target.value)}
- placeholder={t("Profile URL")}
- InputProps={{
- sx: { pr: 1 },
- endAdornment: !url ? (
- <IconButton
- size="small"
- sx={{ p: 0.5 }}
- title={t("Paste")}
- onClick={onCopyLink}
- >
- <ContentPasteRounded fontSize="inherit" />
- </IconButton>
- ) : (
- <IconButton
- size="small"
- sx={{ p: 0.5 }}
- title={t("Clear")}
- onClick={() => setUrl("")}
- >
- <ClearRounded fontSize="inherit" />
- </IconButton>
- ),
- }}
- />
- <LoadingButton
- disabled={!url || disabled}
- loading={loading}
- variant="contained"
- size="small"
- sx={{ borderRadius: "6px" }}
- onClick={onImport}
- >
- {t("Import")}
- </LoadingButton>
- </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>
- );
- };
- export default QuickPage;
|