Merge branch 'fix-migrate-tauri2-errors'

* fix-migrate-tauri2-errors: (288 commits)

# Conflicts:
#	.github/ISSUE_TEMPLATE/bug_report.yml
This commit is contained in:
huzibaca
2024-11-24 00:14:46 +08:00
123 changed files with 8721 additions and 5789 deletions

View File

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

View File

@@ -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" },
)
}
>

View File

@@ -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"}
/>
) : (

View File

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

View File

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

View File

@@ -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 />
)}