mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-01-29 17:15:38 +08:00
Merge branch 'fix-migrate-tauri2-errors'
* fix-migrate-tauri2-errors: (288 commits) # Conflicts: # .github/ISSUE_TEMPLATE/bug_report.yml
This commit is contained in:
@@ -4,9 +4,8 @@ import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import { SWRConfig, mutate } from "swr";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocation, useRoutes } from "react-router-dom";
|
||||
import { useLocation, useRoutes, useNavigate } from "react-router-dom";
|
||||
import { List, Paper, ThemeProvider, SvgIcon } from "@mui/material";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
import { routers } from "./_routers";
|
||||
import { getAxios } from "@/services/api";
|
||||
@@ -25,9 +24,10 @@ import getSystem from "@/utils/get-system";
|
||||
import "dayjs/locale/ru";
|
||||
import "dayjs/locale/zh-cn";
|
||||
import { getPortableFlag } from "@/services/cmds";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import React from "react";
|
||||
import { TransitionGroup, CSSTransition } from "react-transition-group";
|
||||
import { useListen } from "@/hooks/use-listen";
|
||||
|
||||
const appWindow = getCurrentWebviewWindow();
|
||||
export let portableFlag = false;
|
||||
|
||||
@@ -46,17 +46,13 @@ const Layout = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const routersEles = useRoutes(routers);
|
||||
const { addListener, setupCloseListener } = useListen();
|
||||
if (!routersEles) return null;
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("keydown", (e) => {
|
||||
// macOS有cmd+w
|
||||
if (e.key === "Escape" && OS !== "macos") {
|
||||
appWindow.close();
|
||||
}
|
||||
});
|
||||
setupCloseListener();
|
||||
|
||||
listen("verge://refresh-clash-config", async () => {
|
||||
useEffect(() => {
|
||||
addListener("verge://refresh-clash-config", async () => {
|
||||
// the clash info may be updated
|
||||
await getAxios(true);
|
||||
mutate("getProxies");
|
||||
@@ -66,12 +62,21 @@ const Layout = () => {
|
||||
});
|
||||
|
||||
// update the verge config
|
||||
listen("verge://refresh-verge-config", () => mutate("getVergeConfig"));
|
||||
addListener("verge://refresh-verge-config", () => mutate("getVergeConfig"));
|
||||
|
||||
// 设置提示监听
|
||||
listen("verge://notice-message", ({ payload }) => {
|
||||
addListener("verge://notice-message", ({ payload }) => {
|
||||
const [status, msg] = payload as [string, string];
|
||||
switch (status) {
|
||||
case "import_sub_url::ok":
|
||||
navigate("/profile", { state: { current: msg } });
|
||||
|
||||
Notice.success(t("Import Subscription Successful"));
|
||||
break;
|
||||
case "import_sub_url::error":
|
||||
navigate("/profile");
|
||||
Notice.error(msg);
|
||||
break;
|
||||
case "set_config::ok":
|
||||
Notice.success(t("Clash Config Updated"));
|
||||
break;
|
||||
|
||||
@@ -15,11 +15,11 @@ import {
|
||||
ConnectionDetailRef,
|
||||
} from "@/components/connection/connection-detail";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
import { useCustomTheme } from "@/components/layout/use-custom-theme";
|
||||
import { BaseSearchBox } from "@/components/base/base-search-box";
|
||||
import { BaseStyledSelect } from "@/components/base/base-styled-select";
|
||||
import useSWRSubscription from "swr/subscription";
|
||||
import { createSockette } from "@/utils/websocket";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
const initConn: IConnections = {
|
||||
uploadTotal: 0,
|
||||
@@ -32,7 +32,8 @@ type OrderFunc = (list: IConnectionsItem[]) => IConnectionsItem[];
|
||||
const ConnectionsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const { clashInfo } = useClashInfo();
|
||||
const { theme } = useCustomTheme();
|
||||
|
||||
const theme = useTheme();
|
||||
const isDark = theme.palette.mode === "dark";
|
||||
const [match, setMatch] = useState(() => (_: string) => true);
|
||||
const [curOrderOpt, setOrderOpt] = useState("Default");
|
||||
@@ -46,7 +47,7 @@ const ConnectionsPage = () => {
|
||||
list.sort(
|
||||
(a, b) =>
|
||||
new Date(b.start || "0").getTime()! -
|
||||
new Date(a.start || "0").getTime()!
|
||||
new Date(a.start || "0").getTime()!,
|
||||
),
|
||||
"Upload Speed": (list) => list.sort((a, b) => b.curUpload! - a.curUpload!),
|
||||
"Download Speed": (list) =>
|
||||
@@ -102,7 +103,7 @@ const ConnectionsPage = () => {
|
||||
next(event);
|
||||
},
|
||||
},
|
||||
3
|
||||
3,
|
||||
);
|
||||
|
||||
return () => {
|
||||
@@ -113,7 +114,7 @@ const ConnectionsPage = () => {
|
||||
const [filterConn, download, upload] = useMemo(() => {
|
||||
const orderFunc = orderOpts[curOrderOpt];
|
||||
let connections = connData.connections.filter((conn) =>
|
||||
match(conn.metadata.host || conn.metadata.destinationIP || "")
|
||||
match(conn.metadata.host || conn.metadata.destinationIP || ""),
|
||||
);
|
||||
|
||||
if (orderFunc) connections = orderFunc(connections);
|
||||
@@ -151,7 +152,7 @@ const ConnectionsPage = () => {
|
||||
setSetting((o) =>
|
||||
o?.layout !== "table"
|
||||
? { ...o, layout: "table" }
|
||||
: { ...o, layout: "list" }
|
||||
: { ...o, layout: "list" },
|
||||
)
|
||||
}
|
||||
>
|
||||
|
||||
@@ -2,35 +2,43 @@ import { useMemo, useState } from "react";
|
||||
import { Box, Button, IconButton, MenuItem } from "@mui/material";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocalStorage } from "foxact/use-local-storage";
|
||||
|
||||
import {
|
||||
PlayCircleOutlineRounded,
|
||||
PauseCircleOutlineRounded,
|
||||
} from "@mui/icons-material";
|
||||
import { useLogData } from "@/hooks/use-log-data";
|
||||
import { useLogData, LogLevel, clearLogs } from "@/hooks/use-log-data";
|
||||
import { useEnableLog } from "@/services/states";
|
||||
import { BaseEmpty, BasePage } from "@/components/base";
|
||||
import LogItem from "@/components/log/log-item";
|
||||
import { useCustomTheme } from "@/components/layout/use-custom-theme";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { BaseSearchBox } from "@/components/base/base-search-box";
|
||||
import { BaseStyledSelect } from "@/components/base/base-styled-select";
|
||||
import { mutate } from "swr";
|
||||
import { SearchState } from "@/components/base/base-search-box";
|
||||
|
||||
const LogPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const { data: logData = [] } = useLogData();
|
||||
const [enableLog, setEnableLog] = useEnableLog();
|
||||
const { theme } = useCustomTheme();
|
||||
const theme = useTheme();
|
||||
const isDark = theme.palette.mode === "dark";
|
||||
const [logState, setLogState] = useState("all");
|
||||
const [logLevel, setLogLevel] = useLocalStorage<LogLevel>(
|
||||
"log:log-level",
|
||||
"info",
|
||||
);
|
||||
const [match, setMatch] = useState(() => (_: string) => true);
|
||||
const logData = useLogData(logLevel);
|
||||
const [searchState, setSearchState] = useState<SearchState>();
|
||||
|
||||
const filterLogs = useMemo(() => {
|
||||
return logData.filter(
|
||||
(data) =>
|
||||
(logState === "all" ? true : data.type.includes(logState)) &&
|
||||
match(data.payload)
|
||||
);
|
||||
}, [logData, logState, match]);
|
||||
return logData
|
||||
? logData.filter((data) =>
|
||||
logLevel === "all"
|
||||
? match(data.payload)
|
||||
: data.type.includes(logLevel) && match(data.payload),
|
||||
)
|
||||
: [];
|
||||
}, [logData, logLevel, match]);
|
||||
|
||||
return (
|
||||
<BasePage
|
||||
@@ -52,15 +60,17 @@ const LogPage = () => {
|
||||
)}
|
||||
</IconButton>
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
// useSWRSubscription adds a prefix "$sub$" to the cache key
|
||||
// https://github.com/vercel/swr/blob/1585a3e37d90ad0df8097b099db38f1afb43c95d/src/subscription/index.ts#L37
|
||||
onClick={() => mutate("$sub$getClashLog", [])}
|
||||
>
|
||||
{t("Clear")}
|
||||
</Button>
|
||||
{enableLog === true && (
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
clearLogs(logLevel);
|
||||
}}
|
||||
>
|
||||
{t("Clear")}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
@@ -75,15 +85,21 @@ const LogPage = () => {
|
||||
}}
|
||||
>
|
||||
<BaseStyledSelect
|
||||
value={logState}
|
||||
onChange={(e) => setLogState(e.target.value)}
|
||||
value={logLevel}
|
||||
onChange={(e) => setLogLevel(e.target.value as LogLevel)}
|
||||
>
|
||||
<MenuItem value="all">ALL</MenuItem>
|
||||
<MenuItem value="inf">INFO</MenuItem>
|
||||
<MenuItem value="warn">WARN</MenuItem>
|
||||
<MenuItem value="err">ERROR</MenuItem>
|
||||
<MenuItem value="info">INFO</MenuItem>
|
||||
<MenuItem value="warning">WARNING</MenuItem>
|
||||
<MenuItem value="error">ERROR</MenuItem>
|
||||
<MenuItem value="debug">DEBUG</MenuItem>
|
||||
</BaseStyledSelect>
|
||||
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
|
||||
<BaseSearchBox
|
||||
onSearch={(matcher, state) => {
|
||||
setMatch(() => matcher);
|
||||
setSearchState(state);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
@@ -98,7 +114,9 @@ const LogPage = () => {
|
||||
<Virtuoso
|
||||
initialTopMostItemIndex={999}
|
||||
data={filterLogs}
|
||||
itemContent={(index, item) => <LogItem value={item} />}
|
||||
itemContent={(index, item) => (
|
||||
<LogItem value={item} searchState={searchState} />
|
||||
)}
|
||||
followOutput={"smooth"}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -47,13 +47,15 @@ import { useProfiles } from "@/hooks/use-profiles";
|
||||
import { ConfigViewer } from "@/components/setting/mods/config-viewer";
|
||||
import { throttle } from "lodash-es";
|
||||
import { BaseStyledTextField } from "@/components/base/base-styled-text-field";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { readTextFile } from "@tauri-apps/plugin-fs";
|
||||
import { readText } from "@tauri-apps/plugin-clipboard-manager";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useListen } from "@/hooks/use-listen";
|
||||
|
||||
const ProfilePage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const location = useLocation();
|
||||
const { addListener } = useListen();
|
||||
const [url, setUrl] = useState("");
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
const [activatings, setActivatings] = useState<string[]>([]);
|
||||
@@ -62,11 +64,12 @@ const ProfilePage = () => {
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
})
|
||||
}),
|
||||
);
|
||||
const { current } = location.state || {};
|
||||
|
||||
useEffect(() => {
|
||||
const unlisten = listen("tauri://file-drop", async (event) => {
|
||||
const unlisten = addListener("tauri://file-drop", async (event) => {
|
||||
const fileList = event.payload as string[];
|
||||
for (let file of fileList) {
|
||||
if (!file.endsWith(".yaml") && !file.endsWith(".yml")) {
|
||||
@@ -102,7 +105,7 @@ const ProfilePage = () => {
|
||||
|
||||
const { data: chainLogs = {}, mutate: mutateLogs } = useSWR(
|
||||
"getRuntimeLogs",
|
||||
getRuntimeLogs
|
||||
getRuntimeLogs,
|
||||
);
|
||||
|
||||
const viewerRef = useRef<ProfileViewerRef>(null);
|
||||
@@ -132,23 +135,8 @@ const ProfilePage = () => {
|
||||
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");
|
||||
|
||||
const profilesCount = newProfiles.items?.filter(
|
||||
(e) => e.type === "remote" || e.type === "local"
|
||||
).length as number;
|
||||
|
||||
if (remoteItem && (profilesCount == 1 || !newProfiles.current)) {
|
||||
const current = remoteItem.uid;
|
||||
await patchProfiles({ current });
|
||||
mutateLogs();
|
||||
setTimeout(() => activateSelected(), 2000);
|
||||
}
|
||||
});
|
||||
mutateProfiles();
|
||||
await onEnhance(false);
|
||||
} catch (err: any) {
|
||||
Notice.error(err.message || err.toString());
|
||||
setLoading(false);
|
||||
@@ -168,33 +156,49 @@ const ProfilePage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const onSelect = useLockFn(async (current: string, force: boolean) => {
|
||||
if (!force && current === profiles.current) return;
|
||||
const activateProfile = async (profile: string, notifySuccess: boolean) => {
|
||||
// 避免大多数情况下loading态闪烁
|
||||
const reset = setTimeout(() => {
|
||||
setActivatings([...currentActivatings(), current]);
|
||||
setActivatings((prev) => [...prev, profile]);
|
||||
}, 100);
|
||||
|
||||
try {
|
||||
await patchProfiles({ current });
|
||||
await patchProfiles({ current: profile });
|
||||
await mutateLogs();
|
||||
closeAllConnections();
|
||||
activateSelected().then(() => {
|
||||
await activateSelected();
|
||||
if (notifySuccess) {
|
||||
Notice.success(t("Profile Switched"), 1000);
|
||||
});
|
||||
}
|
||||
} catch (err: any) {
|
||||
Notice.error(err?.message || err.toString(), 4000);
|
||||
} finally {
|
||||
clearTimeout(reset);
|
||||
setActivatings([]);
|
||||
}
|
||||
};
|
||||
const onSelect = useLockFn(async (current: string, force: boolean) => {
|
||||
if (!force && current === profiles.current) return;
|
||||
await activateProfile(current, true);
|
||||
});
|
||||
|
||||
const onEnhance = useLockFn(async () => {
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (current) {
|
||||
mutateProfiles();
|
||||
await activateProfile(current, false);
|
||||
}
|
||||
})();
|
||||
}, current);
|
||||
|
||||
const onEnhance = useLockFn(async (notifySuccess: boolean) => {
|
||||
setActivatings(currentActivatings());
|
||||
try {
|
||||
await enhanceProfiles();
|
||||
mutateLogs();
|
||||
Notice.success(t("Profile Reactivated"), 1000);
|
||||
if (notifySuccess) {
|
||||
Notice.success(t("Profile Reactivated"), 1000);
|
||||
}
|
||||
} catch (err: any) {
|
||||
Notice.error(err.message || err.toString(), 3000);
|
||||
} finally {
|
||||
@@ -209,7 +213,7 @@ const ProfilePage = () => {
|
||||
await deleteProfile(uid);
|
||||
mutateProfiles();
|
||||
mutateLogs();
|
||||
current && (await onEnhance());
|
||||
current && (await onEnhance(false));
|
||||
} catch (err: any) {
|
||||
Notice.error(err?.message || err.toString());
|
||||
} finally {
|
||||
@@ -236,7 +240,7 @@ const ProfilePage = () => {
|
||||
setLoadingCache((cache) => {
|
||||
// 获取没有正在更新的订阅
|
||||
const items = profileItems.filter(
|
||||
(e) => e.type === "remote" && !cache[e.uid]
|
||||
(e) => e.type === "remote" && !cache[e.uid],
|
||||
);
|
||||
const change = Object.fromEntries(items.map((e) => [e.uid, true]));
|
||||
|
||||
@@ -286,7 +290,7 @@ const ProfilePage = () => {
|
||||
size="small"
|
||||
color="primary"
|
||||
title={t("Reactivate Profiles")}
|
||||
onClick={onEnhance}
|
||||
onClick={() => onEnhance(true)}
|
||||
>
|
||||
<LocalFireDepartmentRounded />
|
||||
</IconButton>
|
||||
@@ -385,7 +389,7 @@ const ProfilePage = () => {
|
||||
onEdit={() => viewerRef.current?.edit(item)}
|
||||
onSave={async (prev, curr) => {
|
||||
if (prev !== curr && profiles.current === item.uid) {
|
||||
await onEnhance();
|
||||
await onEnhance(false);
|
||||
}
|
||||
}}
|
||||
onDelete={() => onDelete(item.uid)}
|
||||
@@ -408,7 +412,7 @@ const ProfilePage = () => {
|
||||
id="Merge"
|
||||
onSave={async (prev, curr) => {
|
||||
if (prev !== curr) {
|
||||
await onEnhance();
|
||||
await onEnhance(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -419,7 +423,7 @@ const ProfilePage = () => {
|
||||
logInfo={chainLogs["Script"]}
|
||||
onSave={async (prev, curr) => {
|
||||
if (prev !== curr) {
|
||||
await onEnhance();
|
||||
await onEnhance(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -428,7 +432,13 @@ const ProfilePage = () => {
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<ProfileViewer ref={viewerRef} onChange={() => mutateProfiles()} />
|
||||
<ProfileViewer
|
||||
ref={viewerRef}
|
||||
onChange={async () => {
|
||||
mutateProfiles();
|
||||
await onEnhance(false);
|
||||
}}
|
||||
/>
|
||||
<ConfigViewer ref={configRef} />
|
||||
</BasePage>
|
||||
);
|
||||
|
||||
@@ -3,11 +3,7 @@ import { useEffect } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Box, Button, ButtonGroup } from "@mui/material";
|
||||
import {
|
||||
closeAllConnections,
|
||||
getClashConfig,
|
||||
updateConfigs,
|
||||
} from "@/services/api";
|
||||
import { closeAllConnections, getClashConfig } from "@/services/api";
|
||||
import { patchClashConfig } from "@/services/cmds";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { BasePage } from "@/components/base";
|
||||
@@ -19,7 +15,7 @@ const ProxyPage = () => {
|
||||
|
||||
const { data: clashConfig, mutate: mutateClash } = useSWR(
|
||||
"getClashConfig",
|
||||
getClashConfig
|
||||
getClashConfig,
|
||||
);
|
||||
|
||||
const { verge } = useVerge();
|
||||
@@ -33,7 +29,6 @@ const ProxyPage = () => {
|
||||
if (mode !== curMode && verge?.auto_close_connection) {
|
||||
closeAllConnections();
|
||||
}
|
||||
await updateConfigs({ mode });
|
||||
await patchClashConfig({ mode });
|
||||
mutateClash();
|
||||
});
|
||||
|
||||
@@ -1,26 +1,40 @@
|
||||
import useSWR from "swr";
|
||||
import { useState, useMemo } from "react";
|
||||
import { useState, useMemo, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
import { Virtuoso, VirtuosoHandle } from "react-virtuoso";
|
||||
import { Box } from "@mui/material";
|
||||
import { getRules } from "@/services/api";
|
||||
import { BaseEmpty, BasePage } 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 { useTheme } from "@mui/material/styles";
|
||||
import { ScrollTopButton } from "@/components/layout/scroll-top-button";
|
||||
|
||||
const RulesPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const { data = [] } = useSWR("getRules", getRules);
|
||||
const { theme } = useCustomTheme();
|
||||
const theme = useTheme();
|
||||
const isDark = theme.palette.mode === "dark";
|
||||
const [match, setMatch] = useState(() => (_: string) => true);
|
||||
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
||||
const [showScrollTop, setShowScrollTop] = useState(false);
|
||||
|
||||
const rules = useMemo(() => {
|
||||
return data.filter((item) => match(item.payload));
|
||||
}, [data, match]);
|
||||
|
||||
const scrollToTop = () => {
|
||||
virtuosoRef.current?.scrollTo({
|
||||
top: 0,
|
||||
behavior: "smooth",
|
||||
});
|
||||
};
|
||||
|
||||
const handleScroll = (e: any) => {
|
||||
setShowScrollTop(e.target.scrollTop > 100);
|
||||
};
|
||||
|
||||
return (
|
||||
<BasePage
|
||||
full
|
||||
@@ -51,16 +65,24 @@ const RulesPage = () => {
|
||||
margin: "10px",
|
||||
borderRadius: "8px",
|
||||
bgcolor: isDark ? "#282a36" : "#ffffff",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{rules.length > 0 ? (
|
||||
<Virtuoso
|
||||
data={rules}
|
||||
itemContent={(index, item) => (
|
||||
<RuleItem index={index + 1} value={item} />
|
||||
)}
|
||||
followOutput={"smooth"}
|
||||
/>
|
||||
<>
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
data={rules}
|
||||
itemContent={(index, item) => (
|
||||
<RuleItem index={index + 1} value={item} />
|
||||
)}
|
||||
followOutput={"smooth"}
|
||||
scrollerRef={(ref) => {
|
||||
if (ref) ref.addEventListener("scroll", handleScroll);
|
||||
}}
|
||||
/>
|
||||
<ScrollTopButton onClick={scrollToTop} show={showScrollTop} />
|
||||
</>
|
||||
) : (
|
||||
<BaseEmpty />
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user