refactor: invock mihomo api by use tauri-plugin-mihomo (#4926)

* feat: add tauri-plugin-mihomo

* refactor: invock mihomo api by use tauri-plugin-mihomo

* chore: todo

* chore: update

* chore: update

* chore: update

* chore: update

* fix: incorrect delay status and update pretty config

* chore: update

* chore: remove cache

* chore: update

* chore: update

* fix: app freezed when change group proxy

* chore: update

* chore: update

* chore: add rustfmt.toml to tauri-plugin-mihomo

* chore: happy clippy

* refactor: connect mihomo websocket

* chore: update

* chore: update

* fix: parse bigint to number

* chore: update

* Revert "fix: parse bigint to number"

This reverts commit 74c006522e23aa52cf8979a8fb47d2b1ae0bb043.

* chore: use number instead of bigint

* chore: cleanup

* fix: rule data not refresh when switch profile

* chore: update

* chore: cleanup

* chore: update

* fix: traffic graph data display

* feat: add ipc connection pool

* chore: update

* chore: clippy

* fix: incorrect delay status

* fix: typo

* fix: empty proxies tray menu

* chore: clippy

* chore: import tauri-plugin-mihomo by using git repo

* chore: cleanup

* fix: mihomo api

* fix: incorrect delay status

* chore: update tauri-plugin-mihomo dep

chore: update
This commit is contained in:
oomeow
2025-10-08 12:32:40 +08:00
committed by GitHub
parent 72aa56007c
commit 7fc238c27b
85 changed files with 1780 additions and 3344 deletions

View File

@@ -1,26 +1,33 @@
import { List, Paper, ThemeProvider, SvgIcon } from "@mui/material";
import { List, Paper, SvgIcon, ThemeProvider } from "@mui/material";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import { useLocalStorage } from "foxact/use-local-storage";
import { useEffect, useCallback, useState, useRef } from "react";
import React from "react";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useLocation, useRoutes, useNavigate } from "react-router-dom";
import { useLocation, useNavigate, useRoutes } from "react-router-dom";
import { SWRConfig, mutate } from "swr";
import iconDark from "@/assets/image/icon_dark.svg?react";
import iconLight from "@/assets/image/icon_light.svg?react";
import LogoSvg from "@/assets/image/logo.svg?react";
import { NoticeManager } from "@/components/base/NoticeManager";
import { LayoutItem } from "@/components/layout/layout-item";
import { LayoutTraffic } from "@/components/layout/layout-traffic";
import { UpdateButton } from "@/components/layout/update-button";
import { useCustomTheme } from "@/components/layout/use-custom-theme";
import { useClashInfo } from "@/hooks/use-clash";
import { useConnectionData } from "@/hooks/use-connection-data";
import { useI18n } from "@/hooks/use-i18n";
import { useListen } from "@/hooks/use-listen";
import { useLogData } from "@/hooks/use-log-data-new";
import { useMemoryData } from "@/hooks/use-memory-data";
import { useTrafficData } from "@/hooks/use-traffic-data";
import { useVerge } from "@/hooks/use-verge";
import { getAxios } from "@/services/api";
import { forceRefreshClashConfig } from "@/services/cmds";
import { useThemeMode, useEnableLog } from "@/services/states";
import { showNotice } from "@/services/noticeService";
import { useClashLog, useThemeMode } from "@/services/states";
import getSystem from "@/utils/get-system";
import { routers } from "./_routers";
@@ -28,19 +35,6 @@ import { routers } from "./_routers";
import "dayjs/locale/ru";
import "dayjs/locale/zh-cn";
import { useListen } from "@/hooks/use-listen";
import { listen } from "@tauri-apps/api/event";
import { useClashInfo } from "@/hooks/use-clash";
import { initGlobalLogService } from "@/services/global-log-service";
import { invoke } from "@tauri-apps/api/core";
import { showNotice } from "@/services/noticeService";
import { NoticeManager } from "@/components/base/NoticeManager";
import { LogLevel } from "@/hooks/use-log-data";
const appWindow = getCurrentWebviewWindow();
export const portableFlag = false;
@@ -157,14 +151,20 @@ const handleNoticeMessage = (
};
const Layout = () => {
useTrafficData();
useMemoryData();
useConnectionData();
useLogData();
const mode = useThemeMode();
const isDark = mode === "light" ? false : true;
const { t } = useTranslation();
const { theme } = useCustomTheme();
const { verge } = useVerge();
const { clashInfo } = useClashInfo();
const [enableLog] = useEnableLog();
const [logLevel] = useLocalStorage<LogLevel>("log:log-level", "info");
const [clashLog] = useClashLog();
const enableLog = clashLog.enable;
const logLevel = clashLog.logLevel;
// const [logLevel] = useLocalStorage<LogLevel>("log:log-level", "info");
const { language, start_page } = verge ?? {};
const { switchLanguage } = useI18n();
const navigate = useNavigate();
@@ -193,19 +193,17 @@ const Layout = () => {
);
// 初始化全局日志服务
useEffect(() => {
if (clashInfo) {
initGlobalLogService(enableLog, logLevel);
}
}, [clashInfo, enableLog, logLevel]);
// useEffect(() => {
// if (clashInfo) {
// initGlobalLogService(enableLog, logLevel);
// }
// }, [clashInfo, enableLog, logLevel]);
// 设置监听器
useEffect(() => {
const listeners = [
addListener("verge://refresh-clash-config", async () => {
await getAxios(true);
// 后端配置变更事件触发,强制刷新配置缓存
await forceRefreshClashConfig();
mutate("getProxies");
mutate("getVersion");
mutate("getClashConfig");
@@ -521,15 +519,16 @@ const Layout = () => {
borderTopRightRadius: "0px",
}}
onContextMenu={(e) => {
if (
OS === "windows" &&
!["input", "textarea"].includes(
e.currentTarget.tagName.toLowerCase(),
) &&
!e.currentTarget.isContentEditable
) {
e.preventDefault();
}
// TODO: 禁止右键菜单
// if (
// OS === "windows" &&
// !["input", "textarea"].includes(
// e.currentTarget.tagName.toLowerCase(),
// ) &&
// !e.currentTarget.isContentEditable
// ) {
// e.preventDefault();
// }
}}
sx={[
({ palette }) => ({ bgcolor: palette.background.paper }),

View File

@@ -9,6 +9,7 @@ import { useLockFn } from "ahooks";
import { useCallback, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Virtuoso } from "react-virtuoso";
import { closeAllConnections } from "tauri-plugin-mihomo-api";
import { BaseEmpty, BasePage } from "@/components/base";
import { BaseSearchBox } from "@/components/base/base-search-box";
@@ -19,9 +20,8 @@ import {
} from "@/components/connection/connection-detail";
import { ConnectionItem } from "@/components/connection/connection-item";
import { ConnectionTable } from "@/components/connection/connection-table";
import { useConnectionData } from "@/hooks/use-connection-data";
import { useVisibility } from "@/hooks/use-visibility";
import { useAppData } from "@/providers/app-data-context";
import { closeAllConnections } from "@/services/cmds";
import { useConnectionSetting } from "@/services/states";
import parseTraffic from "@/utils/parse-traffic";
@@ -39,8 +39,9 @@ const ConnectionsPage = () => {
const [match, setMatch] = useState(() => (_: string) => true);
const [curOrderOpt, setOrderOpt] = useState("Default");
// 使用全局数据
const { connections } = useAppData();
const {
response: { data: connections },
} = useConnectionData();
const [setting, setSetting] = useConnectionSetting();
@@ -72,30 +73,30 @@ const ConnectionsPage = () => {
if (isPaused) {
return (
frozenData ?? {
uploadTotal: connections.uploadTotal,
downloadTotal: connections.downloadTotal,
connections: connections.data,
uploadTotal: connections?.uploadTotal,
downloadTotal: connections?.downloadTotal,
connections: connections?.connections,
}
);
}
return {
uploadTotal: connections.uploadTotal,
downloadTotal: connections.downloadTotal,
connections: connections.data,
uploadTotal: connections?.uploadTotal,
downloadTotal: connections?.downloadTotal,
connections: connections?.connections,
};
}, [isPaused, frozenData, connections, pageVisible]);
const [filterConn] = useMemo(() => {
const orderFunc = orderOpts[curOrderOpt];
let conns = displayData.connections.filter((conn) => {
let conns = displayData.connections?.filter((conn) => {
const { host, destinationIP, process } = conn.metadata;
return (
match(host || "") || match(destinationIP || "") || match(process || "")
);
});
if (orderFunc) conns = orderFunc(conns);
if (orderFunc) conns = orderFunc(conns ?? []);
return [conns];
}, [displayData, match, curOrderOpt, orderOpts]);
@@ -112,9 +113,9 @@ const ConnectionsPage = () => {
setIsPaused((prev) => {
if (!prev) {
setFrozenData({
uploadTotal: connections.uploadTotal,
downloadTotal: connections.downloadTotal,
connections: connections.data,
uploadTotal: connections?.uploadTotal ?? 0,
downloadTotal: connections?.downloadTotal ?? 0,
connections: connections?.connections ?? [],
});
} else {
setFrozenData(null);
@@ -206,7 +207,7 @@ const ConnectionsPage = () => {
<BaseSearchBox onSearch={handleSearch} />
</Box>
{filterConn.length === 0 ? (
{!filterConn || filterConn.length === 0 ? (
<BaseEmpty />
) : isTableLayout ? (
<ConnectionTable

View File

@@ -3,7 +3,6 @@ import {
PauseCircleOutlineRounded,
} from "@mui/icons-material";
import { Box, Button, IconButton, MenuItem } from "@mui/material";
import { useLocalStorage } from "foxact/use-local-storage";
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Virtuoso } from "react-virtuoso";
@@ -13,27 +12,22 @@ import { BaseSearchBox } from "@/components/base/base-search-box";
import { SearchState } from "@/components/base/base-search-box";
import { BaseStyledSelect } from "@/components/base/base-styled-select";
import LogItem from "@/components/log/log-item";
import { LogLevel } from "@/hooks/use-log-data";
import {
useGlobalLogData,
clearGlobalLogs,
changeLogLevel,
toggleLogEnabled,
} from "@/services/global-log-service";
import { useEnableLog } from "@/services/states";
// 后端通过 /logs?level={level} 进行筛选,前端不再需要手动筛选日志级别
import { useLogData } from "@/hooks/use-log-data-new";
import { toggleLogEnabled } from "@/services/global-log-service";
import { LogFilter, useClashLog } from "@/services/states";
const LogPage = () => {
const { t } = useTranslation();
const [enableLog, setEnableLog] = useEnableLog();
const [logLevel, setLogLevel] = useLocalStorage<LogLevel>(
"log:log-level",
"info",
);
const [clashLog, setClashLog] = useClashLog();
const enableLog = clashLog.enable;
const logState = clashLog.logFilter;
const [match, setMatch] = useState(() => (_: string) => true);
const logData = useGlobalLogData("all");
const [searchState, setSearchState] = useState<SearchState>();
const {
response: { data: logData },
refreshGetClashLog,
} = useLogData();
const filterLogs = useMemo(() => {
if (!logData || logData.length === 0) {
@@ -49,18 +43,21 @@ const LogPage = () => {
const matchesSearch = match(searchText);
return matchesSearch;
return (
(logState == "all" ? true : data.type.includes(logState)) &&
matchesSearch
);
});
}, [logData, match]);
}, [logData, logState, match]);
const handleLogLevelChange = (newLevel: LogLevel) => {
setLogLevel(newLevel);
changeLogLevel(newLevel);
const handleLogLevelChange = (newLevel: string) => {
setClashLog((pre: any) => ({ ...pre, logFilter: newLevel }));
// changeLogLevel(newLevel);
};
const handleToggleLog = async () => {
await toggleLogEnabled();
setEnableLog(!enableLog);
setClashLog((pre: any) => ({ ...pre, enable: !enableLog }));
};
return (
@@ -92,7 +89,8 @@ const LogPage = () => {
size="small"
variant="contained"
onClick={() => {
clearGlobalLogs();
refreshGetClashLog(true);
// clearGlobalLogs();
}}
>
{t("Clear")}
@@ -111,14 +109,14 @@ const LogPage = () => {
}}
>
<BaseStyledSelect
value={logLevel}
onChange={(e) => handleLogLevelChange(e.target.value as LogLevel)}
value={logState}
onChange={(e) => handleLogLevelChange(e.target.value as LogFilter)}
>
<MenuItem value="all">ALL</MenuItem>
<MenuItem value="debug">DEBUG</MenuItem>
<MenuItem value="info">INFO</MenuItem>
<MenuItem value="warning">WARNING</MenuItem>
<MenuItem value="error">ERROR</MenuItem>
<MenuItem value="warn">WARN</MenuItem>
<MenuItem value="err">ERROR</MenuItem>
</BaseStyledSelect>
<BaseSearchBox
onSearch={(matcher, state) => {

View File

@@ -34,6 +34,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useLocation } from "react-router-dom";
import useSWR, { mutate } from "swr";
import { closeAllConnections } from "tauri-plugin-mihomo-api";
import { BasePage, DialogRef } from "@/components/base";
import { BaseStyledTextField } from "@/components/base/base-styled-text-field";
@@ -47,7 +48,6 @@ import { ConfigViewer } from "@/components/setting/mods/config-viewer";
import { useListen } from "@/hooks/use-listen";
import { useProfiles } from "@/hooks/use-profiles";
import {
closeAllConnections,
createProfile,
deleteProfile,
enhanceProfiles,

View File

@@ -3,14 +3,13 @@ import { useLockFn } from "ahooks";
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import useSWR from "swr";
import { closeAllConnections, getBaseConfig } from "tauri-plugin-mihomo-api";
import { BasePage } from "@/components/base";
import { ProviderButton } from "@/components/proxy/provider-button";
import { ProxyGroups } from "@/components/proxy/proxy-groups";
import { useVerge } from "@/hooks/use-verge";
import {
closeAllConnections,
getClashConfig,
getRuntimeProxyChainConfig,
patchClashMode,
updateProxyChainConfigInRuntime,
@@ -33,7 +32,7 @@ const ProxyPage = () => {
const { data: clashConfig, mutate: mutateClash } = useSWR(
"getClashConfig",
getClashConfig,
getBaseConfig,
{
revalidateOnFocus: false,
revalidateIfStale: true,

View File

@@ -74,7 +74,7 @@ const RulesPage = () => {
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
</Box>
{filteredRules.length > 0 ? (
{filteredRules && filteredRules.length > 0 ? (
<>
<Virtuoso
ref={virtuosoRef}