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

@@ -4,8 +4,8 @@ import dayjs from "dayjs";
import { t } from "i18next";
import { useImperativeHandle, useState, type Ref } from "react";
import { deleteConnection } from "@/services/cmds";
import parseTraffic from "@/utils/parse-traffic";
import { closeConnections } from "tauri-plugin-mihomo-api";
export interface ConnectionDetailRef {
open: (detail: IConnectionsItem) => void;
@@ -97,7 +97,7 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
{ label: t("Type"), value: `${metadata.type}(${metadata.network})` },
];
const onDelete = useLockFn(async () => deleteConnection(data.id));
const onDelete = useLockFn(async () => closeConnections(data.id));
return (
<Box sx={{ userSelect: "text", color: theme.palette.text.secondary }}>

View File

@@ -10,8 +10,8 @@ import {
import { useLockFn } from "ahooks";
import dayjs from "dayjs";
import { deleteConnection } from "@/services/cmds";
import parseTraffic from "@/utils/parse-traffic";
import { closeConnections } from "tauri-plugin-mihomo-api";
const Tag = styled("span")(({ theme }) => ({
fontSize: "10px",
@@ -34,7 +34,7 @@ export const ConnectionItem = (props: Props) => {
const { id, metadata, chains, start, curUpload, curDownload } = value;
const onDelete = useLockFn(async () => deleteConnection(id));
const onDelete = useLockFn(async () => closeConnections(id));
const showTraffic = curUpload! >= 100 || curDownload! >= 100;
return (

View File

@@ -53,7 +53,7 @@ export const ClashInfoCard = () => {
{t("Mixed Port")}
</Typography>
<Typography variant="body2" fontWeight="medium">
{clashConfig["mixed-port"] || "-"}
{clashConfig.mixedPort || "-"}
</Typography>
</Stack>
<Divider />

View File

@@ -7,10 +7,11 @@ import { Box, Paper, Stack, Typography } from "@mui/material";
import { useLockFn } from "ahooks";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { closeAllConnections } from "tauri-plugin-mihomo-api";
import { useVerge } from "@/hooks/use-verge";
import { useAppData } from "@/providers/app-data-context";
import { closeAllConnections, patchClashMode } from "@/services/cmds";
import { patchClashMode } from "@/services/cmds";
export const ClashModeCard = () => {
const { t } = useTranslation();

View File

@@ -36,8 +36,8 @@ import { EnhancedCard } from "@/components/home/enhanced-card";
import { useProxySelection } from "@/hooks/use-proxy-selection";
import { useVerge } from "@/hooks/use-verge";
import { useAppData } from "@/providers/app-data-context";
import { getGroupProxyDelays, providerHealthCheck } from "@/services/cmds";
import delayManager from "@/services/delay";
import { delayGroup, healthcheckProxyProvider } from "tauri-plugin-mihomo-api";
// 本地存储的键名
const STORAGE_KEY_GROUP = "clash-verge-selected-proxy-group";
@@ -466,7 +466,7 @@ export const CurrentProxyCard = () => {
if (providers.size > 0) {
console.log(`[CurrentProxyCard] 开始测试提供者节点`);
await Promise.allSettled(
[...providers].map((p) => providerHealthCheck(p)),
[...providers].map((p) => healthcheckProxyProvider(p)),
);
}
@@ -478,7 +478,7 @@ export const CurrentProxyCard = () => {
try {
await Promise.race([
delayManager.checkListDelay(proxyNames, groupName, timeout),
getGroupProxyDelays(groupName, url, timeout),
delayGroup(groupName, url, timeout),
]);
console.log(`[CurrentProxyCard] 延迟测试完成,组: ${groupName}`);
} catch (error) {

View File

@@ -92,7 +92,7 @@ export const EnhancedCanvasTrafficGraph = memo(
const { t } = useTranslation();
// 使用增强版全局流量数据管理
const { dataPoints, getDataForTimeRange, isDataFresh, samplerStats } =
const { dataPoints, getDataForTimeRange, samplerStats } =
useTrafficGraphDataEnhanced();
// 基础状态
@@ -865,6 +865,7 @@ export const EnhancedCanvasTrafficGraph = memo(
}}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
onClick={toggleStyle}
/>
{/* 控制层覆盖 */}
@@ -962,8 +963,8 @@ export const EnhancedCanvasTrafficGraph = memo(
lineHeight: 1.2,
}}
>
Points: {displayData.length} | Fresh: {isDataFresh ? "✓" : "✗"} |
Compressed: {samplerStats.compressedBufferSize}
Points: {displayData.length} | Compressed:{" "}
{samplerStats.compressedBufferSize}
</Box>
{/* 悬浮提示框 */}
@@ -988,6 +989,7 @@ export const EnhancedCanvasTrafficGraph = memo(
boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
backdropFilter: "none",
opacity: 1,
whiteSpace: "nowrap",
}}
>
<Box color="text.secondary" mb={0.2}>

View File

@@ -7,7 +7,6 @@ import {
MemoryRounded,
} from "@mui/icons-material";
import {
Box,
Grid,
PaletteColor,
Paper,
@@ -15,16 +14,16 @@ import {
alpha,
useTheme,
} from "@mui/material";
import { ReactNode, memo, useCallback, useMemo, useRef } from "react";
import { useRef, memo, useMemo } from "react";
import { ReactNode } from "react";
import { useTranslation } from "react-i18next";
import useSWR from "swr";
import { TrafficErrorBoundary } from "@/components/common/traffic-error-boundary";
import { useTrafficDataEnhanced } from "@/hooks/use-traffic-monitor";
import { useConnectionData } from "@/hooks/use-connection-data";
import { useMemoryData } from "@/hooks/use-memory-data";
import { useTrafficData } from "@/hooks/use-traffic-data";
import { useVerge } from "@/hooks/use-verge";
import { useVisibility } from "@/hooks/use-visibility";
import { useAppData } from "@/providers/app-data-context";
import { gc, isDebugEnabled } from "@/services/cmds";
import parseTraffic from "@/utils/parse-traffic";
import {
@@ -148,51 +147,33 @@ export const EnhancedTrafficStats = () => {
const trafficRef = useRef<EnhancedCanvasTrafficGraphRef>(null);
const pageVisible = useVisibility();
// 使用AppDataProvider
const { connections } = useAppData();
const {
response: { data: traffic },
} = useTrafficData();
// 使用增强版的统一流量数据Hook
const { traffic, memory, isLoading, isDataFresh, hasValidData } =
useTrafficDataEnhanced();
const {
response: { data: memory },
} = useMemoryData();
const {
response: { data: connections },
} = useConnectionData();
// 是否显示流量图表
const trafficGraph = verge?.traffic_graph ?? true;
// 检查是否支持调试
// TODO: merge this hook with layout-traffic.tsx
const { data: isDebug } = useSWR(
`clash-verge-rev-internal://isDebugEnabled`,
() => isDebugEnabled(),
{
// default value before is fetched
fallbackData: false,
},
);
// Canvas组件现在直接从全局Hook获取数据无需手动添加数据点
// 执行垃圾回收
const handleGarbageCollection = useCallback(async () => {
if (isDebug) {
try {
await gc();
console.log("[Debug] 垃圾回收已执行");
} catch (err) {
console.error("[Debug] 垃圾回收失败:", err);
}
}
}, [isDebug]);
// 使用useMemo计算解析后的流量数据
const parsedData = useMemo(() => {
const [up, upUnit] = parseTraffic(traffic?.raw?.up_rate || 0);
const [down, downUnit] = parseTraffic(traffic?.raw?.down_rate || 0);
const [inuse, inuseUnit] = parseTraffic(memory?.raw?.inuse || 0);
const [up, upUnit] = parseTraffic(traffic?.up || 0);
const [down, downUnit] = parseTraffic(traffic?.down || 0);
const [inuse, inuseUnit] = parseTraffic(memory?.inuse || 0);
const [uploadTotal, uploadTotalUnit] = parseTraffic(
connections.uploadTotal,
connections?.uploadTotal,
);
const [downloadTotal, downloadTotalUnit] = parseTraffic(
connections.downloadTotal,
connections?.downloadTotal,
);
return {
@@ -206,7 +187,7 @@ export const EnhancedTrafficStats = () => {
uploadTotalUnit,
downloadTotal,
downloadTotalUnit,
connectionsCount: connections.count,
connectionsCount: connections?.connections.length,
};
}, [traffic, memory, connections]);
@@ -228,33 +209,10 @@ export const EnhancedTrafficStats = () => {
>
<div style={{ height: "100%", position: "relative" }}>
<EnhancedCanvasTrafficGraph ref={trafficRef} />
{isDebug && (
<div
style={{
position: "absolute",
top: "2px",
left: "2px",
zIndex: 10,
backgroundColor: "rgba(0,0,0,0.5)",
color: "white",
fontSize: "8px",
padding: "2px 4px",
borderRadius: "4px",
}}
>
DEBUG: {trafficRef.current ? "图表已初始化" : "图表未初始化"}
<br />
: {isDataFresh ? "active" : "inactive"}
<br />
: {traffic?.is_fresh ? "Fresh" : "Stale"}
<br />
{new Date().toISOString().slice(11, 19)}
</div>
)}
</div>
</Paper>
);
}, [trafficGraph, pageVisible, theme.palette.divider, isDebug]);
}, [trafficGraph, pageVisible, theme.palette.divider]);
// 使用useMemo计算统计卡片配置
const statCards = useMemo(
@@ -300,10 +258,10 @@ export const EnhancedTrafficStats = () => {
value: parsedData.inuse,
unit: parsedData.inuseUnit,
color: "error" as const,
onClick: isDebug ? handleGarbageCollection : undefined,
onClick: undefined,
},
],
[t, parsedData, isDebug, handleGarbageCollection],
[t, parsedData],
);
return (
@@ -320,28 +278,11 @@ export const EnhancedTrafficStats = () => {
</Grid>
)}
{/* 统计卡片区域 */}
{statCards.map((card, index) => (
<Grid key={index} size={4}>
<CompactStatCard {...card} />
{statCards.map((card, _index) => (
<Grid key={card.title} size={4}>
<CompactStatCard {...(card as StatCardProps)} />
</Grid>
))}
{/* 数据状态指示器(调试用)*/}
{isDebug && (
<Grid size={12}>
<Box
sx={{
p: 1,
bgcolor: "action.hover",
borderRadius: 1,
fontSize: "0.75rem",
}}
>
: {isDataFresh ? "新鲜" : "过期"} | :{" "}
{hasValidData ? "是" : "否"} | : {isLoading ? "是" : "否"}
</Box>
</Grid>
)}
</Grid>
</TrafficErrorBoundary>
);

View File

@@ -6,34 +6,19 @@ import {
import { Box, Typography } from "@mui/material";
import { useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import useSWR from "swr";
import { LightweightTrafficErrorBoundary } from "@/components/common/traffic-error-boundary";
import { useClashInfo } from "@/hooks/use-clash";
import { useTrafficDataEnhanced } from "@/hooks/use-traffic-monitor";
import { useVerge } from "@/hooks/use-verge";
import { useVisibility } from "@/hooks/use-visibility";
import { isDebugEnabled, gc, startTrafficService } from "@/services/cmds";
import parseTraffic from "@/utils/parse-traffic";
import { TrafficGraph, type TrafficRef } from "./traffic-graph";
import { useTrafficData } from "@/hooks/use-traffic-data";
import { useMemoryData } from "@/hooks/use-memory-data";
// setup the traffic
export const LayoutTraffic = () => {
const { data: isDebug } = useSWR(
"clash-verge-rev-internal://isDebugEnabled",
() => isDebugEnabled(),
{
// default value before is fetched
fallbackData: false,
},
);
if (isDebug) {
console.debug("[Traffic][LayoutTraffic] 组件正在渲染");
}
const { t } = useTranslation();
const { clashInfo } = useClashInfo();
const { verge } = useVerge();
// whether hide traffic graph
@@ -42,31 +27,19 @@ export const LayoutTraffic = () => {
const trafficRef = useRef<TrafficRef>(null);
const pageVisible = useVisibility();
// 使用增强版的统一流量数据Hook
const { traffic, memory } = useTrafficDataEnhanced();
// 启动流量服务
useEffect(() => {
console.log(
"[Traffic][LayoutTraffic] useEffect 触发clashInfo:",
clashInfo,
"pageVisible:",
pageVisible,
);
// 简化条件,只要组件挂载就尝试启动服务
console.log("[Traffic][LayoutTraffic] 开始启动流量服务");
startTrafficService().catch((error) => {
console.error("[Traffic][LayoutTraffic] 启动流量服务失败:", error);
});
}, []); // 移除依赖,只在组件挂载时启动一次
const {
response: { data: traffic },
} = useTrafficData();
const {
response: { data: memory },
} = useMemoryData();
// 监听数据变化,为图表添加数据点
useEffect(() => {
if (traffic?.raw && trafficRef.current) {
if (trafficRef.current) {
trafficRef.current.appendData({
up: traffic.raw.up_rate || 0,
down: traffic.raw.down_rate || 0,
up: traffic?.up || 0,
down: traffic?.down || 0,
});
}
}, [traffic]);
@@ -75,9 +48,9 @@ export const LayoutTraffic = () => {
const displayMemory = verge?.enable_memory_usage ?? true;
// 使用parseTraffic统一处理转换保持与首页一致的显示格式
const [up, upUnit] = parseTraffic(traffic?.raw?.up_rate || 0);
const [down, downUnit] = parseTraffic(traffic?.raw?.down_rate || 0);
const [inuse, inuseUnit] = parseTraffic(memory?.raw?.inuse || 0);
const [up, upUnit] = parseTraffic(traffic?.up || 0);
const [down, downUnit] = parseTraffic(traffic?.down || 0);
const [inuse, inuseUnit] = parseTraffic(memory?.inuse || 0);
const boxStyle: any = {
display: "flex",
@@ -114,18 +87,16 @@ export const LayoutTraffic = () => {
<Box display="flex" flexDirection="column" gap={0.75}>
<Box
title={`${t("Upload Speed")} ${traffic?.is_fresh ? "" : "(Stale)"}`}
title={`${t("Upload Speed")}`}
{...boxStyle}
sx={{
...boxStyle.sx,
opacity: traffic?.is_fresh ? 1 : 0.6,
// opacity: traffic?.is_fresh ? 1 : 0.6,
}}
>
<ArrowUpwardRounded
{...iconStyle}
color={
(traffic?.raw?.up_rate || 0) > 0 ? "secondary" : "disabled"
}
color={(traffic?.up || 0) > 0 ? "secondary" : "disabled"}
/>
<Typography {...valStyle} color="secondary">
{up}
@@ -134,18 +105,16 @@ export const LayoutTraffic = () => {
</Box>
<Box
title={`${t("Download Speed")} ${traffic?.is_fresh ? "" : "(Stale)"}`}
title={`${t("Download Speed")}`}
{...boxStyle}
sx={{
...boxStyle.sx,
opacity: traffic?.is_fresh ? 1 : 0.6,
// opacity: traffic?.is_fresh ? 1 : 0.6,
}}
>
<ArrowDownwardRounded
{...iconStyle}
color={
(traffic?.raw?.down_rate || 0) > 0 ? "primary" : "disabled"
}
color={(traffic?.down || 0) > 0 ? "primary" : "disabled"}
/>
<Typography {...valStyle} color="primary">
{down}
@@ -155,15 +124,15 @@ export const LayoutTraffic = () => {
{displayMemory && (
<Box
title={`${t(isDebug ? "Memory Cleanup" : "Memory Usage")} ${memory?.is_fresh ? "" : "(Stale)"} ${"usage_percent" in (memory?.formatted || {}) && memory.formatted.usage_percent ? `(${memory.formatted.usage_percent.toFixed(1)}%)` : ""}`}
title={`${t("Memory Usage")} `}
{...boxStyle}
sx={{
cursor: isDebug ? "pointer" : "auto",
opacity: memory?.is_fresh ? 1 : 0.6,
cursor: "auto",
// opacity: memory?.is_fresh ? 1 : 0.6,
}}
color={isDebug ? "success.main" : "disabled"}
color={"disabled"}
onClick={async () => {
isDebug && (await gc());
// isDebug && (await gc());
}}
>
<MemoryRounded {...iconStyle} />

View File

@@ -20,26 +20,12 @@ import { useLockFn } from "ahooks";
import dayjs from "dayjs";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { updateProxyProvider } from "tauri-plugin-mihomo-api";
import { useAppData } from "@/providers/app-data-context";
import { proxyProviderUpdate } from "@/services/cmds";
import { showNotice } from "@/services/noticeService";
import parseTraffic from "@/utils/parse-traffic";
// 定义代理提供者类型
interface ProxyProviderItem {
name?: string;
proxies: any[];
updatedAt: number;
vehicleType: string;
subscriptionInfo?: {
Upload: number;
Download: number;
Total: number;
Expire: number;
};
}
// 样式化组件 - 类型框
const TypeBox = styled(Box)<{ component?: React.ElementType }>(({ theme }) => ({
display: "inline-block",
@@ -74,7 +60,7 @@ export const ProviderButton = () => {
// 设置更新状态
setUpdating((prev) => ({ ...prev, [name]: true }));
await proxyProviderUpdate(name);
await updateProxyProvider(name);
// 刷新数据
await refreshProxy();
@@ -115,7 +101,7 @@ export const ProviderButton = () => {
// 改为串行逐个更新所有provider
for (const name of allProviders) {
try {
await proxyProviderUpdate(name);
await updateProxyProvider(name);
// 每个更新完成后更新状态
setUpdating((prev) => ({ ...prev, [name]: false }));
} catch (err) {
@@ -177,161 +163,164 @@ export const ProviderButton = () => {
<DialogContent>
<List sx={{ py: 0, minHeight: 250 }}>
{Object.entries(proxyProviders || {}).map(([key, item]) => {
const provider = item as ProxyProviderItem;
const time = dayjs(provider.updatedAt);
const isUpdating = updating[key];
{Object.entries(proxyProviders || {})
.sort()
.map(([key, item]) => {
const provider = item;
const time = dayjs(provider.updatedAt);
const isUpdating = updating[key];
// 订阅信息
const sub = provider.subscriptionInfo;
const hasSubInfo = !!sub;
const upload = sub?.Upload || 0;
const download = sub?.Download || 0;
const total = sub?.Total || 0;
const expire = sub?.Expire || 0;
// 订阅信息
const sub = provider.subscriptionInfo;
const hasSubInfo = !!sub;
const upload = sub?.Upload || 0;
const download = sub?.Download || 0;
const total = sub?.Total || 0;
const expire = sub?.Expire || 0;
// 流量使用进度
const progress =
total > 0
? Math.min(
Math.round(((download + upload) * 100) / total) + 1,
100,
)
: 0;
// 流量使用进度
const progress =
total > 0
? Math.min(
Math.round(((download + upload) * 100) / total) + 1,
100,
)
: 0;
return (
<ListItem
key={key}
sx={[
{
p: 0,
mb: "8px",
borderRadius: 2,
overflow: "hidden",
transition: "all 0.2s",
},
({ palette: { mode, primary } }) => {
const bgcolor = mode === "light" ? "#ffffff" : "#24252f";
const hoverColor =
mode === "light"
? alpha(primary.main, 0.1)
: alpha(primary.main, 0.2);
return (
<ListItem
key={key}
sx={[
{
p: 0,
mb: "8px",
borderRadius: 2,
overflow: "hidden",
transition: "all 0.2s",
},
({ palette: { mode, primary } }) => {
const bgcolor =
mode === "light" ? "#ffffff" : "#24252f";
const hoverColor =
mode === "light"
? alpha(primary.main, 0.1)
: alpha(primary.main, 0.2);
return {
backgroundColor: bgcolor,
"&:hover": {
backgroundColor: hoverColor,
},
};
},
]}
>
<ListItemText
sx={{ px: 2, py: 1 }}
primary={
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Typography
variant="subtitle1"
component="div"
noWrap
title={key}
sx={{ display: "flex", alignItems: "center" }}
>
<span style={{ marginRight: "8px" }}>{key}</span>
<TypeBox component="span">
{provider.proxies.length}
</TypeBox>
<TypeBox component="span">
{provider.vehicleType}
</TypeBox>
</Typography>
<Typography
variant="body2"
color="text.secondary"
noWrap
>
<small>{t("Update At")}: </small>
{time.fromNow()}
</Typography>
</Box>
}
secondary={
<>
{/* 订阅信息 */}
{hasSubInfo && (
<>
<Box
sx={{
mb: 1,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<span title={t("Used / Total") as string}>
{parseTraffic(upload + download)} /{" "}
{parseTraffic(total)}
</span>
<span title={t("Expire Time") as string}>
{parseExpire(expire)}
</span>
</Box>
{/* 进度条 */}
<LinearProgress
variant="determinate"
value={progress}
sx={{
height: 6,
borderRadius: 3,
opacity: total > 0 ? 1 : 0,
}}
/>
</>
)}
</>
}
/>
<Divider orientation="vertical" flexItem />
<Box
sx={{
width: 40,
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
return {
backgroundColor: bgcolor,
"&:hover": {
backgroundColor: hoverColor,
},
};
},
]}
>
<IconButton
size="small"
color="primary"
onClick={() => {
updateProvider(key);
}}
disabled={isUpdating}
<ListItemText
sx={{ px: 2, py: 1 }}
primary={
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Typography
variant="subtitle1"
component="div"
noWrap
title={key}
sx={{ display: "flex", alignItems: "center" }}
>
<span style={{ marginRight: "8px" }}>{key}</span>
<TypeBox component="span">
{provider.proxies.length}
</TypeBox>
<TypeBox component="span">
{provider.vehicleType}
</TypeBox>
</Typography>
<Typography
variant="body2"
color="text.secondary"
noWrap
>
<small>{t("Update At")}: </small>
{time.fromNow()}
</Typography>
</Box>
}
secondary={
<>
{/* 订阅信息 */}
{hasSubInfo && (
<>
<Box
sx={{
mb: 1,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<span title={t("Used / Total") as string}>
{parseTraffic(upload + download)} /{" "}
{parseTraffic(total)}
</span>
<span title={t("Expire Time") as string}>
{parseExpire(expire)}
</span>
</Box>
{/* 进度条 */}
<LinearProgress
variant="determinate"
value={progress}
sx={{
height: 6,
borderRadius: 3,
opacity: total > 0 ? 1 : 0,
}}
/>
</>
)}
</>
}
/>
<Divider orientation="vertical" flexItem />
<Box
sx={{
animation: isUpdating
? "spin 1s linear infinite"
: "none",
"@keyframes spin": {
"0%": { transform: "rotate(0deg)" },
"100%": { transform: "rotate(360deg)" },
},
width: 40,
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
title={t("Update Provider") as string}
>
<RefreshRounded />
</IconButton>
</Box>
</ListItem>
);
})}
<IconButton
size="small"
color="primary"
onClick={() => {
updateProvider(key);
}}
disabled={isUpdating}
sx={{
animation: isUpdating
? "spin 1s linear infinite"
: "none",
"@keyframes spin": {
"0%": { transform: "rotate(0deg)" },
"100%": { transform: "rotate(360deg)" },
},
}}
title={t("Update Provider") as string}
>
<RefreshRounded />
</IconButton>
</Box>
</ListItem>
);
})}
</List>
</DialogContent>

View File

@@ -34,14 +34,13 @@ import {
import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import useSWR from "swr";
import { useAppData } from "@/providers/app-data-context";
import {
closeAllConnections,
getProxies,
updateProxyAndSync,
updateProxyChainConfigInRuntime,
} from "@/services/cmds";
selectNodeForGroup,
} from "tauri-plugin-mihomo-api";
import { useAppData } from "@/providers/app-data-context";
import { calcuProxies, updateProxyChainConfigInRuntime } from "@/services/cmds";
interface ProxyChainItem {
id: string;
@@ -204,7 +203,7 @@ export const ProxyChain = ({
// 获取当前代理信息以检查连接状态
const { data: currentProxies, mutate: mutateProxies } = useSWR(
"getProxies",
getProxies,
calcuProxies,
{
revalidateOnFocus: true,
revalidateIfStale: true,
@@ -367,7 +366,7 @@ export const ProxyChain = ({
const targetGroup = mode === "global" ? "GLOBAL" : selectedGroup;
await updateProxyAndSync(targetGroup || "GLOBAL", lastNode.name);
await selectNodeForGroup(targetGroup || "GLOBAL", lastNode.name);
localStorage.setItem("proxy-chain-group", targetGroup || "GLOBAL");
localStorage.setItem("proxy-chain-exit-node", lastNode.name);

View File

@@ -14,14 +14,13 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso";
import useSWR from "swr";
import { delayGroup, healthcheckProxyProvider } from "tauri-plugin-mihomo-api";
import { useProxySelection } from "@/hooks/use-proxy-selection";
import { useVerge } from "@/hooks/use-verge";
import { useAppData } from "@/providers/app-data-context";
import {
getGroupProxyDelays,
getRuntimeConfig,
providerHealthCheck,
updateProxyChainConfigInRuntime,
} from "@/services/cmds";
import delayManager from "@/services/delay";
@@ -153,15 +152,14 @@ export const ProxyGroups = (props: Props) => {
// 添加和清理滚动事件监听器
useEffect(() => {
const currentScroller = scrollerRef.current;
if (currentScroller) {
currentScroller.addEventListener("scroll", handleScroll, {
passive: true,
});
return () => {
currentScroller.removeEventListener("scroll", handleScroll);
};
}
if (!scrollerRef.current) return;
scrollerRef.current.addEventListener("scroll", handleScroll, {
passive: true,
});
return () => {
scrollerRef.current?.removeEventListener("scroll", handleScroll);
};
}, [handleScroll]);
// 滚动到顶部
@@ -215,6 +213,7 @@ export const ProxyGroups = (props: Props) => {
const currentGroup = getCurrentGroup();
const availableGroups = getAvailableGroups();
// TODO: 频繁点击切换代理节点,导致应用卡死
const handleChangeProxy = useCallback(
(group: IProxyGroupItem, proxy: IProxyItem) => {
if (isChainMode) {
@@ -273,7 +272,7 @@ export const ProxyGroups = (props: Props) => {
if (providers.size) {
console.log(`[ProxyGroups] 发现提供者,数量: ${providers.size}`);
Promise.allSettled(
[...providers].map((p) => providerHealthCheck(p)),
[...providers].map((p) => healthcheckProxyProvider(p)),
).then(() => {
console.log(`[ProxyGroups] 提供者健康检查完成`);
onProxies();
@@ -289,7 +288,7 @@ export const ProxyGroups = (props: Props) => {
try {
await Promise.race([
delayManager.checkListDelay(names, groupName, timeout),
getGroupProxyDelays(groupName, url, timeout).then((result) => {
delayGroup(groupName, url, timeout).then((result) => {
console.log(
`[ProxyGroups] getGroupProxyDelays返回结果数量:`,
Object.keys(result || {}).length,
@@ -518,7 +517,7 @@ export const ProxyGroups = (props: Props) => {
},
}}
>
{availableGroups.map((group: any, index: number) => (
{availableGroups.map((group: any, _index: number) => (
<MenuItem
key={group.name}
onClick={() => handleGroupSelect(group.name)}

View File

@@ -37,12 +37,12 @@ export const ProxyItemMini = (props: Props) => {
return () => {
delayManager.removeListener(proxy.name, group.name);
};
}, [proxy.name, group.name]);
}, [isPreset, proxy.name, group.name]);
useEffect(() => {
if (!proxy) return;
setDelay(delayManager.getDelayFix(proxy, group.name));
}, [proxy]);
}, [proxy, group.name]);
const onDelay = useLockFn(async () => {
setDelay(-2);
@@ -200,7 +200,7 @@ export const ProxyItemMini = (props: Props) => {
</Widget>
)}
{delay > 0 && (
{delay >= 0 && (
// 显示延迟
<Widget
className="the-delay"
@@ -220,7 +220,7 @@ export const ProxyItemMini = (props: Props) => {
{delayManager.formatDelay(delay, timeout)}
</Widget>
)}
{delay !== -2 && delay <= 0 && selected && (
{proxy.type !== "Direct" && delay !== -2 && delay < 0 && selected && (
// 展示已选择的icon
<CheckCircleOutlineRounded
className="the-icon"

View File

@@ -19,19 +19,11 @@ import { useLockFn } from "ahooks";
import dayjs from "dayjs";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { updateRuleProvider } from "tauri-plugin-mihomo-api";
import { useAppData } from "@/providers/app-data-context";
import { ruleProviderUpdate } from "@/services/cmds";
import { showNotice } from "@/services/noticeService";
// 定义规则提供者类型
interface RuleProviderItem {
behavior: string;
ruleCount: number;
updatedAt: number;
vehicleType: string;
}
// 辅助组件 - 类型框
const TypeBox = styled(Box)<{ component?: React.ElementType }>(({ theme }) => ({
display: "inline-block",
@@ -60,7 +52,7 @@ export const ProviderButton = () => {
// 设置更新状态
setUpdating((prev) => ({ ...prev, [name]: true }));
await ruleProviderUpdate(name);
await updateRuleProvider(name);
// 刷新数据
await refreshRules();
@@ -101,7 +93,7 @@ export const ProviderButton = () => {
// 改为串行逐个更新所有provider
for (const name of allProviders) {
try {
await ruleProviderUpdate(name);
await updateRuleProvider(name);
// 每个更新完成后更新状态
setUpdating((prev) => ({ ...prev, [name]: false }));
} catch (err) {
@@ -160,112 +152,117 @@ export const ProviderButton = () => {
<DialogContent>
<List sx={{ py: 0, minHeight: 250 }}>
{Object.entries(ruleProviders || {}).map(([key, item]) => {
const provider = item as RuleProviderItem;
const time = dayjs(provider.updatedAt);
const isUpdating = updating[key];
{Object.entries(ruleProviders || {})
.sort()
.map(([key, item]) => {
const provider = item;
const time = dayjs(provider.updatedAt);
const isUpdating = updating[key];
return (
<ListItem
key={key}
sx={[
{
p: 0,
mb: "8px",
borderRadius: 2,
overflow: "hidden",
transition: "all 0.2s",
},
({ palette: { mode, primary } }) => {
const bgcolor = mode === "light" ? "#ffffff" : "#24252f";
const hoverColor =
mode === "light"
? alpha(primary.main, 0.1)
: alpha(primary.main, 0.2);
return (
<ListItem
key={key}
sx={[
{
p: 0,
mb: "8px",
borderRadius: 2,
overflow: "hidden",
transition: "all 0.2s",
},
({ palette: { mode, primary } }) => {
const bgcolor =
mode === "light" ? "#ffffff" : "#24252f";
const hoverColor =
mode === "light"
? alpha(primary.main, 0.1)
: alpha(primary.main, 0.2);
return {
backgroundColor: bgcolor,
"&:hover": {
backgroundColor: hoverColor,
borderColor: alpha(primary.main, 0.3),
},
};
},
]}
>
<ListItemText
sx={{ px: 2, py: 1 }}
primary={
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Typography
variant="subtitle1"
component="div"
noWrap
title={key}
sx={{ display: "flex", alignItems: "center" }}
>
<span style={{ marginRight: "8px" }}>{key}</span>
<TypeBox component="span">
{provider.ruleCount}
</TypeBox>
</Typography>
<Typography
variant="body2"
color="text.secondary"
noWrap
>
<small>{t("Update At")}: </small>
{time.fromNow()}
</Typography>
</Box>
}
secondary={
<Box sx={{ display: "flex" }}>
<TypeBox component="span">
{provider.vehicleType}
</TypeBox>
<TypeBox component="span">{provider.behavior}</TypeBox>
</Box>
}
/>
<Divider orientation="vertical" flexItem />
<Box
sx={{
width: 40,
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
return {
backgroundColor: bgcolor,
"&:hover": {
backgroundColor: hoverColor,
borderColor: alpha(primary.main, 0.3),
},
};
},
]}
>
<IconButton
size="small"
color="primary"
onClick={() => updateProvider(key)}
disabled={isUpdating}
<ListItemText
sx={{ px: 2, py: 1 }}
primary={
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Typography
variant="subtitle1"
component="div"
noWrap
title={key}
sx={{ display: "flex", alignItems: "center" }}
>
<span style={{ marginRight: "8px" }}>{key}</span>
<TypeBox component="span">
{provider.ruleCount}
</TypeBox>
</Typography>
<Typography
variant="body2"
color="text.secondary"
noWrap
>
<small>{t("Update At")}: </small>
{time.fromNow()}
</Typography>
</Box>
}
secondary={
<Box sx={{ display: "flex" }}>
<TypeBox component="span">
{provider.vehicleType}
</TypeBox>
<TypeBox component="span">
{provider.behavior}
</TypeBox>
</Box>
}
/>
<Divider orientation="vertical" flexItem />
<Box
sx={{
animation: isUpdating
? "spin 1s linear infinite"
: "none",
"@keyframes spin": {
"0%": { transform: "rotate(0deg)" },
"100%": { transform: "rotate(360deg)" },
},
width: 40,
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
title={t("Update Provider") as string}
>
<RefreshRounded />
</IconButton>
</Box>
</ListItem>
);
})}
<IconButton
size="small"
color="primary"
onClick={() => updateProvider(key)}
disabled={isUpdating}
sx={{
animation: isUpdating
? "spin 1s linear infinite"
: "none",
"@keyframes spin": {
"0%": { transform: "rotate(0deg)" },
"100%": { transform: "rotate(360deg)" },
},
}}
title={t("Update Provider") as string}
>
<RefreshRounded />
</IconButton>
</Box>
</ListItem>
);
})}
</List>
</DialogContent>

View File

@@ -16,16 +16,11 @@ import type { Ref } from "react";
import { useImperativeHandle, useState } from "react";
import { useTranslation } from "react-i18next";
import { mutate } from "swr";
import { closeAllConnections, upgradeCore } from "tauri-plugin-mihomo-api";
import { BaseDialog, DialogRef } from "@/components/base";
import { useVerge } from "@/hooks/use-verge";
import {
changeClashCore,
closeAllConnections,
forceRefreshClashConfig,
restartCore,
upgradeCore,
} from "@/services/cmds";
import { changeClashCore, restartCore } from "@/services/cmds";
import { showNotice } from "@/services/noticeService";
const VALID_CORE = [
@@ -66,8 +61,6 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
mutateVerge();
setTimeout(async () => {
// 核心切换后强制刷新配置缓存
await forceRefreshClashConfig();
mutate("getClashConfig");
mutate("getVersion");
setChangingCore(null);

View File

@@ -20,6 +20,7 @@ import {
} from "react";
import { useTranslation } from "react-i18next";
import useSWR, { mutate } from "swr";
import { getBaseConfig } from "tauri-plugin-mihomo-api";
import { BaseDialog, DialogRef, Switch } from "@/components/base";
import { BaseFieldset } from "@/components/base/base-fieldset";
@@ -29,7 +30,6 @@ import { useVerge } from "@/hooks/use-verge";
import { useAppData } from "@/providers/app-data-context";
import {
getAutotemProxy,
getClashConfig,
getNetworkInterfacesInfo,
getSystemHostname,
getSystemProxy,
@@ -123,26 +123,21 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
return "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,localhost,*.local,*.crashlytics.com,<local>";
};
const { data: clashConfig } = useSWR("getClashConfig", getClashConfig, {
const { data: clashConfig } = useSWR("getClashConfig", getBaseConfig, {
revalidateOnFocus: false,
revalidateIfStale: true,
dedupingInterval: 1000,
errorRetryInterval: 5000,
});
const [prevMixedPort, setPrevMixedPort] = useState(
clashConfig?.["mixed-port"],
);
const [prevMixedPort, setPrevMixedPort] = useState(clashConfig?.mixedPort);
useEffect(() => {
if (
clashConfig?.["mixed-port"] &&
clashConfig?.["mixed-port"] !== prevMixedPort
) {
setPrevMixedPort(clashConfig?.["mixed-port"]);
if (clashConfig?.mixedPort && clashConfig.mixedPort !== prevMixedPort) {
setPrevMixedPort(clashConfig.mixedPort);
resetSystemProxy();
}
}, [clashConfig?.["mixed-port"]]);
}, [clashConfig?.mixedPort]);
const resetSystemProxy = async () => {
try {
@@ -180,7 +175,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
if (isPacMode) {
const host = value.proxy_host || "127.0.0.1";
const port = verge?.verge_mixed_port || clashConfig["mixed-port"] || 7897;
const port = verge?.verge_mixed_port || clashConfig.mixedPort || 7897;
return `${host}:${port}`;
} else {
return systemProxyAddress;
@@ -332,7 +327,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
if (pacContent) {
pacContent = pacContent.replace(/%proxy_host%/g, value.proxy_host);
// 将 mixed-port 转换为字符串
const mixedPortStr = (clashConfig?.["mixed-port"] || "").toString();
const mixedPortStr = (clashConfig?.mixedPort || "").toString();
pacContent = pacContent.replace(/%mixed-port%/g, mixedPortStr);
}

View File

@@ -4,14 +4,15 @@ import { invoke } from "@tauri-apps/api/core";
import { useLockFn } from "ahooks";
import { useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { updateGeo } from "tauri-plugin-mihomo-api";
import { DialogRef, Switch } from "@/components/base";
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
import { useClash } from "@/hooks/use-clash";
import { useVerge } from "@/hooks/use-verge";
import { invoke_uwp_tool } from "@/services/cmds";
import { updateGeoData } from "@/services/cmds";
import { showNotice } from "@/services/noticeService";
import { useClashLog } from "@/services/states";
import getSystem from "@/utils/get-system";
import { ClashCoreViewer } from "./mods/clash-core-viewer";
@@ -35,6 +36,7 @@ const SettingClash = ({ onError }: Props) => {
const { clash, version, mutateClash, patchClash } = useClash();
const { verge, patchVerge } = useVerge();
const [, setClashLog] = useClashLog();
const {
ipv6,
@@ -64,7 +66,7 @@ const SettingClash = ({ onError }: Props) => {
};
const onUpdateGeo = async () => {
try {
await updateGeoData();
await updateGeo();
showNotice("success", t("GeoData Updated"));
} catch (err: any) {
showNotice("error", err?.response.data.message || err.toString());
@@ -186,7 +188,10 @@ const SettingClash = ({ onError }: Props) => {
onCatch={onError}
onFormat={(e: any) => e.target.value}
onChange={(e) => onChangeData({ "log-level": e })}
onGuard={(e) => patchClash({ "log-level": e })}
onGuard={(e) => {
setClashLog((pre: any) => ({ ...pre, logLevel: e }));
return patchClash({ "log-level": e });
}}
>
<Select size="small" sx={{ width: 100, "> div": { py: "7.5px" } }}>
<MenuItem value="debug">Debug</MenuItem>

View File

@@ -5,7 +5,7 @@ import { Box, Divider, MenuItem, Menu, styled, alpha } from "@mui/material";
import { convertFileSrc } from "@tauri-apps/api/core";
import { UnlistenFn } from "@tauri-apps/api/event";
import { useLockFn } from "ahooks";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { BaseLoading } from "@/components/base";
@@ -44,11 +44,11 @@ export const TestItem = (props: Props) => {
const [iconCachePath, setIconCachePath] = useState("");
const { addListener } = useListen();
const onDelay = async () => {
const onDelay = useCallback(async () => {
setDelay(-2);
const result = await cmdTestDelay(url);
setDelay(result);
};
}, [url]);
useEffect(() => {
initIconCachePath();