feat: add rustfmt configuration and CI workflow for code formatting

refactor: streamline formatting workflow by removing unused taplo steps and clarifying directory change

refactor: remove unnecessary directory change step in formatting workflow
This commit is contained in:
Tunglies
2025-06-06 21:11:14 +08:00
parent 689042df60
commit 09969d95de
89 changed files with 2630 additions and 2008 deletions

View File

@@ -4,9 +4,9 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu",
"Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
user-select: none;

View File

@@ -1,7 +1,11 @@
import React, { useState, useEffect } from 'react';
import { Snackbar, Alert, IconButton, Box } from '@mui/material';
import { CloseRounded } from '@mui/icons-material';
import { subscribeNotices, hideNotice, NoticeItem } from '@/services/noticeService';
import React, { useState, useEffect } from "react";
import { Snackbar, Alert, IconButton, Box } from "@mui/material";
import { CloseRounded } from "@mui/icons-material";
import {
subscribeNotices,
hideNotice,
NoticeItem,
} from "@/services/noticeService";
export const NoticeManager: React.FC = () => {
const [currentNotices, setCurrentNotices] = useState<NoticeItem[]>([]);
@@ -23,49 +27,49 @@ export const NoticeManager: React.FC = () => {
return (
<Box
sx={{
position: 'fixed',
top: '20px',
right: '20px',
position: "fixed",
top: "20px",
right: "20px",
zIndex: 1500,
display: 'flex',
flexDirection: 'column',
gap: '10px',
maxWidth: '360px',
display: "flex",
flexDirection: "column",
gap: "10px",
maxWidth: "360px",
}}
>
{currentNotices.map((notice) => (
<Snackbar
key={notice.id}
open={true}
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
anchorOrigin={{ vertical: "top", horizontal: "right" }}
sx={{
position: 'relative',
transform: 'none',
top: 'auto',
right: 'auto',
bottom: 'auto',
left: 'auto',
width: '100%',
position: "relative",
transform: "none",
top: "auto",
right: "auto",
bottom: "auto",
left: "auto",
width: "100%",
}}
>
<Alert
severity={notice.type}
variant="filled"
sx={{ width: '100%' }}
action={
<IconButton
size="small"
color="inherit"
onClick={() => handleClose(notice.id)}
>
<CloseRounded fontSize="inherit" />
</IconButton>
}
>
{notice.message}
</Alert>
<Alert
severity={notice.type}
variant="filled"
sx={{ width: "100%" }}
action={
<IconButton
size="small"
color="inherit"
onClick={() => handleClose(notice.id)}
>
<CloseRounded fontSize="inherit" />
</IconButton>
}
>
{notice.message}
</Alert>
</Snackbar>
))}
</Box>
);
};
};

View File

@@ -157,7 +157,7 @@ export const BaseSearchBox = (props: SearchProps) => {
</Tooltip>
</Box>
),
}
},
}}
/>
</Tooltip>

View File

@@ -107,7 +107,14 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
{information.map((each) => (
<div key={each.label}>
<b>{each.label}</b>
<span style={{ wordBreak: "break-all", color: theme.palette.text.primary }}>: {each.value}</span>
<span
style={{
wordBreak: "break-all",
color: theme.palette.text.primary,
}}
>
: {each.value}
</span>
</div>
))}

View File

@@ -25,7 +25,7 @@ export const ClashInfoCard = () => {
// 使用备忘录组件内容,减少重新渲染
const cardContent = useMemo(() => {
if (!clashConfig) return null;
return (
<Stack spacing={1.5}>
<Stack direction="row" justifyContent="space-between">

View File

@@ -24,11 +24,14 @@ export const ClashModeCard = () => {
const currentMode = clashConfig?.mode?.toLowerCase();
// 模式图标映射
const modeIcons = useMemo(() => ({
rule: <MultipleStopRounded fontSize="small" />,
global: <LanguageRounded fontSize="small" />,
direct: <DirectionsRounded fontSize="small" />
}), []);
const modeIcons = useMemo(
() => ({
rule: <MultipleStopRounded fontSize="small" />,
global: <LanguageRounded fontSize="small" />,
direct: <DirectionsRounded fontSize="small" />,
}),
[],
);
// 切换模式的处理函数
const onChangeMode = useLockFn(async (mode: string) => {
@@ -68,18 +71,19 @@ export const ClashModeCard = () => {
"&:active": {
transform: "translateY(1px)",
},
"&::after": mode === currentMode
? {
content: '""',
position: "absolute",
bottom: -16,
left: "50%",
width: 2,
height: 16,
bgcolor: "primary.main",
transform: "translateX(-50%)",
}
: {},
"&::after":
mode === currentMode
? {
content: '""',
position: "absolute",
bottom: -16,
left: "50%",
width: 2,
height: 16,
bgcolor: "primary.main",
transform: "translateX(-50%)",
}
: {},
});
// 描述样式
@@ -143,12 +147,10 @@ export const ClashModeCard = () => {
overflow: "visible",
}}
>
<Typography
variant="caption"
component="div"
sx={descriptionStyles}
>
{t(`${currentMode?.charAt(0).toUpperCase()}${currentMode?.slice(1)} Mode Description`)}
<Typography variant="caption" component="div" sx={descriptionStyles}>
{t(
`${currentMode?.charAt(0).toUpperCase()}${currentMode?.slice(1)} Mode Description`,
)}
</Typography>
</Box>
</Box>

View File

@@ -105,7 +105,7 @@ export const CurrentProxyCard = () => {
// 添加排序类型状态
const [sortType, setSortType] = useState<ProxySortType>(() => {
const savedSortType = localStorage.getItem(STORAGE_KEY_SORT_TYPE);
return savedSortType ? Number(savedSortType) as ProxySortType : 0;
return savedSortType ? (Number(savedSortType) as ProxySortType) : 0;
});
// 定义状态类型
@@ -156,7 +156,8 @@ export const CurrentProxyCard = () => {
primaryKeywords.some((keyword) =>
group.name.toLowerCase().includes(keyword.toLowerCase()),
),
) || proxies.groups.filter((g: { name: string }) => g.name !== "GLOBAL")[0];
) ||
proxies.groups.filter((g: { name: string }) => g.name !== "GLOBAL")[0];
return primaryGroup?.name || "";
};
@@ -200,11 +201,13 @@ export const CurrentProxyCard = () => {
// 只保留 Selector 类型的组用于选择
const filteredGroups = proxies.groups
.filter((g: { name: string; type?: string }) => g.type === "Selector")
.map((g: { name: string; now: string; all: Array<{ name: string }> }) => ({
name: g.name,
now: g.now || "",
all: g.all.map((p: { name: string }) => p.name),
}));
.map(
(g: { name: string; now: string; all: Array<{ name: string }> }) => ({
name: g.name,
now: g.now || "",
all: g.all.map((p: { name: string }) => p.name),
}),
);
let newProxy = "";
let newDisplayProxy = null;
@@ -230,12 +233,12 @@ export const CurrentProxyCard = () => {
if (selectorGroup) {
newGroup = selectorGroup.name;
newProxy = selectorGroup.now || selectorGroup.all[0] || "";
newDisplayProxy = proxies.records?.[newProxy] || null;
newDisplayProxy = proxies.records?.[newProxy] || null;
if (!isGlobalMode && !isDirectMode) {
localStorage.setItem(STORAGE_KEY_GROUP, newGroup);
if (newProxy) {
localStorage.setItem(STORAGE_KEY_PROXY, newProxy);
if (!isGlobalMode && !isDirectMode) {
localStorage.setItem(STORAGE_KEY_GROUP, newGroup);
if (newProxy) {
localStorage.setItem(STORAGE_KEY_PROXY, newProxy);
}
}
}
@@ -280,7 +283,9 @@ export const CurrentProxyCard = () => {
localStorage.setItem(STORAGE_KEY_GROUP, newGroup);
setState((prev) => {
const group = prev.proxyData.groups.find((g: { name: string }) => g.name === newGroup);
const group = prev.proxyData.groups.find(
(g: { name: string }) => g.name === newGroup,
);
if (group) {
return {
...prev,
@@ -368,14 +373,16 @@ export const CurrentProxyCard = () => {
}, [state.displayProxy]);
// 获取当前节点的延迟(增加非空校验)
const currentDelay = currentProxy && state.selection.group
? delayManager.getDelayFix(currentProxy, state.selection.group)
: -1;
const currentDelay =
currentProxy && state.selection.group
? delayManager.getDelayFix(currentProxy, state.selection.group)
: -1;
// 信号图标(增加非空校验)
const signalInfo = currentProxy && state.selection.group
? getSignalIcon(currentDelay)
: { icon: <SignalNone />, text: "未初始化", color: "text.secondary" };
const signalInfo =
currentProxy && state.selection.group
? getSignalIcon(currentDelay)
: { icon: <SignalNone />, text: "未初始化", color: "text.secondary" };
// 自定义渲染选择框中的值
const renderProxyValue = useCallback(
@@ -384,7 +391,7 @@ export const CurrentProxyCard = () => {
const delayValue = delayManager.getDelayFix(
state.proxyData.records[selected],
state.selection.group
state.selection.group,
);
return (
@@ -441,7 +448,7 @@ export const CurrentProxyCard = () => {
return list;
},
[sortType, state.proxyData.records, state.selection.group]
[sortType, state.proxyData.records, state.selection.group],
);
// 计算要显示的代理选项(增加非空校验)
@@ -452,11 +459,11 @@ export const CurrentProxyCard = () => {
if (isGlobalMode && proxies?.global) {
const options = proxies.global.all
.filter((p: any) => {
const name = typeof p === 'string' ? p : p.name;
const name = typeof p === "string" ? p : p.name;
return name !== "DIRECT" && name !== "REJECT";
})
.map((p: any) => ({
name: typeof p === 'string' ? p : p.name
name: typeof p === "string" ? p : p.name,
}));
return sortProxies(options);
@@ -464,7 +471,7 @@ export const CurrentProxyCard = () => {
// 规则模式
const group = state.selection.group
? state.proxyData.groups.find(g => g.name === state.selection.group)
? state.proxyData.groups.find((g) => g.name === state.selection.group)
: null;
if (group) {
@@ -473,7 +480,14 @@ export const CurrentProxyCard = () => {
}
return [];
}, [isDirectMode, isGlobalMode, proxies, state.proxyData, state.selection.group, sortProxies]);
}, [
isDirectMode,
isGlobalMode,
proxies,
state.proxyData,
state.selection.group,
sortProxies,
]);
// 获取排序图标
const getSortIcon = () => {
@@ -660,12 +674,14 @@ export const CurrentProxyCard = () => {
{isDirectMode
? null
: proxyOptions.map((proxy, index) => {
const delayValue = state.proxyData.records[proxy.name] && state.selection.group
? delayManager.getDelayFix(
state.proxyData.records[proxy.name],
state.selection.group,
)
: -1;
const delayValue =
state.proxyData.records[proxy.name] &&
state.selection.group
? delayManager.getDelayFix(
state.proxyData.records[proxy.name],
state.selection.group,
)
: -1;
return (
<MenuItem
key={`${proxy.name}-${index}`}
@@ -706,4 +722,4 @@ export const CurrentProxyCard = () => {
)}
</EnhancedCard>
);
};
};

View File

@@ -38,7 +38,7 @@ export const EnhancedCard = ({
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
display: "block"
display: "block",
};
return (
@@ -62,13 +62,15 @@ export const EnhancedCard = ({
borderColor: "divider",
}}
>
<Box sx={{
display: "flex",
alignItems: "center",
minWidth: 0,
flex: 1,
overflow: "hidden"
}}>
<Box
sx={{
display: "flex",
alignItems: "center",
minWidth: 0,
flex: 1,
overflow: "hidden",
}}
>
<Box
sx={{
display: "flex",
@@ -87,9 +89,9 @@ export const EnhancedCard = ({
</Box>
<Box sx={{ minWidth: 0, flex: 1 }}>
{typeof title === "string" ? (
<Typography
variant="h6"
fontWeight="medium"
<Typography
variant="h6"
fontWeight="medium"
fontSize={18}
sx={titleTruncateStyle}
title={title}
@@ -97,9 +99,7 @@ export const EnhancedCard = ({
{title}
</Typography>
) : (
<Box sx={titleTruncateStyle}>
{title}
</Box>
<Box sx={titleTruncateStyle}>{title}</Box>
)}
</Box>
</Box>

View File

@@ -30,7 +30,7 @@ ChartJS.register(
PointElement,
LineElement,
Tooltip,
Filler
Filler,
);
// 流量数据项接口
@@ -54,8 +54,8 @@ type DataPoint = ITrafficItem & { name: string; timestamp: number };
/**
* 增强型流量图表组件
*/
export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
(props, ref) => {
export const EnhancedTrafficGraph = memo(
forwardRef<EnhancedTrafficGraphRef>((props, ref) => {
const theme = useTheme();
const { t } = useTranslation();
@@ -63,20 +63,20 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
const [timeRange, setTimeRange] = useState<TimeRange>(10);
const [chartStyle, setChartStyle] = useState<"line" | "area">("area");
const [displayData, setDisplayData] = useState<DataPoint[]>([]);
// 数据缓冲区
const dataBufferRef = useRef<DataPoint[]>([]);
// 根据时间范围计算保留的数据点数量
const getMaxPointsByTimeRange = useCallback(
(minutes: TimeRange): number => minutes * 60,
[]
[],
);
// 最大数据点数量
const MAX_BUFFER_SIZE = useMemo(
() => getMaxPointsByTimeRange(10),
[getMaxPointsByTimeRange]
[getMaxPointsByTimeRange],
);
// 颜色配置
@@ -89,23 +89,28 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
text: theme.palette.text.primary,
tooltipBorder: theme.palette.divider,
}),
[theme]
[theme],
);
// 切换时间范围
const handleTimeRangeClick = useCallback((event: React.MouseEvent<SVGTextElement>) => {
event.stopPropagation();
setTimeRange((prevRange) => {
return prevRange === 1 ? 5 : prevRange === 5 ? 10 : 1;
});
}, []);
// 点击图表主体或图例时切换样式
const handleToggleStyleClick = useCallback((event: React.MouseEvent<SVGTextElement | HTMLDivElement>) => {
event.stopPropagation();
setChartStyle((prev) => (prev === "line" ? "area" : "line"));
}, []);
const handleTimeRangeClick = useCallback(
(event: React.MouseEvent<SVGTextElement>) => {
event.stopPropagation();
setTimeRange((prevRange) => {
return prevRange === 1 ? 5 : prevRange === 5 ? 10 : 1;
});
},
[],
);
// 点击图表主体或图例时切换样式
const handleToggleStyleClick = useCallback(
(event: React.MouseEvent<SVGTextElement | HTMLDivElement>) => {
event.stopPropagation();
setChartStyle((prev) => (prev === "line" ? "area" : "line"));
},
[],
);
// 初始化数据缓冲区
useEffect(() => {
@@ -121,7 +126,9 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
let nameValue: string;
try {
if (isNaN(date.getTime())) {
console.warn(`Initial data generation: Invalid date for timestamp ${pointTime}`);
console.warn(
`Initial data generation: Invalid date for timestamp ${pointTime}`,
);
nameValue = "??:??:??";
} else {
nameValue = date.toLocaleTimeString("en-US", {
@@ -132,7 +139,14 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
});
}
} catch (e) {
console.error("Error in toLocaleTimeString during initial data gen:", e, "Date:", date, "Timestamp:", pointTime);
console.error(
"Error in toLocaleTimeString during initial data gen:",
e,
"Date:",
date,
"Timestamp:",
pointTime,
);
nameValue = "Err:Time";
}
@@ -142,55 +156,66 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
timestamp: pointTime,
name: nameValue,
};
}
},
);
dataBufferRef.current = initialBuffer;
// 更新显示数据
const pointsToShow = getMaxPointsByTimeRange(timeRange);
setDisplayData(initialBuffer.slice(-pointsToShow));
}, [MAX_BUFFER_SIZE, getMaxPointsByTimeRange]);
// 添加数据点方法
const appendData = useCallback((data: ITrafficItem) => {
const safeData = {
up: typeof data.up === "number" && !isNaN(data.up) ? data.up : 0,
down: typeof data.down === "number" && !isNaN(data.down) ? data.down : 0,
};
const appendData = useCallback(
(data: ITrafficItem) => {
const safeData = {
up: typeof data.up === "number" && !isNaN(data.up) ? data.up : 0,
down:
typeof data.down === "number" && !isNaN(data.down) ? data.down : 0,
};
const timestamp = data.timestamp || Date.now();
const date = new Date(timestamp);
const timestamp = data.timestamp || Date.now();
const date = new Date(timestamp);
let nameValue: string;
try {
if (isNaN(date.getTime())) {
console.warn(`appendData: Invalid date for timestamp ${timestamp}`);
nameValue = "??:??:??";
} else {
nameValue = date.toLocaleTimeString("en-US", {
hour12: false,
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
let nameValue: string;
try {
if (isNaN(date.getTime())) {
console.warn(`appendData: Invalid date for timestamp ${timestamp}`);
nameValue = "??:??:??";
} else {
nameValue = date.toLocaleTimeString("en-US", {
hour12: false,
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
}
} catch (e) {
console.error(
"Error in toLocaleTimeString in appendData:",
e,
"Date:",
date,
"Timestamp:",
timestamp,
);
nameValue = "Err:Time";
}
} catch (e) {
console.error("Error in toLocaleTimeString in appendData:", e, "Date:", date, "Timestamp:", timestamp);
nameValue = "Err:Time";
}
// 带时间标签的新数据点
const newPoint: DataPoint = {
...safeData,
name: nameValue,
timestamp: timestamp,
};
// 带时间标签的新数据点
const newPoint: DataPoint = {
...safeData,
name: nameValue,
timestamp: timestamp,
};
const newBuffer = [...dataBufferRef.current.slice(1), newPoint];
dataBufferRef.current = newBuffer;
const newBuffer = [...dataBufferRef.current.slice(1), newPoint];
dataBufferRef.current = newBuffer;
const pointsToShow = getMaxPointsByTimeRange(timeRange);
setDisplayData(newBuffer.slice(-pointsToShow));
}, [timeRange, getMaxPointsByTimeRange]);
const pointsToShow = getMaxPointsByTimeRange(timeRange);
setDisplayData(newBuffer.slice(-pointsToShow));
},
[timeRange, getMaxPointsByTimeRange],
);
// 监听时间范围变化
useEffect(() => {
@@ -202,7 +227,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
// 切换图表样式
const toggleStyle = useCallback(() => {
setChartStyle((prev) => prev === "line" ? "area" : "line");
setChartStyle((prev) => (prev === "line" ? "area" : "line"));
}, []);
// 暴露方法给父组件
@@ -212,30 +237,31 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
appendData,
toggleStyle,
}),
[appendData, toggleStyle]
[appendData, toggleStyle],
);
const formatYAxis = useCallback((value: number | string): string => {
if (typeof value !== 'number') return String(value);
if (typeof value !== "number") return String(value);
const [num, unit] = parseTraffic(value);
return `${num}${unit}`;
}, []);
const formatXLabel = useCallback((tickValue: string | number, index: number, ticks: any[]) => {
const dataPoint = displayData[index as number];
if (dataPoint && dataPoint.name) {
const parts = dataPoint.name.split(":");
return `${parts[0]}:${parts[1]}`;
}
if(typeof tickValue === 'string') {
const parts = tickValue.split(":");
if (parts.length >= 2) return `${parts[0]}:${parts[1]}`;
return tickValue;
}
return '';
}, [displayData]);
const formatXLabel = useCallback(
(tickValue: string | number, index: number, ticks: any[]) => {
const dataPoint = displayData[index as number];
if (dataPoint && dataPoint.name) {
const parts = dataPoint.name.split(":");
return `${parts[0]}:${parts[1]}`;
}
if (typeof tickValue === "string") {
const parts = tickValue.split(":");
if (parts.length >= 2) return `${parts[0]}:${parts[1]}`;
return tickValue;
}
return "";
},
[displayData],
);
// 获取当前时间范围文本
const getTimeRangeText = useCallback(() => {
@@ -243,13 +269,13 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
}, [timeRange, t]);
const chartData = useMemo(() => {
const labels = displayData.map(d => d.name);
const labels = displayData.map((d) => d.name);
return {
labels,
datasets: [
{
label: t("Upload"),
data: displayData.map(d => d.up),
data: displayData.map((d) => d.up),
borderColor: colors.up,
backgroundColor: chartStyle === "area" ? colors.up : colors.up,
fill: chartStyle === "area",
@@ -260,7 +286,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
},
{
label: t("Download"),
data: displayData.map(d => d.down),
data: displayData.map((d) => d.down),
borderColor: colors.down,
backgroundColor: chartStyle === "area" ? colors.down : colors.down,
fill: chartStyle === "area",
@@ -268,113 +294,130 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
pointRadius: 0,
pointHoverRadius: 4,
borderWidth: 2,
}
]
},
],
};
}, [displayData, colors.up, colors.down, t, chartStyle]);
const chartOptions = useMemo(() => ({
responsive: true,
maintainAspectRatio: false,
animation: false as false,
scales: {
x: {
display: true,
type: 'category' as const,
labels: displayData.map(d => d.name),
ticks: {
const chartOptions = useMemo(
() => ({
responsive: true,
maintainAspectRatio: false,
animation: false as false,
scales: {
x: {
display: true,
color: colors.text,
font: { size: 10 },
callback: function(this: Scale, tickValue: string | number, index: number, ticks: Tick[]): string | undefined {
let labelToFormat: string | undefined = undefined;
type: "category" as const,
labels: displayData.map((d) => d.name),
ticks: {
display: true,
color: colors.text,
font: { size: 10 },
callback: function (
this: Scale,
tickValue: string | number,
index: number,
ticks: Tick[],
): string | undefined {
let labelToFormat: string | undefined = undefined;
const currentDisplayTick = ticks[index];
if (currentDisplayTick && typeof currentDisplayTick.label === 'string') {
labelToFormat = currentDisplayTick.label;
} else {
const sourceLabels = displayData.map(d => d.name);
if (typeof tickValue === 'number' && tickValue >= 0 && tickValue < sourceLabels.length) {
labelToFormat = sourceLabels[tickValue];
} else if (typeof tickValue === 'string') {
labelToFormat = tickValue;
const currentDisplayTick = ticks[index];
if (
currentDisplayTick &&
typeof currentDisplayTick.label === "string"
) {
labelToFormat = currentDisplayTick.label;
} else {
const sourceLabels = displayData.map((d) => d.name);
if (
typeof tickValue === "number" &&
tickValue >= 0 &&
tickValue < sourceLabels.length
) {
labelToFormat = sourceLabels[tickValue];
} else if (typeof tickValue === "string") {
labelToFormat = tickValue;
}
}
}
if (typeof labelToFormat !== 'string') {
return undefined;
}
if (typeof labelToFormat !== "string") {
return undefined;
}
const parts: string[] = labelToFormat.split(':');
return parts.length >= 2 ? `${parts[0]}:${parts[1]}` : labelToFormat;
const parts: string[] = labelToFormat.split(":");
return parts.length >= 2
? `${parts[0]}:${parts[1]}`
: labelToFormat;
},
autoSkip: true,
maxTicksLimit: Math.max(
5,
Math.floor(displayData.length / (timeRange * 2)),
),
minRotation: 0,
maxRotation: 0,
},
grid: {
display: true,
drawOnChartArea: false,
drawTicks: true,
tickLength: 2,
color: colors.text,
},
autoSkip: true,
maxTicksLimit: Math.max(5, Math.floor(displayData.length / (timeRange * 2))),
minRotation: 0,
maxRotation: 0,
},
grid: {
display: true,
drawOnChartArea: false,
drawTicks: true,
tickLength: 2,
color: colors.text,
y: {
beginAtZero: true,
ticks: {
color: colors.text,
font: { size: 10 },
callback: formatYAxis,
},
grid: {
display: true,
drawTicks: true,
tickLength: 3,
color: colors.grid,
},
},
},
y: {
beginAtZero: true,
ticks: {
color: colors.text,
font: { size: 10 },
callback: formatYAxis,
},
grid: {
display: true,
drawTicks: true,
tickLength: 3,
color: colors.grid,
},
}
},
plugins: {
tooltip: {
enabled: true,
mode: 'index' as const,
intersect: false,
backgroundColor: colors.tooltipBg,
titleColor: colors.text,
bodyColor: colors.text,
borderColor: colors.tooltipBorder,
borderWidth: 1,
cornerRadius: 4,
padding: 8,
callbacks: {
title: (tooltipItems: any[]) => {
return `${t("Time")}: ${tooltipItems[0].label}`;
plugins: {
tooltip: {
enabled: true,
mode: "index" as const,
intersect: false,
backgroundColor: colors.tooltipBg,
titleColor: colors.text,
bodyColor: colors.text,
borderColor: colors.tooltipBorder,
borderWidth: 1,
cornerRadius: 4,
padding: 8,
callbacks: {
title: (tooltipItems: any[]) => {
return `${t("Time")}: ${tooltipItems[0].label}`;
},
label: (context: any): string => {
const label = context.dataset.label || "";
const value = context.parsed.y;
const [num, unit] = parseTraffic(value);
return `${label}: ${num} ${unit}/s`;
},
},
label: (context: any): string => {
const label = context.dataset.label || '';
const value = context.parsed.y;
const [num, unit] = parseTraffic(value);
return `${label}: ${num} ${unit}/s`;
}
}
},
legend: {
display: false,
},
},
legend: {
display: false
}
},
layout: {
padding: {
top: 16,
right: 7,
left: 3,
}
}
}), [colors, t, formatYAxis, timeRange, displayData]);
layout: {
padding: {
top: 16,
right: 7,
left: 3,
},
},
}),
[colors, t, formatYAxis, timeRange, displayData],
);
return (
<Box
@@ -392,8 +435,17 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
{displayData.length > 0 && (
<ChartJsLine data={chartData} options={chartOptions} />
)}
<svg width="100%" height="100%" style={{ position: 'absolute', top: 0, left: 0, pointerEvents: 'none' }}>
<svg
width="100%"
height="100%"
style={{
position: "absolute",
top: 0,
left: 0,
pointerEvents: "none",
}}
>
<text
x="3.5%"
y="10%"
@@ -402,11 +454,11 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
fontSize={11}
fontWeight="bold"
onClick={handleTimeRangeClick}
style={{ cursor: "pointer", pointerEvents: 'all' }}
style={{ cursor: "pointer", pointerEvents: "all" }}
>
{getTimeRangeText()}
</text>
<text
x="99%"
y="10%"
@@ -415,7 +467,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
fontSize={12}
fontWeight="bold"
onClick={handleToggleStyleClick}
style={{ cursor: "pointer", pointerEvents: 'all' }}
style={{ cursor: "pointer", pointerEvents: "all" }}
>
{t("Upload")}
</text>
@@ -428,7 +480,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
fontSize={12}
fontWeight="bold"
onClick={handleToggleStyleClick}
style={{ cursor: "pointer", pointerEvents: 'all' }}
style={{ cursor: "pointer", pointerEvents: "all" }}
>
{t("Download")}
</text>
@@ -436,7 +488,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
</div>
</Box>
);
},
));
}),
);
EnhancedTrafficGraph.displayName = "EnhancedTrafficGraph";

View File

@@ -66,85 +66,90 @@ const CONNECTIONS_UPDATE_INTERVAL = 5000; // 5秒更新一次连接数据
const THROTTLE_TRAFFIC_UPDATE = 500; // 500ms节流流量数据更新
// 统计卡片组件 - 使用memo优化
const CompactStatCard = memo(({
icon,
title,
value,
unit,
color,
onClick,
}: StatCardProps) => {
const theme = useTheme();
const CompactStatCard = memo(
({ icon, title, value, unit, color, onClick }: StatCardProps) => {
const theme = useTheme();
// 获取调色板颜色 - 使用useMemo避免重复计算
const colorValue = useMemo(() => {
const palette = theme.palette;
if (
color in palette &&
palette[color as keyof typeof palette] &&
"main" in (palette[color as keyof typeof palette] as PaletteColor)
) {
return (palette[color as keyof typeof palette] as PaletteColor).main;
}
return palette.primary.main;
}, [theme.palette, color]);
// 获取调色板颜色 - 使用useMemo避免重复计算
const colorValue = useMemo(() => {
const palette = theme.palette;
if (
color in palette &&
palette[color as keyof typeof palette] &&
"main" in (palette[color as keyof typeof palette] as PaletteColor)
) {
return (palette[color as keyof typeof palette] as PaletteColor).main;
}
return palette.primary.main;
}, [theme.palette, color]);
return (
<Paper
elevation={0}
sx={{
display: "flex",
alignItems: "center",
borderRadius: 2,
bgcolor: alpha(colorValue, 0.05),
border: `1px solid ${alpha(colorValue, 0.15)}`,
padding: "8px",
transition: "all 0.2s ease-in-out",
cursor: onClick ? "pointer" : "default",
"&:hover": onClick ? {
bgcolor: alpha(colorValue, 0.1),
border: `1px solid ${alpha(colorValue, 0.3)}`,
boxShadow: `0 4px 8px rgba(0,0,0,0.05)`,
} : {},
}}
onClick={onClick}
>
{/* 图标容器 */}
<Grid
component="div"
return (
<Paper
elevation={0}
sx={{
mr: 1,
ml: "2px",
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 32,
height: 32,
borderRadius: "50%",
bgcolor: alpha(colorValue, 0.1),
color: colorValue,
borderRadius: 2,
bgcolor: alpha(colorValue, 0.05),
border: `1px solid ${alpha(colorValue, 0.15)}`,
padding: "8px",
transition: "all 0.2s ease-in-out",
cursor: onClick ? "pointer" : "default",
"&:hover": onClick
? {
bgcolor: alpha(colorValue, 0.1),
border: `1px solid ${alpha(colorValue, 0.3)}`,
boxShadow: `0 4px 8px rgba(0,0,0,0.05)`,
}
: {},
}}
onClick={onClick}
>
{icon}
</Grid>
{/* 文本内容 */}
<Grid component="div" sx={{ flexGrow: 1, minWidth: 0 }}>
<Typography variant="caption" color="text.secondary" noWrap>
{title}
</Typography>
<Grid component="div" sx={{ display: "flex", alignItems: "baseline" }}>
<Typography variant="body1" fontWeight="bold" noWrap sx={{ mr: 0.5 }}>
{value}
</Typography>
<Typography variant="caption" color="text.secondary">
{unit}
</Typography>
{/* 图标容器 */}
<Grid
component="div"
sx={{
mr: 1,
ml: "2px",
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 32,
height: 32,
borderRadius: "50%",
bgcolor: alpha(colorValue, 0.1),
color: colorValue,
}}
>
{icon}
</Grid>
</Grid>
</Paper>
);
});
{/* 文本内容 */}
<Grid component="div" sx={{ flexGrow: 1, minWidth: 0 }}>
<Typography variant="caption" color="text.secondary" noWrap>
{title}
</Typography>
<Grid
component="div"
sx={{ display: "flex", alignItems: "baseline" }}
>
<Typography
variant="body1"
fontWeight="bold"
noWrap
sx={{ mr: 0.5 }}
>
{value}
</Typography>
<Typography variant="caption" color="text.secondary">
{unit}
</Typography>
</Grid>
</Grid>
</Paper>
);
},
);
// 添加显示名称
CompactStatCard.displayName = "CompactStatCard";
@@ -205,25 +210,25 @@ export const EnhancedTrafficStats = () => {
down: data.down,
timestamp: now,
});
} catch { }
} catch {}
return;
}
lastUpdateRef.current.traffic = now;
const safeUp = isNaN(data.up) ? 0 : data.up;
const safeDown = isNaN(data.down) ? 0 : data.down;
try {
setStats(prev => ({
setStats((prev) => ({
...prev,
traffic: { up: safeUp, down: safeDown }
traffic: { up: safeUp, down: safeDown },
}));
} catch { }
} catch {}
try {
trafficRef.current?.appendData({
up: safeUp,
down: safeDown,
timestamp: now,
});
} catch { }
} catch {}
}
} catch (err) {
console.error("[Traffic] 解析数据错误:", err, event.data);
@@ -235,12 +240,12 @@ export const EnhancedTrafficStats = () => {
try {
const data = JSON.parse(event.data) as MemoryUsage;
if (data && typeof data.inuse === "number") {
setStats(prev => ({
setStats((prev) => ({
...prev,
memory: {
inuse: isNaN(data.inuse) ? 0 : data.inuse,
oslimit: data.oslimit,
}
},
}));
}
} catch (err) {
@@ -257,7 +262,7 @@ export const EnhancedTrafficStats = () => {
// 清理现有连接的函数
const cleanupSockets = () => {
Object.values(socketRefs.current).forEach(socket => {
Object.values(socketRefs.current).forEach((socket) => {
if (socket) {
socket.close();
}
@@ -269,40 +274,78 @@ export const EnhancedTrafficStats = () => {
cleanupSockets();
// 创建新连接
console.log(`[Traffic][${EnhancedTrafficStats.name}] 正在连接: ${server}/traffic`);
socketRefs.current.traffic = createAuthSockette(`${server}/traffic`, secret, {
onmessage: handleTrafficUpdate,
onopen: (event) => {
console.log(`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接已建立`, event);
console.log(
`[Traffic][${EnhancedTrafficStats.name}] 正在连接: ${server}/traffic`,
);
socketRefs.current.traffic = createAuthSockette(
`${server}/traffic`,
secret,
{
onmessage: handleTrafficUpdate,
onopen: (event) => {
console.log(
`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接已建立`,
event,
);
},
onerror: (event) => {
console.error(
`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接错误或达到最大重试次数`,
event,
);
setStats((prev) => ({ ...prev, traffic: { up: 0, down: 0 } }));
},
onclose: (event) => {
console.log(
`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接关闭`,
event.code,
event.reason,
);
if (event.code !== 1000 && event.code !== 1001) {
console.warn(
`[Traffic][${EnhancedTrafficStats.name}] 连接非正常关闭,重置状态`,
);
setStats((prev) => ({ ...prev, traffic: { up: 0, down: 0 } }));
}
},
},
onerror: (event) => {
console.error(`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接错误或达到最大重试次数`, event);
setStats(prev => ({ ...prev, traffic: { up: 0, down: 0 } }));
},
onclose: (event) => {
console.log(`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接关闭`, event.code, event.reason);
if (event.code !== 1000 && event.code !== 1001) {
console.warn(`[Traffic][${EnhancedTrafficStats.name}] 连接非正常关闭,重置状态`);
setStats(prev => ({ ...prev, traffic: { up: 0, down: 0 } }));
}
},
});
);
console.log(`[Memory][${EnhancedTrafficStats.name}] 正在连接: ${server}/memory`);
console.log(
`[Memory][${EnhancedTrafficStats.name}] 正在连接: ${server}/memory`,
);
socketRefs.current.memory = createAuthSockette(`${server}/memory`, secret, {
onmessage: handleMemoryUpdate,
onopen: (event) => {
console.log(`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接已建立`, event);
console.log(
`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接已建立`,
event,
);
},
onerror: (event) => {
console.error(`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接错误或达到最大重试次数`, event);
setStats(prev => ({ ...prev, memory: { inuse: 0, oslimit: undefined } }));
console.error(
`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接错误或达到最大重试次数`,
event,
);
setStats((prev) => ({
...prev,
memory: { inuse: 0, oslimit: undefined },
}));
},
onclose: (event) => {
console.log(`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接关闭`, event.code, event.reason);
console.log(
`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接关闭`,
event.code,
event.reason,
);
if (event.code !== 1000 && event.code !== 1001) {
console.warn(`[Memory][${EnhancedTrafficStats.name}] 连接非正常关闭,重置状态`);
setStats(prev => ({ ...prev, memory: { inuse: 0, oslimit: undefined } }));
console.warn(
`[Memory][${EnhancedTrafficStats.name}] 连接非正常关闭,重置状态`,
);
setStats((prev) => ({
...prev,
memory: { inuse: 0, oslimit: undefined },
}));
}
},
});
@@ -314,11 +357,11 @@ export const EnhancedTrafficStats = () => {
useEffect(() => {
return () => {
try {
Object.values(socketRefs.current).forEach(socket => {
Object.values(socketRefs.current).forEach((socket) => {
if (socket) socket.close();
});
socketRefs.current = { traffic: null, memory: null };
} catch { }
} catch {}
};
}, []);
@@ -339,13 +382,25 @@ export const EnhancedTrafficStats = () => {
const [up, upUnit] = parseTraffic(stats.traffic.up);
const [down, downUnit] = parseTraffic(stats.traffic.down);
const [inuse, inuseUnit] = parseTraffic(stats.memory.inuse);
const [uploadTotal, uploadTotalUnit] = parseTraffic(connections.uploadTotal);
const [downloadTotal, downloadTotalUnit] = parseTraffic(connections.downloadTotal);
const [uploadTotal, uploadTotalUnit] = parseTraffic(
connections.uploadTotal,
);
const [downloadTotal, downloadTotalUnit] = parseTraffic(
connections.downloadTotal,
);
return {
up, upUnit, down, downUnit, inuse, inuseUnit,
uploadTotal, uploadTotalUnit, downloadTotal, downloadTotalUnit,
connectionsCount: connections.count
up,
upUnit,
down,
downUnit,
inuse,
inuseUnit,
uploadTotal,
uploadTotalUnit,
downloadTotal,
downloadTotalUnit,
connectionsCount: connections.count,
};
}, [stats, connections]);
@@ -392,51 +447,54 @@ export const EnhancedTrafficStats = () => {
}, [trafficGraph, pageVisible, theme.palette.divider, isDebug]);
// 使用useMemo计算统计卡片配置
const statCards = useMemo(() => [
{
icon: <ArrowUpwardRounded fontSize="small" />,
title: t("Upload Speed"),
value: parsedData.up,
unit: `${parsedData.upUnit}/s`,
color: "secondary" as const,
},
{
icon: <ArrowDownwardRounded fontSize="small" />,
title: t("Download Speed"),
value: parsedData.down,
unit: `${parsedData.downUnit}/s`,
color: "primary" as const,
},
{
icon: <LinkRounded fontSize="small" />,
title: t("Active Connections"),
value: parsedData.connectionsCount,
unit: "",
color: "success" as const,
},
{
icon: <CloudUploadRounded fontSize="small" />,
title: t("Uploaded"),
value: parsedData.uploadTotal,
unit: parsedData.uploadTotalUnit,
color: "secondary" as const,
},
{
icon: <CloudDownloadRounded fontSize="small" />,
title: t("Downloaded"),
value: parsedData.downloadTotal,
unit: parsedData.downloadTotalUnit,
color: "primary" as const,
},
{
icon: <MemoryRounded fontSize="small" />,
title: t("Memory Usage"),
value: parsedData.inuse,
unit: parsedData.inuseUnit,
color: "error" as const,
onClick: isDebug ? handleGarbageCollection : undefined,
},
], [t, parsedData, isDebug, handleGarbageCollection]);
const statCards = useMemo(
() => [
{
icon: <ArrowUpwardRounded fontSize="small" />,
title: t("Upload Speed"),
value: parsedData.up,
unit: `${parsedData.upUnit}/s`,
color: "secondary" as const,
},
{
icon: <ArrowDownwardRounded fontSize="small" />,
title: t("Download Speed"),
value: parsedData.down,
unit: `${parsedData.downUnit}/s`,
color: "primary" as const,
},
{
icon: <LinkRounded fontSize="small" />,
title: t("Active Connections"),
value: parsedData.connectionsCount,
unit: "",
color: "success" as const,
},
{
icon: <CloudUploadRounded fontSize="small" />,
title: t("Uploaded"),
value: parsedData.uploadTotal,
unit: parsedData.uploadTotalUnit,
color: "secondary" as const,
},
{
icon: <CloudDownloadRounded fontSize="small" />,
title: t("Downloaded"),
value: parsedData.downloadTotal,
unit: parsedData.downloadTotalUnit,
color: "primary" as const,
},
{
icon: <MemoryRounded fontSize="small" />,
title: t("Memory Usage"),
value: parsedData.inuse,
unit: parsedData.inuseUnit,
color: "error" as const,
onClick: isDebug ? handleGarbageCollection : undefined,
},
],
[t, parsedData, isDebug, handleGarbageCollection],
);
return (
<Grid container spacing={1} columns={{ xs: 8, sm: 8, md: 12 }}>

View File

@@ -78,12 +78,16 @@ const truncateStyle = {
maxWidth: "calc(100% - 28px)",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap"
whiteSpace: "nowrap",
};
// 提取独立组件减少主组件复杂度
const ProfileDetails = ({ current, onUpdateProfile, updating }: {
current: ProfileItem;
const ProfileDetails = ({
current,
onUpdateProfile,
updating,
}: {
current: ProfileItem;
onUpdateProfile: () => void;
updating: boolean;
}) => {
@@ -99,7 +103,7 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
if (!current.extra || !current.extra.total) return 1;
return Math.min(
Math.round((usedTraffic * 100) / (current.extra.total + 0.01)) + 1,
100
100,
);
}, [current.extra, usedTraffic]);
@@ -109,19 +113,24 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
{current.url && (
<Stack direction="row" alignItems="center" spacing={1}>
<DnsOutlined fontSize="small" color="action" />
<Typography variant="body2" color="text.secondary" noWrap sx={{ display: "flex", alignItems: "center" }}>
<Typography
variant="body2"
color="text.secondary"
noWrap
sx={{ display: "flex", alignItems: "center" }}
>
<span style={{ flexShrink: 0 }}>{t("From")}: </span>
{current.home ? (
<Link
component="button"
fontWeight="medium"
onClick={() => current.home && openWebUrl(current.home)}
sx={{
sx={{
display: "inline-flex",
alignItems: "center",
minWidth: 0,
maxWidth: "calc(100% - 40px)",
ml: 0.5
ml: 0.5,
}}
title={parseUrl(current.url)}
>
@@ -132,14 +141,19 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
textOverflow: "ellipsis",
whiteSpace: "nowrap",
minWidth: 0,
flex: 1
flex: 1,
}}
>
{parseUrl(current.url)}
</Typography>
<LaunchOutlined
fontSize="inherit"
sx={{ ml: 0.5, fontSize: "0.8rem", opacity: 0.7, flexShrink: 0 }}
sx={{
ml: 0.5,
fontSize: "0.8rem",
opacity: 0.7,
flexShrink: 0,
}}
/>
</Link>
) : (
@@ -152,7 +166,7 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
whiteSpace: "nowrap",
minWidth: 0,
flex: 1,
ml: 0.5
ml: 0.5,
}}
title={parseUrl(current.url)}
>
@@ -195,7 +209,8 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
<Typography variant="body2" color="text.secondary">
{t("Used / Total")}:{" "}
<Box component="span" fontWeight="medium">
{parseTraffic(usedTraffic)} / {parseTraffic(current.extra.total)}
{parseTraffic(usedTraffic)} /{" "}
{parseTraffic(current.extra.total)}
</Box>
</Typography>
</Stack>
@@ -240,7 +255,7 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
// 提取空配置组件
const EmptyProfile = ({ onClick }: { onClick: () => void }) => {
const { t } = useTranslation();
return (
<Box
sx={{
@@ -268,27 +283,30 @@ const EmptyProfile = ({ onClick }: { onClick: () => void }) => {
);
};
export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardProps) => {
export const HomeProfileCard = ({
current,
onProfileUpdated,
}: HomeProfileCardProps) => {
const { t } = useTranslation();
const navigate = useNavigate();
const { refreshAll } = useAppData();
// 更新当前订阅
const [updating, setUpdating] = useState(false);
const onUpdateProfile = useLockFn(async () => {
if (!current?.uid) return;
setUpdating(true);
try {
await updateProfile(current.uid, current.option);
showNotice('success', t("Update subscription successfully"), 1000);
showNotice("success", t("Update subscription successfully"), 1000);
onProfileUpdated?.();
// 刷新首页数据
refreshAll();
} catch (err: any) {
showNotice('error', err.message || err.toString(), 3000);
showNotice("error", err.message || err.toString(), 3000);
} finally {
setUpdating(false);
}
@@ -302,9 +320,9 @@ export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardPr
// 卡片标题
const cardTitle = useMemo(() => {
if (!current) return t("Profiles");
if (!current.home) return current.name;
return (
<Link
component="button"
@@ -323,19 +341,19 @@ export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardPr
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
flex: 1
}
flex: 1,
},
}}
title={current.name}
>
<span>{current.name}</span>
<LaunchOutlined
fontSize="inherit"
sx={{
ml: 0.5,
fontSize: "0.8rem",
sx={{
ml: 0.5,
fontSize: "0.8rem",
opacity: 0.7,
flexShrink: 0
flexShrink: 0,
}}
/>
</Link>
@@ -345,7 +363,7 @@ export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardPr
// 卡片操作按钮
const cardAction = useMemo(() => {
if (!current) return null;
return (
<Button
variant="outlined"
@@ -367,10 +385,10 @@ export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardPr
action={cardAction}
>
{current ? (
<ProfileDetails
current={current}
onUpdateProfile={onUpdateProfile}
updating={updating}
<ProfileDetails
current={current}
onUpdateProfile={onUpdateProfile}
updating={updating}
/>
) : (
<EmptyProfile onClick={goToProfiles} />

View File

@@ -83,28 +83,28 @@ export const IpInfoCard = () => {
// 组件加载时获取IP信息
useEffect(() => {
fetchIpInfo();
// 倒计时实现优化,减少不必要的重渲染
let timer: number | null = null;
let currentCount = IP_REFRESH_SECONDS;
// 只在必要时更新状态,减少重渲染次数
const startCountdown = () => {
timer = window.setInterval(() => {
currentCount -= 1;
if (currentCount <= 0) {
fetchIpInfo();
currentCount = IP_REFRESH_SECONDS;
}
// 每5秒或倒计时结束时才更新UI
if (currentCount % 5 === 0 || currentCount <= 0) {
setCountdown(currentCount);
}
}, 1000);
};
startCountdown();
return () => {
if (timer) clearInterval(timer);
@@ -112,7 +112,7 @@ export const IpInfoCard = () => {
}, [fetchIpInfo]);
const toggleShowIp = useCallback(() => {
setShowIp(prev => !prev);
setShowIp((prev) => !prev);
}, []);
// 渲染加载状态
@@ -282,9 +282,7 @@ export const IpInfoCard = () => {
<InfoItem label={t("ORG")} value={ipInfo?.asn_organization} />
<InfoItem
label={t("Location")}
value={[ipInfo?.city, ipInfo?.region]
.filter(Boolean)
.join(", ")}
value={[ipInfo?.city, ipInfo?.region].filter(Boolean).join(", ")}
/>
<InfoItem label={t("Timezone")} value={ipInfo?.timezone} />
</Box>

View File

@@ -1,19 +1,24 @@
import { useTranslation } from "react-i18next";
import { Typography, Stack, Divider, Chip, IconButton, Tooltip } from "@mui/material";
import {
InfoOutlined,
SettingsOutlined,
WarningOutlined,
import {
Typography,
Stack,
Divider,
Chip,
IconButton,
Tooltip,
} from "@mui/material";
import {
InfoOutlined,
SettingsOutlined,
WarningOutlined,
AdminPanelSettingsOutlined,
DnsOutlined,
ExtensionOutlined
ExtensionOutlined,
} from "@mui/icons-material";
import { useVerge } from "@/hooks/use-verge";
import { EnhancedCard } from "./enhanced-card";
import useSWR from "swr";
import {
getSystemInfo,
} from "@/services/cmds";
import { getSystemInfo } from "@/services/cmds";
import { useNavigate } from "react-router-dom";
import { version as appVersion } from "@root/package.json";
import { useCallback, useEffect, useMemo, useState } from "react";
@@ -30,32 +35,35 @@ export const SystemInfoCard = () => {
const { isAdminMode, isSidecarMode, mutateRunningMode } = useSystemState();
const { installServiceAndRestartCore } = useServiceInstaller();
// 系统信息状态
const [systemState, setSystemState] = useState({
osInfo: "",
lastCheckUpdate: "-",
});
// 系统信息状态
const [systemState, setSystemState] = useState({
osInfo: "",
lastCheckUpdate: "-",
});
// 初始化系统信息
useEffect(() => {
getSystemInfo()
.then((info) => {
const lines = info.split("\n");
if (lines.length > 0) {
const sysName = lines[0].split(": ")[1] || "";
let sysVersion = lines[1].split(": ")[1] || "";
// 初始化系统信息
useEffect(() => {
getSystemInfo()
.then((info) => {
const lines = info.split("\n");
if (lines.length > 0) {
const sysName = lines[0].split(": ")[1] || "";
let sysVersion = lines[1].split(": ")[1] || "";
if (sysName && sysVersion.toLowerCase().startsWith(sysName.toLowerCase())) {
sysVersion = sysVersion.substring(sysName.length).trim();
if (
sysName &&
sysVersion.toLowerCase().startsWith(sysName.toLowerCase())
) {
sysVersion = sysVersion.substring(sysName.length).trim();
}
setSystemState((prev) => ({
...prev,
osInfo: `${sysName} ${sysVersion}`,
}));
}
setSystemState((prev) => ({
...prev,
osInfo: `${sysName} ${sysVersion}`,
}));
}
})
.catch(console.error);
})
.catch(console.error);
// 获取最后检查更新时间
const lastCheck = localStorage.getItem("last_check_update");
@@ -122,7 +130,6 @@ useEffect(() => {
}
}, [verge, patchVerge]);
// 点击运行模式处理,Sidecar或纯管理员模式允许安装服务
const handleRunningModeClick = useCallback(() => {
if (isSidecarMode || (isAdminMode && isSidecarMode)) {
@@ -135,13 +142,13 @@ useEffect(() => {
try {
const info = await checkUpdate();
if (!info?.available) {
showNotice('success', t("Currently on the Latest Version"));
showNotice("success", t("Currently on the Latest Version"));
} else {
showNotice('info', t("Update Available"), 2000);
showNotice("info", t("Update Available"), 2000);
goToSettings();
}
} catch (err: any) {
showNotice('error', err.message || err.toString());
showNotice("error", err.message || err.toString());
}
});
@@ -155,13 +162,15 @@ useEffect(() => {
const runningModeStyle = useMemo(
() => ({
// Sidecar或纯管理员模式允许安装服务
cursor: (isSidecarMode || (isAdminMode && isSidecarMode)) ? "pointer" : "default",
textDecoration: (isSidecarMode || (isAdminMode && isSidecarMode)) ? "underline" : "none",
cursor:
isSidecarMode || (isAdminMode && isSidecarMode) ? "pointer" : "default",
textDecoration:
isSidecarMode || (isAdminMode && isSidecarMode) ? "underline" : "none",
display: "flex",
alignItems: "center",
gap: 0.5,
"&:hover": {
opacity: (isSidecarMode || (isAdminMode && isSidecarMode)) ? 0.7 : 1,
opacity: isSidecarMode || (isAdminMode && isSidecarMode) ? 0.7 : 1,
},
}),
[isSidecarMode, isAdminMode],
@@ -174,34 +183,34 @@ useEffect(() => {
if (!isSidecarMode) {
return (
<>
<AdminPanelSettingsOutlined
sx={{ color: "primary.main", fontSize: 16 }}
<AdminPanelSettingsOutlined
sx={{ color: "primary.main", fontSize: 16 }}
titleAccess={t("Administrator Mode")}
/>
<DnsOutlined
sx={{ color: "success.main", fontSize: 16, ml: 0.5 }}
<DnsOutlined
sx={{ color: "success.main", fontSize: 16, ml: 0.5 }}
titleAccess={t("Service Mode")}
/>
</>
);
}
return (
<AdminPanelSettingsOutlined
sx={{ color: "primary.main", fontSize: 16 }}
<AdminPanelSettingsOutlined
sx={{ color: "primary.main", fontSize: 16 }}
titleAccess={t("Administrator Mode")}
/>
);
} else if (isSidecarMode) {
return (
<ExtensionOutlined
sx={{ color: "info.main", fontSize: 16 }}
<ExtensionOutlined
sx={{ color: "info.main", fontSize: 16 }}
titleAccess={t("Sidecar Mode")}
/>
);
} else {
return (
<DnsOutlined
sx={{ color: "success.main", fontSize: 16 }}
<DnsOutlined
sx={{ color: "success.main", fontSize: 16 }}
titleAccess={t("Service Mode")}
/>
);
@@ -247,13 +256,19 @@ useEffect(() => {
</Typography>
</Stack>
<Divider />
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
>
<Typography variant="body2" color="text.secondary">
{t("Auto Launch")}
</Typography>
<Stack direction="row" spacing={1} alignItems="center">
{isAdminMode && (
<Tooltip title={t("Administrator mode may not support auto launch")}>
<Tooltip
title={t("Administrator mode may not support auto launch")}
>
<WarningOutlined sx={{ color: "warning.main", fontSize: 20 }} />
</Tooltip>
)}
@@ -268,7 +283,11 @@ useEffect(() => {
</Stack>
</Stack>
<Divider />
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
>
<Typography variant="body2" color="text.secondary">
{t("Running Mode")}
</Typography>

View File

@@ -87,12 +87,12 @@ export const TestCard = () => {
}
const newList = testList.map((x) =>
x.uid === uid ? { ...x, ...patch } : x
x.uid === uid ? { ...x, ...patch } : x,
);
mutateVerge({ ...verge, test_list: newList }, false);
},
[testList, verge, mutateVerge]
[testList, verge, mutateVerge],
);
const onDeleteTestListItem = useCallback(
@@ -101,7 +101,7 @@ export const TestCard = () => {
patchVerge({ test_list: newList });
mutateVerge({ ...verge, test_list: newList }, false);
},
[testList, verge, patchVerge, mutateVerge]
[testList, verge, patchVerge, mutateVerge],
);
const onDragEnd = useCallback(
@@ -122,7 +122,7 @@ export const TestCard = () => {
const patchFn = () => {
try {
patchVerge({ test_list: newList });
} catch { }
} catch {}
};
if (window.requestIdleCallback) {
window.requestIdleCallback(patchFn);
@@ -131,7 +131,7 @@ export const TestCard = () => {
}
}
},
[testList, verge, mutateVerge, patchVerge]
[testList, verge, mutateVerge, patchVerge],
);
// 仅在verge首次加载时初始化测试列表
@@ -142,22 +142,25 @@ export const TestCard = () => {
}, [verge, patchVerge]);
// 使用useMemo优化UI内容减少渲染计算
const renderTestItems = useMemo(() => (
<Grid container spacing={1} columns={12}>
<SortableContext items={testList.map((x) => x.uid)}>
{testList.map((item) => (
<Grid key={item.uid} size={3}>
<TestItem
id={item.uid}
itemData={item}
onEdit={() => viewerRef.current?.edit(item)}
onDelete={onDeleteTestListItem}
/>
</Grid>
))}
</SortableContext>
</Grid>
), [testList, onDeleteTestListItem]);
const renderTestItems = useMemo(
() => (
<Grid container spacing={1} columns={12}>
<SortableContext items={testList.map((x) => x.uid)}>
{testList.map((item) => (
<Grid key={item.uid} size={3}>
<TestItem
id={item.uid}
itemData={item}
onEdit={() => viewerRef.current?.edit(item)}
onDelete={onDeleteTestListItem}
/>
</Grid>
))}
</SortableContext>
</Grid>
),
[testList, onDeleteTestListItem],
);
const handleTestAll = useCallback(() => {
emit("verge://test-all");

View File

@@ -24,7 +24,7 @@ export const UpdateButton = (props: Props) => {
errorRetryCount: 2,
revalidateIfStale: false,
focusThrottleInterval: 36e5, // 1 hour
}
},
);
if (!updateInfo?.available) return null;

View File

@@ -1,6 +1,9 @@
import { useEffect, useMemo } from "react";
import { alpha, createTheme, Shadows, Theme as MuiTheme } from "@mui/material";
import { getCurrentWebviewWindow, WebviewWindow } from "@tauri-apps/api/webviewWindow";
import {
getCurrentWebviewWindow,
WebviewWindow,
} from "@tauri-apps/api/webviewWindow";
import { useSetThemeMode, useThemeMode } from "@/services/states";
import { defaultTheme, defaultDarkTheme } from "@/pages/_theme";
import { useVerge } from "@/hooks/use-verge";
@@ -51,13 +54,16 @@ export const useCustomTheme = () => {
const timerId = setTimeout(() => {
if (!isMounted) return;
appWindow.theme().then((systemTheme) => {
if (isMounted && systemTheme) {
setMode(systemTheme);
}
}).catch(err => {
console.error("Failed to get initial system theme:", err);
});
appWindow
.theme()
.then((systemTheme) => {
if (isMounted && systemTheme) {
setMode(systemTheme);
}
})
.catch((err) => {
console.error("Failed to get initial system theme:", err);
});
}, 0);
const unlistenPromise = appWindow.onThemeChanged(({ payload }) => {
@@ -69,13 +75,15 @@ export const useCustomTheme = () => {
return () => {
isMounted = false;
clearTimeout(timerId);
unlistenPromise.then((unlistenFn) => {
if (typeof unlistenFn === 'function') {
unlistenFn();
}
}).catch(err => {
console.error("Failed to unlisten from theme changes:", err);
});
unlistenPromise
.then((unlistenFn) => {
if (typeof unlistenFn === "function") {
unlistenFn();
}
})
.catch((err) => {
console.error("Failed to unlisten from theme changes:", err);
});
};
}, [theme_mode, appWindow, setMode]);
@@ -86,7 +94,10 @@ export const useCustomTheme = () => {
if (theme_mode === "system") {
appWindow.setTheme(null).catch((err) => {
console.error("Failed to set window theme to follow system (setTheme(null)):", err);
console.error(
"Failed to set window theme to follow system (setTheme(null)):",
err,
);
});
} else if (mode) {
appWindow.setTheme(mode as TauriOsTheme).catch((err) => {
@@ -153,21 +164,24 @@ export const useCustomTheme = () => {
const rootEle = document.documentElement;
if (rootEle) {
const backgroundColor = mode === "light" ? "#ECECEC" : "#2e303d";
const selectColor = mode === "light" ? "#f5f5f5" : "#d5d5d5";
const scrollColor = mode === "light" ? "#90939980" : "#3E3E3Eee";
const dividerColor =
const backgroundColor = mode === "light" ? "#ECECEC" : "#2e303d";
const selectColor = mode === "light" ? "#f5f5f5" : "#d5d5d5";
const scrollColor = mode === "light" ? "#90939980" : "#3E3E3Eee";
const dividerColor =
mode === "light" ? "rgba(0, 0, 0, 0.06)" : "rgba(255, 255, 255, 0.06)";
rootEle.style.setProperty("--divider-color", dividerColor);
rootEle.style.setProperty("--background-color", backgroundColor);
rootEle.style.setProperty("--selection-color", selectColor);
rootEle.style.setProperty("--scroller-color", scrollColor);
rootEle.style.setProperty("--primary-main", muiTheme.palette.primary.main);
rootEle.style.setProperty(
rootEle.style.setProperty("--divider-color", dividerColor);
rootEle.style.setProperty("--background-color", backgroundColor);
rootEle.style.setProperty("--selection-color", selectColor);
rootEle.style.setProperty("--scroller-color", scrollColor);
rootEle.style.setProperty(
"--primary-main",
muiTheme.palette.primary.main,
);
rootEle.style.setProperty(
"--background-color-alpha",
alpha(muiTheme.palette.primary.main, 0.1),
);
);
}
// inject css
let styleElement = document.querySelector("style#verge-theme");

View File

@@ -127,7 +127,7 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
currData.current = value;
onChange?.(prevData.current, currData.current);
} catch (err: any) {
showNotice('error', err.message || err.toString());
showNotice("error", err.message || err.toString());
}
});
@@ -136,7 +136,7 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
!readOnly && onSave?.(prevData.current, currData.current);
onClose();
} catch (err: any) {
showNotice('error', err.message || err.toString());
showNotice("error", err.message || err.toString());
}
});
@@ -144,7 +144,7 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
try {
onClose();
} catch (err: any) {
showNotice('error', err.message || err.toString());
showNotice("error", err.message || err.toString());
}
});

View File

@@ -70,8 +70,8 @@ export const GroupItem = (props: Props) => {
? alpha(palette.background.paper, 0.3)
: alpha(palette.grey[400], 0.3)
: type === "delete"
? alpha(palette.error.main, 0.3)
: alpha(palette.success.main, 0.3),
? alpha(palette.error.main, 0.3)
: alpha(palette.success.main, 0.3),
height: "100%",
margin: "8px 0",
borderRadius: "8px",

View File

@@ -90,27 +90,27 @@ export const GroupsEditorViewer = (props: Props) => {
const filteredPrependSeq = useMemo(
() => prependSeq.filter((group) => match(group.name)),
[prependSeq, match]
[prependSeq, match],
);
const filteredGroupList = useMemo(
() => groupList.filter((group) => match(group.name)),
[groupList, match]
[groupList, match],
);
const filteredAppendSeq = useMemo(
() => appendSeq.filter((group) => match(group.name)),
[appendSeq, match]
[appendSeq, match],
);
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
}),
);
const reorder = (
list: IProxyGroupConfig[],
startIndex: number,
endIndex: number
endIndex: number,
) => {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
@@ -188,8 +188,8 @@ export const GroupsEditorViewer = (props: Props) => {
setCurrData(
yaml.dump(
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq },
{ forceQuotes: true }
)
{ forceQuotes: true },
),
);
} catch (e) {
// 防止异常导致UI卡死
@@ -226,7 +226,7 @@ export const GroupsEditorViewer = (props: Props) => {
return !moreDeleteProxies.includes(proxy);
}
}),
moreAppendProxies
moreAppendProxies,
);
setProxyPolicyList(
@@ -236,8 +236,8 @@ export const GroupsEditorViewer = (props: Props) => {
.map((group: IProxyGroupConfig) => group.name)
.filter((name) => !deleteSeq.includes(name)) || [],
appendSeq.map((group: IProxyGroupConfig) => group.name),
proxies.map((proxy: any) => proxy.name)
)
proxies.map((proxy: any) => proxy.name),
),
);
};
const fetchProfile = async () => {
@@ -266,7 +266,7 @@ export const GroupsEditorViewer = (props: Props) => {
{},
originProvider,
moreProvider,
globalProvider
globalProvider,
);
setProxyProviderList(Object.keys(provider));
@@ -297,11 +297,11 @@ export const GroupsEditorViewer = (props: Props) => {
const handleSave = useLockFn(async () => {
try {
await saveProfileFile(property, currData);
showNotice('success', t("Saved Successfully"));
showNotice("success", t("Saved Successfully"));
onSave?.(prevData, currData);
onClose();
} catch (err: any) {
showNotice('error', err.toString());
showNotice("error", err.toString());
}
});
@@ -502,7 +502,7 @@ export const GroupsEditorViewer = (props: Props) => {
{t("seconds")}
</InputAdornment>
),
}
},
}}
/>
</Item>
@@ -530,7 +530,7 @@ export const GroupsEditorViewer = (props: Props) => {
{t("millis")}
</InputAdornment>
),
}
},
}}
/>
</Item>
@@ -742,7 +742,7 @@ export const GroupsEditorViewer = (props: Props) => {
}
setPrependSeq([formIns.getValues(), ...prependSeq]);
} catch (err: any) {
showNotice('error', err.message || err.toString());
showNotice("error", err.message || err.toString());
}
}}
>
@@ -764,7 +764,7 @@ export const GroupsEditorViewer = (props: Props) => {
}
setAppendSeq([...appendSeq, formIns.getValues()]);
} catch (err: any) {
showNotice('error', err.message || err.toString());
showNotice("error", err.message || err.toString());
}
}}
>
@@ -811,8 +811,8 @@ export const GroupsEditorViewer = (props: Props) => {
onDelete={() => {
setPrependSeq(
prependSeq.filter(
(v) => v.name !== item.name
)
(v) => v.name !== item.name,
),
);
}}
/>
@@ -838,8 +838,8 @@ export const GroupsEditorViewer = (props: Props) => {
) {
setDeleteSeq(
deleteSeq.filter(
(v) => v !== filteredGroupList[newIndex].name
)
(v) => v !== filteredGroupList[newIndex].name,
),
);
} else {
setDeleteSeq((prev) => [
@@ -871,8 +871,8 @@ export const GroupsEditorViewer = (props: Props) => {
onDelete={() => {
setAppendSeq(
appendSeq.filter(
(v) => v.name !== item.name
)
(v) => v.name !== item.name,
),
);
}}
/>
@@ -906,8 +906,9 @@ export const GroupsEditorViewer = (props: Props) => {
padding: {
top: 33, // 顶部padding防止遮挡snippets
},
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${getSystem() === "windows" ? ", twemoji mozilla" : ""
}`,
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${
getSystem() === "windows" ? ", twemoji mozilla" : ""
}`,
fontLigatures: false, // 连字符
smoothScrolling: true, // 平滑滚动
}}

View File

@@ -1,57 +1,58 @@
import { alpha, Box, styled } from "@mui/material";
export const ProfileBox = styled(Box)(
({ theme, "aria-selected": selected }) => {
const { mode, primary, text } = theme.palette;
const key = `${mode}-${!!selected}`;
export const ProfileBox = styled(Box)(({
theme,
"aria-selected": selected,
}) => {
const { mode, primary, text } = theme.palette;
const key = `${mode}-${!!selected}`;
const backgroundColor = mode === "light" ? "#ffffff" : "#282A36";
const backgroundColor = mode === "light" ? "#ffffff" : "#282A36";
const color = {
"light-true": text.secondary,
"light-false": text.secondary,
"dark-true": alpha(text.secondary, 0.65),
"dark-false": alpha(text.secondary, 0.65),
}[key]!;
const color = {
"light-true": text.secondary,
"light-false": text.secondary,
"dark-true": alpha(text.secondary, 0.65),
"dark-false": alpha(text.secondary, 0.65),
}[key]!;
const h2color = {
"light-true": primary.main,
"light-false": text.primary,
"dark-true": primary.main,
"dark-false": text.primary,
}[key]!;
const h2color = {
"light-true": primary.main,
"light-false": text.primary,
"dark-true": primary.main,
"dark-false": text.primary,
}[key]!;
const borderSelect = {
"light-true": {
borderLeft: `3px solid ${primary.main}`,
width: `calc(100% + 3px)`,
marginLeft: `-3px`,
},
"light-false": {
width: "100%",
},
"dark-true": {
borderLeft: `3px solid ${primary.main}`,
width: `calc(100% + 3px)`,
marginLeft: `-3px`,
},
"dark-false": {
width: "100%",
},
}[key];
const borderSelect = {
"light-true": {
borderLeft: `3px solid ${primary.main}`,
width: `calc(100% + 3px)`,
marginLeft: `-3px`,
},
"light-false": {
width: "100%",
},
"dark-true": {
borderLeft: `3px solid ${primary.main}`,
width: `calc(100% + 3px)`,
marginLeft: `-3px`,
},
"dark-false": {
width: "100%",
},
}[key];
return {
position: "relative",
display: "block",
cursor: "pointer",
textAlign: "left",
padding: "8px 16px",
boxSizing: "border-box",
backgroundColor,
...borderSelect,
borderRadius: "8px",
color,
"& h2": { color: h2color },
};
}
);
return {
position: "relative",
display: "block",
cursor: "pointer",
textAlign: "left",
padding: "8px 16px",
boxSizing: "border-box",
backgroundColor,
...borderSelect,
borderRadius: "8px",
color,
"& h2": { color: h2color },
};
});

View File

@@ -75,7 +75,10 @@ export const ProfileItem = (props: Props) => {
// 获取下次更新时间的函数
const fetchNextUpdateTime = useLockFn(async (forceRefresh = false) => {
if (itemData.option?.update_interval && itemData.option.update_interval > 0) {
if (
itemData.option?.update_interval &&
itemData.option.update_interval > 0
) {
try {
console.log(`尝试获取配置 ${itemData.uid} 的下次更新时间`);
@@ -97,7 +100,7 @@ export const ProfileItem = (props: Props) => {
setNextUpdateTime(t("Last Update failed"));
} else {
// 否则显示剩余时间
const diffMinutes = nextUpdateDate.diff(now, 'minute');
const diffMinutes = nextUpdateDate.diff(now, "minute");
if (diffMinutes < 60) {
if (diffMinutes <= 0) {
@@ -159,11 +162,17 @@ export const ProfileItem = (props: Props) => {
};
// 只注册定时器更新事件监听
window.addEventListener('verge://timer-updated', handleTimerUpdate as EventListener);
window.addEventListener(
"verge://timer-updated",
handleTimerUpdate as EventListener,
);
return () => {
// 清理事件监听
window.removeEventListener('verge://timer-updated', handleTimerUpdate as EventListener);
window.removeEventListener(
"verge://timer-updated",
handleTimerUpdate as EventListener,
);
};
}, [showNextUpdate, itemData.uid]);
@@ -271,7 +280,7 @@ export const ProfileItem = (props: Props) => {
try {
await viewProfile(itemData.uid);
} catch (err: any) {
showNotice('error', err?.message || err.toString());
showNotice("error", err?.message || err.toString());
}
});
@@ -302,7 +311,7 @@ export const ProfileItem = (props: Props) => {
await updateProfile(itemData.uid, option);
// 更新成功,刷新列表
showNotice('success', t("Update subscription successfully"));
showNotice("success", t("Update subscription successfully"));
mutate("getProfiles");
} catch (err: any) {
// 更新完全失败(包括后端的回退尝试)
@@ -421,13 +430,25 @@ export const ProfileItem = (props: Props) => {
};
// 注册事件监听
window.addEventListener('profile-update-started', handleUpdateStarted as EventListener);
window.addEventListener('profile-update-completed', handleUpdateCompleted as EventListener);
window.addEventListener(
"profile-update-started",
handleUpdateStarted as EventListener,
);
window.addEventListener(
"profile-update-completed",
handleUpdateCompleted as EventListener,
);
return () => {
// 清理事件监听
window.removeEventListener('profile-update-started', handleUpdateStarted as EventListener);
window.removeEventListener('profile-update-completed', handleUpdateCompleted as EventListener);
window.removeEventListener(
"profile-update-started",
handleUpdateStarted as EventListener,
);
window.removeEventListener(
"profile-update-completed",
handleUpdateCompleted as EventListener,
);
};
}, [itemData.uid, showNextUpdate]);
@@ -541,13 +562,23 @@ export const ProfileItem = (props: Props) => {
)
)}
{hasUrl && (
<Box sx={{ display: "flex", justifyContent: "flex-end", ml: "auto" }}>
<Box
sx={{
display: "flex",
justifyContent: "flex-end",
ml: "auto",
}}
>
<Typography
noWrap
component="span"
fontSize={14}
textAlign="right"
title={showNextUpdate ? t("Click to show last update time") : `${t("Update Time")}: ${parseExpire(updated)}\n${t("Click to show next update")}`}
title={
showNextUpdate
? t("Click to show last update time")
: `${t("Update Time")}: ${parseExpire(updated)}\n${t("Click to show next update")}`
}
sx={{
cursor: "pointer",
display: "inline-block",
@@ -556,13 +587,15 @@ export const ProfileItem = (props: Props) => {
"&:hover": {
borderBottomColor: "primary.main",
color: "primary.main",
}
},
}}
onClick={toggleUpdateTimeDisplay}
>
{showNextUpdate
? nextUpdateTime
: (updated > 0 ? dayjs(updated * 1000).fromNow() : "")}
: updated > 0
? dayjs(updated * 1000).fromNow()
: ""}
</Typography>
</Box>
)}

View File

@@ -43,7 +43,7 @@ export const ProfileMore = (props: Props) => {
try {
await viewProfile(id);
} catch (err: any) {
showNotice('error', err?.message || err.toString());
showNotice("error", err?.message || err.toString());
}
});

View File

@@ -201,7 +201,7 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
setOpen(false);
fileDataRef.current = null;
setTimeout(() => formIns.reset(), 500);
} catch { }
} catch {}
};
const text = {

View File

@@ -66,27 +66,27 @@ export const ProxiesEditorViewer = (props: Props) => {
const filteredPrependSeq = useMemo(
() => prependSeq.filter((proxy) => match(proxy.name)),
[prependSeq, match]
[prependSeq, match],
);
const filteredProxyList = useMemo(
() => proxyList.filter((proxy) => match(proxy.name)),
[proxyList, match]
[proxyList, match],
);
const filteredAppendSeq = useMemo(
() => appendSeq.filter((proxy) => match(proxy.name)),
[appendSeq, match]
[appendSeq, match],
);
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
}),
);
const reorder = (
list: IProxyConfig[],
startIndex: number,
endIndex: number
endIndex: number,
) => {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
@@ -208,8 +208,8 @@ export const ProxiesEditorViewer = (props: Props) => {
setCurrData(
yaml.dump(
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq },
{ forceQuotes: true }
)
{ forceQuotes: true },
),
);
} catch (e) {
// 防止异常导致UI卡死
@@ -232,11 +232,11 @@ export const ProxiesEditorViewer = (props: Props) => {
const handleSave = useLockFn(async () => {
try {
await saveProfileFile(property, currData);
showNotice('success', t("Saved Successfully"));
showNotice("success", t("Saved Successfully"));
onSave?.(prevData, currData);
onClose();
} catch (err: any) {
showNotice('error', err.toString());
showNotice("error", err.toString());
}
});
@@ -358,8 +358,8 @@ export const ProxiesEditorViewer = (props: Props) => {
onDelete={() => {
setPrependSeq(
prependSeq.filter(
(v) => v.name !== item.name
)
(v) => v.name !== item.name,
),
);
}}
/>
@@ -385,8 +385,8 @@ export const ProxiesEditorViewer = (props: Props) => {
) {
setDeleteSeq(
deleteSeq.filter(
(v) => v !== filteredProxyList[newIndex].name
)
(v) => v !== filteredProxyList[newIndex].name,
),
);
} else {
setDeleteSeq((prev) => [
@@ -418,8 +418,8 @@ export const ProxiesEditorViewer = (props: Props) => {
onDelete={() => {
setAppendSeq(
appendSeq.filter(
(v) => v.name !== item.name
)
(v) => v.name !== item.name,
),
);
}}
/>
@@ -453,8 +453,9 @@ export const ProxiesEditorViewer = (props: Props) => {
padding: {
top: 33, // 顶部padding防止遮挡snippets
},
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${getSystem() === "windows" ? ", twemoji mozilla" : ""
}`,
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${
getSystem() === "windows" ? ", twemoji mozilla" : ""
}`,
fontLigatures: false, // 连字符
smoothScrolling: true, // 平滑滚动
}}

View File

@@ -49,8 +49,8 @@ export const ProxyItem = (props: Props) => {
? alpha(palette.background.paper, 0.3)
: alpha(palette.grey[400], 0.3)
: type === "delete"
? alpha(palette.error.main, 0.3)
: alpha(palette.success.main, 0.3),
? alpha(palette.error.main, 0.3)
: alpha(palette.success.main, 0.3),
height: "100%",
margin: "8px 0",
borderRadius: "8px",

View File

@@ -52,8 +52,8 @@ export const RuleItem = (props: Props) => {
? alpha(palette.background.paper, 0.3)
: alpha(palette.grey[400], 0.3)
: type === "delete"
? alpha(palette.error.main, 0.3)
: alpha(palette.success.main, 0.3),
? alpha(palette.error.main, 0.3)
: alpha(palette.success.main, 0.3),
height: "100%",
margin: "8px 0",
borderRadius: "8px",

View File

@@ -55,17 +55,17 @@ interface Props {
const portValidator = (value: string): boolean => {
return new RegExp(
"^(?:[1-9]\\d{0,3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])$"
"^(?:[1-9]\\d{0,3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])$",
).test(value);
};
const ipv4CIDRValidator = (value: string): boolean => {
return new RegExp(
"^(?:(?:[1-9]?[0-9]|1[0-9][0-9]|2(?:[0-4][0-9]|5[0-5]))\\.){3}(?:[1-9]?[0-9]|1[0-9][0-9]|2(?:[0-4][0-9]|5[0-5]))(?:\\/(?:[12]?[0-9]|3[0-2]))$"
"^(?:(?:[1-9]?[0-9]|1[0-9][0-9]|2(?:[0-4][0-9]|5[0-5]))\\.){3}(?:[1-9]?[0-9]|1[0-9][0-9]|2(?:[0-4][0-9]|5[0-5]))(?:\\/(?:[12]?[0-9]|3[0-2]))$",
).test(value);
};
const ipv6CIDRValidator = (value: string): boolean => {
return new RegExp(
"^([0-9a-fA-F]{1,4}(?::[0-9a-fA-F]{1,4}){7}|::|:(?::[0-9a-fA-F]{1,4}){1,6}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,5}|(?:[0-9a-fA-F]{1,4}:){2}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){3}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){4}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){5}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,6}:)\\/(?:12[0-8]|1[01][0-9]|[1-9]?[0-9])$"
"^([0-9a-fA-F]{1,4}(?::[0-9a-fA-F]{1,4}){7}|::|:(?::[0-9a-fA-F]{1,4}){1,6}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,5}|(?:[0-9a-fA-F]{1,4}:){2}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){3}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){4}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){5}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,6}:)\\/(?:12[0-8]|1[01][0-9]|[1-9]?[0-9])$",
).test(value);
};
@@ -76,161 +76,161 @@ const rules: {
noResolve?: boolean;
validator?: (value: string) => boolean;
}[] = [
{
name: "DOMAIN",
example: "example.com",
},
{
name: "DOMAIN-SUFFIX",
example: "example.com",
},
{
name: "DOMAIN-KEYWORD",
example: "example",
},
{
name: "DOMAIN-REGEX",
example: "example.*",
},
{
name: "GEOSITE",
example: "youtube",
},
{
name: "GEOIP",
example: "CN",
noResolve: true,
},
{
name: "SRC-GEOIP",
example: "CN",
},
{
name: "IP-ASN",
example: "13335",
noResolve: true,
validator: (value) => (+value ? true : false),
},
{
name: "SRC-IP-ASN",
example: "9808",
validator: (value) => (+value ? true : false),
},
{
name: "IP-CIDR",
example: "127.0.0.0/8",
noResolve: true,
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
},
{
name: "IP-CIDR6",
example: "2620:0:2d0:200::7/32",
noResolve: true,
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
},
{
name: "SRC-IP-CIDR",
example: "192.168.1.201/32",
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
},
{
name: "IP-SUFFIX",
example: "8.8.8.8/24",
noResolve: true,
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
},
{
name: "SRC-IP-SUFFIX",
example: "192.168.1.201/8",
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
},
{
name: "SRC-PORT",
example: "7777",
validator: (value) => portValidator(value),
},
{
name: "DST-PORT",
example: "80",
validator: (value) => portValidator(value),
},
{
name: "IN-PORT",
example: "7890",
validator: (value) => portValidator(value),
},
{
name: "DSCP",
example: "4",
},
{
name: "PROCESS-NAME",
example: getSystem() === "windows" ? "chrome.exe" : "curl",
},
{
name: "PROCESS-PATH",
example:
getSystem() === "windows"
? "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
: "/usr/bin/wget",
},
{
name: "PROCESS-NAME-REGEX",
example: ".*telegram.*",
},
{
name: "PROCESS-PATH-REGEX",
example:
getSystem() === "windows" ? "(?i).*Application\\chrome.*" : ".*bin/wget",
},
{
name: "NETWORK",
example: "udp",
validator: (value) => ["tcp", "udp"].includes(value),
},
{
name: "UID",
example: "1001",
validator: (value) => (+value ? true : false),
},
{
name: "IN-TYPE",
example: "SOCKS/HTTP",
},
{
name: "IN-USER",
example: "mihomo",
},
{
name: "IN-NAME",
example: "ss",
},
{
name: "SUB-RULE",
example: "(NETWORK,tcp)",
},
{
name: "RULE-SET",
example: "providername",
noResolve: true,
},
{
name: "AND",
example: "((DOMAIN,baidu.com),(NETWORK,UDP))",
},
{
name: "OR",
example: "((NETWORK,UDP),(DOMAIN,baidu.com))",
},
{
name: "NOT",
example: "((DOMAIN,baidu.com))",
},
{
name: "MATCH",
required: false,
},
];
{
name: "DOMAIN",
example: "example.com",
},
{
name: "DOMAIN-SUFFIX",
example: "example.com",
},
{
name: "DOMAIN-KEYWORD",
example: "example",
},
{
name: "DOMAIN-REGEX",
example: "example.*",
},
{
name: "GEOSITE",
example: "youtube",
},
{
name: "GEOIP",
example: "CN",
noResolve: true,
},
{
name: "SRC-GEOIP",
example: "CN",
},
{
name: "IP-ASN",
example: "13335",
noResolve: true,
validator: (value) => (+value ? true : false),
},
{
name: "SRC-IP-ASN",
example: "9808",
validator: (value) => (+value ? true : false),
},
{
name: "IP-CIDR",
example: "127.0.0.0/8",
noResolve: true,
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
},
{
name: "IP-CIDR6",
example: "2620:0:2d0:200::7/32",
noResolve: true,
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
},
{
name: "SRC-IP-CIDR",
example: "192.168.1.201/32",
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
},
{
name: "IP-SUFFIX",
example: "8.8.8.8/24",
noResolve: true,
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
},
{
name: "SRC-IP-SUFFIX",
example: "192.168.1.201/8",
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
},
{
name: "SRC-PORT",
example: "7777",
validator: (value) => portValidator(value),
},
{
name: "DST-PORT",
example: "80",
validator: (value) => portValidator(value),
},
{
name: "IN-PORT",
example: "7890",
validator: (value) => portValidator(value),
},
{
name: "DSCP",
example: "4",
},
{
name: "PROCESS-NAME",
example: getSystem() === "windows" ? "chrome.exe" : "curl",
},
{
name: "PROCESS-PATH",
example:
getSystem() === "windows"
? "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
: "/usr/bin/wget",
},
{
name: "PROCESS-NAME-REGEX",
example: ".*telegram.*",
},
{
name: "PROCESS-PATH-REGEX",
example:
getSystem() === "windows" ? "(?i).*Application\\chrome.*" : ".*bin/wget",
},
{
name: "NETWORK",
example: "udp",
validator: (value) => ["tcp", "udp"].includes(value),
},
{
name: "UID",
example: "1001",
validator: (value) => (+value ? true : false),
},
{
name: "IN-TYPE",
example: "SOCKS/HTTP",
},
{
name: "IN-USER",
example: "mihomo",
},
{
name: "IN-NAME",
example: "ss",
},
{
name: "SUB-RULE",
example: "(NETWORK,tcp)",
},
{
name: "RULE-SET",
example: "providername",
noResolve: true,
},
{
name: "AND",
example: "((DOMAIN,baidu.com),(NETWORK,UDP))",
},
{
name: "OR",
example: "((NETWORK,UDP),(DOMAIN,baidu.com))",
},
{
name: "NOT",
example: "((DOMAIN,baidu.com))",
},
{
name: "MATCH",
required: false,
},
];
const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"];
@@ -260,22 +260,22 @@ export const RulesEditorViewer = (props: Props) => {
const filteredPrependSeq = useMemo(
() => prependSeq.filter((rule) => match(rule)),
[prependSeq, match]
[prependSeq, match],
);
const filteredRuleList = useMemo(
() => ruleList.filter((rule) => match(rule)),
[ruleList, match]
[ruleList, match],
);
const filteredAppendSeq = useMemo(
() => appendSeq.filter((rule) => match(rule)),
[appendSeq, match]
[appendSeq, match],
);
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
}),
);
const reorder = (list: string[], startIndex: number, endIndex: number) => {
const result = Array.from(list);
@@ -333,11 +333,11 @@ export const RulesEditorViewer = (props: Props) => {
setCurrData(
yaml.dump(
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq },
{ forceQuotes: true }
)
{ forceQuotes: true },
),
);
} catch (e: any) {
showNotice('error', e?.message || e?.toString() || 'YAML dump error');
showNotice("error", e?.message || e?.toString() || "YAML dump error");
}
};
if (window.requestIdleCallback) {
@@ -371,7 +371,7 @@ export const RulesEditorViewer = (props: Props) => {
return !moreDeleteGroups.includes(group);
}
}),
moreAppendGroups
moreAppendGroups,
);
let originRuleSetObj = yaml.load(data) as { "rule-providers": {} } | null;
@@ -396,7 +396,7 @@ export const RulesEditorViewer = (props: Props) => {
let globalSubRule = globalSubRuleObj?.["sub-rules"] || {};
let subRule = Object.assign({}, originSubRule, moreSubRule, globalSubRule);
setProxyPolicyList(
builtinProxyPolicies.concat(groups.map((group: any) => group.name))
builtinProxyPolicies.concat(groups.map((group: any) => group.name)),
);
setRuleSetList(Object.keys(ruleSet));
setSubRuleList(Object.keys(subRule));
@@ -417,19 +417,20 @@ export const RulesEditorViewer = (props: Props) => {
throw new Error(t("Invalid Rule"));
}
const condition = ruleType.required ?? true ? ruleContent : "";
return `${ruleType.name}${condition ? "," + condition : ""},${proxyPolicy}${ruleType.noResolve && noResolve ? ",no-resolve" : ""
}`;
const condition = (ruleType.required ?? true) ? ruleContent : "";
return `${ruleType.name}${condition ? "," + condition : ""},${proxyPolicy}${
ruleType.noResolve && noResolve ? ",no-resolve" : ""
}`;
};
const handleSave = useLockFn(async () => {
try {
await saveProfileFile(property, currData);
showNotice('success', t("Saved Successfully"));
showNotice("success", t("Saved Successfully"));
onSave?.(prevData, currData);
onClose();
} catch (err: any) {
showNotice('error', err.toString());
showNotice("error", err.toString());
}
});
@@ -557,7 +558,7 @@ export const RulesEditorViewer = (props: Props) => {
if (prependSeq.includes(raw)) return;
setPrependSeq([raw, ...prependSeq]);
} catch (err: any) {
showNotice('error', err.message || err.toString());
showNotice("error", err.message || err.toString());
}
}}
>
@@ -575,7 +576,7 @@ export const RulesEditorViewer = (props: Props) => {
if (appendSeq.includes(raw)) return;
setAppendSeq([...appendSeq, raw]);
} catch (err: any) {
showNotice('error', err.message || err.toString());
showNotice("error", err.message || err.toString());
}
}}
>
@@ -621,7 +622,7 @@ export const RulesEditorViewer = (props: Props) => {
ruleRaw={item}
onDelete={() => {
setPrependSeq(
prependSeq.filter((v) => v !== item)
prependSeq.filter((v) => v !== item),
);
}}
/>
@@ -645,8 +646,8 @@ export const RulesEditorViewer = (props: Props) => {
if (deleteSeq.includes(filteredRuleList[newIndex])) {
setDeleteSeq(
deleteSeq.filter(
(v) => v !== filteredRuleList[newIndex]
)
(v) => v !== filteredRuleList[newIndex],
),
);
} else {
setDeleteSeq((prev) => [
@@ -677,7 +678,7 @@ export const RulesEditorViewer = (props: Props) => {
ruleRaw={item}
onDelete={() => {
setAppendSeq(
appendSeq.filter((v) => v !== item)
appendSeq.filter((v) => v !== item),
);
}}
/>
@@ -711,8 +712,9 @@ export const RulesEditorViewer = (props: Props) => {
padding: {
top: 33, // 顶部padding防止遮挡snippets
},
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${getSystem() === "windows" ? ", twemoji mozilla" : ""
}`,
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${
getSystem() === "windows" ? ", twemoji mozilla" : ""
}`,
fontLigatures: false, // 连字符
smoothScrolling: true, // 平滑滚动
}}

View File

@@ -1,5 +1,5 @@
import { useState } from "react";
import {
import {
Button,
Box,
Dialog,
@@ -15,7 +15,7 @@ import {
LinearProgress,
alpha,
styled,
useTheme
useTheme,
} from "@mui/material";
import { useTranslation } from "react-i18next";
import { useLockFn } from "ahooks";
@@ -65,77 +65,83 @@ export const ProviderButton = () => {
const [open, setOpen] = useState(false);
const { proxyProviders, refreshProxy, refreshProxyProviders } = useAppData();
const [updating, setUpdating] = useState<Record<string, boolean>>({});
// 检查是否有提供者
const hasProviders = Object.keys(proxyProviders || {}).length > 0;
// 更新单个代理提供者
const updateProvider = useLockFn(async (name: string) => {
try {
// 设置更新状态
setUpdating(prev => ({ ...prev, [name]: true }));
setUpdating((prev) => ({ ...prev, [name]: true }));
await proxyProviderUpdate(name);
// 刷新数据
await refreshProxy();
await refreshProxyProviders();
showNotice('success', `${name} 更新成功`);
showNotice("success", `${name} 更新成功`);
} catch (err: any) {
showNotice('error', `${name} 更新失败: ${err?.message || err.toString()}`);
showNotice(
"error",
`${name} 更新失败: ${err?.message || err.toString()}`,
);
} finally {
// 清除更新状态
setUpdating(prev => ({ ...prev, [name]: false }));
setUpdating((prev) => ({ ...prev, [name]: false }));
}
});
// 更新所有代理提供者
const updateAllProviders = useLockFn(async () => {
try {
// 获取所有provider的名称
const allProviders = Object.keys(proxyProviders || {});
if (allProviders.length === 0) {
showNotice('info', "没有可更新的代理提供者");
showNotice("info", "没有可更新的代理提供者");
return;
}
// 设置所有provider为更新中状态
const newUpdating = allProviders.reduce((acc, key) => {
acc[key] = true;
return acc;
}, {} as Record<string, boolean>);
const newUpdating = allProviders.reduce(
(acc, key) => {
acc[key] = true;
return acc;
},
{} as Record<string, boolean>,
);
setUpdating(newUpdating);
// 改为串行逐个更新所有provider
for (const name of allProviders) {
try {
await proxyProviderUpdate(name);
// 每个更新完成后更新状态
setUpdating(prev => ({ ...prev, [name]: false }));
setUpdating((prev) => ({ ...prev, [name]: false }));
} catch (err) {
console.error(`更新 ${name} 失败`, err);
// 继续执行下一个,不中断整体流程
}
}
// 刷新数据
await refreshProxy();
await refreshProxyProviders();
showNotice('success', "全部代理提供者更新成功");
showNotice("success", "全部代理提供者更新成功");
} catch (err: any) {
showNotice('error', `更新失败: ${err?.message || err.toString()}`);
showNotice("error", `更新失败: ${err?.message || err.toString()}`);
} finally {
// 清除所有更新状态
setUpdating({});
}
});
const handleClose = () => {
setOpen(false);
};
if (!hasProviders) return null;
return (
@@ -149,15 +155,14 @@ export const ProviderButton = () => {
>
{t("Proxy Provider")}
</Button>
<Dialog
open={open}
onClose={handleClose}
maxWidth="sm"
fullWidth
>
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
<DialogTitle>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Box
display="flex"
justifyContent="space-between"
alignItems="center"
>
<Typography variant="h6">{t("Proxy Provider")}</Typography>
<Box>
<Button
@@ -170,14 +175,14 @@ export const ProviderButton = () => {
</Box>
</Box>
</DialogTitle>
<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];
// 订阅信息
const sub = provider.subscriptionInfo;
const hasSubInfo = !!sub;
@@ -185,46 +190,53 @@ export const ProviderButton = () => {
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"
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);
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",
}}>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Typography
variant="subtitle1"
component="div"
@@ -232,7 +244,7 @@ export const ProviderButton = () => {
title={key}
sx={{ display: "flex", alignItems: "center" }}
>
<span style={{ marginRight: "8px" }}>{key}</span>
<span style={{ marginRight: "8px" }}>{key}</span>
<TypeBox component="span">
{provider.proxies.length}
</TypeBox>
@@ -240,9 +252,14 @@ export const ProviderButton = () => {
{provider.vehicleType}
</TypeBox>
</Typography>
<Typography variant="body2" color="text.secondary" noWrap>
<small>{t("Update At")}: </small>{time.fromNow()}
<Typography
variant="body2"
color="text.secondary"
noWrap
>
<small>{t("Update At")}: </small>
{time.fromNow()}
</Typography>
</Box>
}
@@ -251,26 +268,29 @@ export const ProviderButton = () => {
{/* 订阅信息 */}
{hasSubInfo && (
<>
<Box sx={{
mb: 1,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}>
<Box
sx={{
mb: 1,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<span title={t("Used / Total") as string}>
{parseTraffic(upload + download)} / {parseTraffic(total)}
{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,
sx={{
height: 6,
borderRadius: 3,
opacity: total > 0 ? 1 : 0,
}}
@@ -281,12 +301,14 @@ export const ProviderButton = () => {
}
/>
<Divider orientation="vertical" flexItem />
<Box sx={{
width: 40,
display: "flex",
justifyContent: "center",
alignItems: "center"
}}>
<Box
sx={{
width: 40,
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<IconButton
size="small"
color="primary"
@@ -295,11 +317,13 @@ export const ProviderButton = () => {
}}
disabled={isUpdating}
sx={{
animation: isUpdating ? "spin 1s linear infinite" : "none",
animation: isUpdating
? "spin 1s linear infinite"
: "none",
"@keyframes spin": {
"0%": { transform: "rotate(0deg)" },
"100%": { transform: "rotate(360deg)" }
}
"100%": { transform: "rotate(360deg)" },
},
}}
title={t("Update Provider") as string}
>
@@ -311,7 +335,7 @@ export const ProviderButton = () => {
})}
</List>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} variant="outlined">
{t("Close")}

View File

@@ -31,7 +31,10 @@ interface RenderProps {
onLocation: (group: IRenderItem["group"]) => void;
onCheckAll: (groupName: string) => void;
onHeadState: (groupName: string, patch: Partial<HeadState>) => void;
onChangeProxy: (group: IRenderItem["group"], proxy: IRenderItem["proxy"] & { name: string }) => void;
onChangeProxy: (
group: IRenderItem["group"],
proxy: IRenderItem["proxy"] & { name: string },
) => void;
}
export const ProxyRender = (props: RenderProps) => {
@@ -129,14 +132,15 @@ export const ProxyRender = (props: RenderProps) => {
/>
<Box sx={{ display: "flex", alignItems: "center" }}>
<Tooltip title={t("Proxy Count")} arrow>
<Chip
size="small"
label={`${group.all.length}`}
sx={{
mr: 1,
backgroundColor: (theme) => alpha(theme.palette.primary.main, 0.1),
<Chip
size="small"
label={`${group.all.length}`}
sx={{
mr: 1,
backgroundColor: (theme) =>
alpha(theme.palette.primary.main, 0.1),
color: (theme) => theme.palette.primary.main,
}}
}}
/>
</Tooltip>
{headState?.open ? <ExpandLessRounded /> : <ExpandMoreRounded />}

View File

@@ -8,7 +8,7 @@ export default function useFilterSort(
proxies: IProxyItem[],
groupName: string,
filterText: string,
sortType: ProxySortType
sortType: ProxySortType,
) {
const [refresh, setRefresh] = useState({});
@@ -40,7 +40,7 @@ export function filterSort(
proxies: IProxyItem[],
groupName: string,
filterText: string,
sortType: ProxySortType
sortType: ProxySortType,
) {
const fp = filterProxies(proxies, groupName, filterText);
const sp = sortProxies(fp, groupName, sortType);
@@ -60,7 +60,7 @@ const regex2 = /type=(.*)/i;
function filterProxies(
proxies: IProxyItem[],
groupName: string,
filterText: string
filterText: string,
) {
if (!filterText) return proxies;
@@ -100,7 +100,7 @@ function filterProxies(
function sortProxies(
proxies: IProxyItem[],
groupName: string,
sortType: ProxySortType
sortType: ProxySortType,
) {
if (!proxies) return [];
if (sortType === 0) return proxies;

View File

@@ -37,7 +37,7 @@ export function useHeadStateNew() {
try {
const data = JSON.parse(
localStorage.getItem(HEAD_STATE_KEY)!
localStorage.getItem(HEAD_STATE_KEY)!,
) as HeadStateStorage;
const value = data[current] || {};
@@ -74,7 +74,7 @@ export function useHeadStateNew() {
return ret;
});
},
[current]
[current],
);
return [state, setHeadState] as const;

View File

@@ -1,9 +1,9 @@
import { useState } from "react";
import {
Button,
Box,
Dialog,
DialogTitle,
import {
Button,
Box,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
IconButton,
@@ -14,7 +14,7 @@ import {
Divider,
alpha,
styled,
useTheme
useTheme,
} from "@mui/material";
import { useTranslation } from "react-i18next";
import { useLockFn } from "ahooks";
@@ -54,74 +54,80 @@ export const ProviderButton = () => {
// 检查是否有提供者
const hasProviders = Object.keys(ruleProviders || {}).length > 0;
// 更新单个规则提供者
const updateProvider = useLockFn(async (name: string) => {
try {
// 设置更新状态
setUpdating(prev => ({ ...prev, [name]: true }));
setUpdating((prev) => ({ ...prev, [name]: true }));
await ruleProviderUpdate(name);
// 刷新数据
await refreshRules();
await refreshRuleProviders();
showNotice('success', `${name} 更新成功`);
showNotice("success", `${name} 更新成功`);
} catch (err: any) {
showNotice('error', `${name} 更新失败: ${err?.message || err.toString()}`);
showNotice(
"error",
`${name} 更新失败: ${err?.message || err.toString()}`,
);
} finally {
// 清除更新状态
setUpdating(prev => ({ ...prev, [name]: false }));
setUpdating((prev) => ({ ...prev, [name]: false }));
}
});
// 更新所有规则提供者
const updateAllProviders = useLockFn(async () => {
try {
// 获取所有provider的名称
const allProviders = Object.keys(ruleProviders || {});
if (allProviders.length === 0) {
showNotice('info', "没有可更新的规则提供者");
showNotice("info", "没有可更新的规则提供者");
return;
}
// 设置所有provider为更新中状态
const newUpdating = allProviders.reduce((acc, key) => {
acc[key] = true;
return acc;
}, {} as Record<string, boolean>);
const newUpdating = allProviders.reduce(
(acc, key) => {
acc[key] = true;
return acc;
},
{} as Record<string, boolean>,
);
setUpdating(newUpdating);
// 改为串行逐个更新所有provider
for (const name of allProviders) {
try {
await ruleProviderUpdate(name);
// 每个更新完成后更新状态
setUpdating(prev => ({ ...prev, [name]: false }));
setUpdating((prev) => ({ ...prev, [name]: false }));
} catch (err) {
console.error(`更新 ${name} 失败`, err);
// 继续执行下一个,不中断整体流程
}
}
// 刷新数据
await refreshRules();
await refreshRuleProviders();
showNotice('success', "全部规则提供者更新成功");
showNotice("success", "全部规则提供者更新成功");
} catch (err: any) {
showNotice('error', `更新失败: ${err?.message || err.toString()}`);
showNotice("error", `更新失败: ${err?.message || err.toString()}`);
} finally {
// 清除所有更新状态
setUpdating({});
}
});
const handleClose = () => {
setOpen(false);
};
if (!hasProviders) return null;
return (
@@ -134,15 +140,14 @@ export const ProviderButton = () => {
>
{t("Rule Provider")}
</Button>
<Dialog
open={open}
onClose={handleClose}
maxWidth="sm"
fullWidth
>
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
<DialogTitle>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Box
display="flex"
justifyContent="space-between"
alignItems="center"
>
<Typography variant="h6">{t("Rule Providers")}</Typography>
<Button
variant="contained"
@@ -153,49 +158,52 @@ export const ProviderButton = () => {
</Button>
</Box>
</DialogTitle>
<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];
return (
<ListItem
key={key}
sx={[
{
p: 0,
{
p: 0,
mb: "8px",
borderRadius: 2,
overflow: "hidden",
transition: "all 0.2s"
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);
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)
}
borderColor: alpha(primary.main, 0.3),
},
};
}
},
]}
>
<ListItemText
sx={{ px: 2, py: 1 }}
primary={
<Box sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Typography
variant="subtitle1"
component="div"
@@ -203,14 +211,19 @@ export const ProviderButton = () => {
title={key}
sx={{ display: "flex", alignItems: "center" }}
>
<span style={{ marginRight: "8px" }}>{key}</span>
<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
variant="body2"
color="text.secondary"
noWrap
>
<small>{t("Update At")}: </small>
{time.fromNow()}
</Typography>
</Box>
}
@@ -219,30 +232,32 @@ export const ProviderButton = () => {
<TypeBox component="span">
{provider.vehicleType}
</TypeBox>
<TypeBox component="span">
{provider.behavior}
</TypeBox>
<TypeBox component="span">{provider.behavior}</TypeBox>
</Box>
}
/>
<Divider orientation="vertical" flexItem />
<Box sx={{
width: 40,
display: "flex",
justifyContent: "center",
alignItems: "center"
}}>
<Box
sx={{
width: 40,
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<IconButton
size="small"
color="primary"
onClick={() => updateProvider(key)}
disabled={isUpdating}
sx={{
animation: isUpdating ? "spin 1s linear infinite" : "none",
animation: isUpdating
? "spin 1s linear infinite"
: "none",
"@keyframes spin": {
"0%": { transform: "rotate(0deg)" },
"100%": { transform: "rotate(360deg)" }
}
"100%": { transform: "rotate(360deg)" },
},
}}
title={t("Update Provider") as string}
>
@@ -254,7 +269,7 @@ export const ProviderButton = () => {
})}
</List>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} variant="outlined">
{t("Close")}

View File

@@ -82,21 +82,21 @@ export const BackupConfigViewer = memo(
if (!url) {
urlRef.current?.focus();
showNotice('error', t("WebDAV URL Required"));
showNotice("error", t("WebDAV URL Required"));
throw new Error(t("WebDAV URL Required"));
} else if (!isValidUrl(url)) {
urlRef.current?.focus();
showNotice('error', t("Invalid WebDAV URL"));
showNotice("error", t("Invalid WebDAV URL"));
throw new Error(t("Invalid WebDAV URL"));
}
if (!username) {
usernameRef.current?.focus();
showNotice('error', t("WebDAV URL Required"));
showNotice("error", t("WebDAV URL Required"));
throw new Error(t("Username Required"));
}
if (!password) {
passwordRef.current?.focus();
showNotice('error', t("WebDAV URL Required"));
showNotice("error", t("WebDAV URL Required"));
throw new Error(t("Password Required"));
}
};
@@ -110,11 +110,11 @@ export const BackupConfigViewer = memo(
data.username.trim(),
data.password,
).then(() => {
showNotice('success', t("WebDAV Config Saved"));
showNotice("success", t("WebDAV Config Saved"));
onSaveSuccess();
});
} catch (error) {
showNotice('error', t("WebDAV Config Save Failed", { error }), 3000);
showNotice("error", t("WebDAV Config Save Failed", { error }), 3000);
} finally {
setLoading(false);
}
@@ -125,11 +125,11 @@ export const BackupConfigViewer = memo(
try {
setLoading(true);
await createWebdavBackup().then(async () => {
showNotice('success', t("Backup Created"));
showNotice("success", t("Backup Created"));
await onBackupSuccess();
});
} catch (error) {
showNotice('error', t("Backup Failed", { error }));
showNotice("error", t("Backup Failed", { error }));
} finally {
setLoading(false);
}

View File

@@ -61,7 +61,7 @@ export const BackupTableViewer = memo(
const handleRestore = useLockFn(async (filename: string) => {
await restoreWebDavBackup(filename).then(() => {
showNotice('success', t("Restore Success, App will restart in 1s"));
showNotice("success", t("Restore Success, App will restart in 1s"));
});
await restartApp();
});

View File

@@ -52,7 +52,7 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
const errorMsg = await changeClashCore(core);
if (errorMsg) {
showNotice('error', errorMsg);
showNotice("error", errorMsg);
setChangingCore(null);
return;
}
@@ -65,7 +65,7 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
}, 500);
} catch (err: any) {
setChangingCore(null);
showNotice('error', err.message || err.toString());
showNotice("error", err.message || err.toString());
}
});
@@ -73,11 +73,11 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
try {
setRestarting(true);
await restartCore();
showNotice('success', t(`Clash Core Restarted`));
showNotice("success", t(`Clash Core Restarted`));
setRestarting(false);
} catch (err: any) {
setRestarting(false);
showNotice('error', err.message || err.toString());
showNotice("error", err.message || err.toString());
}
});
@@ -86,14 +86,14 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
setUpgrading(true);
await upgradeCore();
setUpgrading(false);
showNotice('success', t(`Core Version Updated`));
showNotice("success", t(`Core Version Updated`));
} catch (err: any) {
setUpgrading(false);
const errMsg = err.response?.data?.message || err.toString();
const showMsg = errMsg.includes("already using latest version")
? "Already Using Latest Core Version"
: errMsg;
showNotice('error', t(showMsg));
showNotice("error", t(showMsg));
}
});

View File

@@ -11,7 +11,7 @@ import {
ListItem,
ListItemText,
Stack,
TextField
TextField,
} from "@mui/material";
import { useLockFn, useRequest } from "ahooks";
import { forwardRef, useImperativeHandle, useState } from "react";
@@ -26,127 +26,136 @@ interface ClashPortViewerRef {
close: () => void;
}
const generateRandomPort = () => Math.floor(Math.random() * (65535 - 1025 + 1)) + 1025;
const generateRandomPort = () =>
Math.floor(Math.random() * (65535 - 1025 + 1)) + 1025;
export const ClashPortViewer = forwardRef<ClashPortViewerRef, ClashPortViewerProps>(
(props, ref) => {
const { t } = useTranslation();
const { clashInfo, patchInfo } = useClashInfo();
const { verge, patchVerge } = useVerge();
const [open, setOpen] = useState(false);
export const ClashPortViewer = forwardRef<
ClashPortViewerRef,
ClashPortViewerProps
>((props, ref) => {
const { t } = useTranslation();
const { clashInfo, patchInfo } = useClashInfo();
const { verge, patchVerge } = useVerge();
const [open, setOpen] = useState(false);
// Mixed Port
const [mixedPort, setMixedPort] = useState(
verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897
);
// Mixed Port
const [mixedPort, setMixedPort] = useState(
verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897,
);
// 其他端口状态
const [socksPort, setSocksPort] = useState(verge?.verge_socks_port ?? 7898);
const [socksEnabled, setSocksEnabled] = useState(verge?.verge_socks_enabled ?? false);
const [httpPort, setHttpPort] = useState(verge?.verge_port ?? 7899);
const [httpEnabled, setHttpEnabled] = useState(verge?.verge_http_enabled ?? false);
const [redirPort, setRedirPort] = useState(verge?.verge_redir_port ?? 7895);
const [redirEnabled, setRedirEnabled] = useState(verge?.verge_redir_enabled ?? false);
const [tproxyPort, setTproxyPort] = useState(verge?.verge_tproxy_port ?? 7896);
const [tproxyEnabled, setTproxyEnabled] = useState(verge?.verge_tproxy_enabled ?? false);
// 其他端口状态
const [socksPort, setSocksPort] = useState(verge?.verge_socks_port ?? 7898);
const [socksEnabled, setSocksEnabled] = useState(
verge?.verge_socks_enabled ?? false,
);
const [httpPort, setHttpPort] = useState(verge?.verge_port ?? 7899);
const [httpEnabled, setHttpEnabled] = useState(
verge?.verge_http_enabled ?? false,
);
const [redirPort, setRedirPort] = useState(verge?.verge_redir_port ?? 7895);
const [redirEnabled, setRedirEnabled] = useState(
verge?.verge_redir_enabled ?? false,
);
const [tproxyPort, setTproxyPort] = useState(
verge?.verge_tproxy_port ?? 7896,
);
const [tproxyEnabled, setTproxyEnabled] = useState(
verge?.verge_tproxy_enabled ?? false,
);
// 添加保存请求防止GUI卡死
const { loading, run: saveSettings } = useRequest(
async (params: {
clashConfig: any;
vergeConfig: any;
}) => {
const { clashConfig, vergeConfig } = params;
await Promise.all([
patchInfo(clashConfig),
patchVerge(vergeConfig)
]);
// 添加保存请求防止GUI卡死
const { loading, run: saveSettings } = useRequest(
async (params: { clashConfig: any; vergeConfig: any }) => {
const { clashConfig, vergeConfig } = params;
await Promise.all([patchInfo(clashConfig), patchVerge(vergeConfig)]);
},
{
manual: true,
onSuccess: () => {
setOpen(false);
showNotice("success", t("Port settings saved")); // 调用提示函数
},
{
manual: true,
onSuccess: () => {
setOpen(false);
showNotice("success", t("Port settings saved")); // 调用提示函数
},
onError: () => {
showNotice("error", t("Failed to save settings")); // 调用提示函数
}
}
);
useImperativeHandle(ref, () => ({
open: () => {
setMixedPort(verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897);
setSocksPort(verge?.verge_socks_port ?? 7898);
setSocksEnabled(verge?.verge_socks_enabled ?? false);
setHttpPort(verge?.verge_port ?? 7899);
setHttpEnabled(verge?.verge_http_enabled ?? false);
setRedirPort(verge?.verge_redir_port ?? 7895);
setRedirEnabled(verge?.verge_redir_enabled ?? false);
setTproxyPort(verge?.verge_tproxy_port ?? 7896);
setTproxyEnabled(verge?.verge_tproxy_enabled ?? false);
setOpen( true);
onError: () => {
showNotice("error", t("Failed to save settings")); // 调用提示函数
},
close: () => setOpen(false),
}));
},
);
const onSave = useLockFn(async () => {
// 端口冲突检测
const portList = [
mixedPort,
socksEnabled ? socksPort : -1,
httpEnabled ? httpPort : -1,
redirEnabled ? redirPort : -1,
tproxyEnabled ? tproxyPort : -1
].filter(p => p !== -1);
useImperativeHandle(ref, () => ({
open: () => {
setMixedPort(verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897);
setSocksPort(verge?.verge_socks_port ?? 7898);
setSocksEnabled(verge?.verge_socks_enabled ?? false);
setHttpPort(verge?.verge_port ?? 7899);
setHttpEnabled(verge?.verge_http_enabled ?? false);
setRedirPort(verge?.verge_redir_port ?? 7895);
setRedirEnabled(verge?.verge_redir_enabled ?? false);
setTproxyPort(verge?.verge_tproxy_port ?? 7896);
setTproxyEnabled(verge?.verge_tproxy_enabled ?? false);
setOpen(true);
},
close: () => setOpen(false),
}));
if (new Set(portList).size !== portList.length) {
return;
}
const onSave = useLockFn(async () => {
// 端口冲突检测
const portList = [
mixedPort,
socksEnabled ? socksPort : -1,
httpEnabled ? httpPort : -1,
redirEnabled ? redirPort : -1,
tproxyEnabled ? tproxyPort : -1,
].filter((p) => p !== -1);
// 验证端口范围
const isValidPort = (port: number) => port >= 1 && port <= 65535;
const allPortsValid = [
mixedPort,
socksEnabled ? socksPort : 0,
httpEnabled ? httpPort : 0,
redirEnabled ? redirPort : 0,
tproxyEnabled ? tproxyPort : 0
].every(port => port === 0 || isValidPort(port));
if (new Set(portList).size !== portList.length) {
return;
}
if (!allPortsValid) {
return;
}
// 验证端口范围
const isValidPort = (port: number) => port >= 1 && port <= 65535;
const allPortsValid = [
mixedPort,
socksEnabled ? socksPort : 0,
httpEnabled ? httpPort : 0,
redirEnabled ? redirPort : 0,
tproxyEnabled ? tproxyPort : 0,
].every((port) => port === 0 || isValidPort(port));
// 准备配置数据
const clashConfig = {
"mixed-port": mixedPort,
"socks-port": socksPort,
port: httpPort,
"redir-port": redirPort,
"tproxy-port": tproxyPort
};
if (!allPortsValid) {
return;
}
const vergeConfig = {
verge_mixed_port: mixedPort,
verge_socks_port: socksPort,
verge_socks_enabled: socksEnabled,
verge_port: httpPort,
verge_http_enabled: httpEnabled,
verge_redir_port: redirPort,
verge_redir_enabled: redirEnabled,
verge_tproxy_port: tproxyPort,
verge_tproxy_enabled: tproxyEnabled
};
// 准备配置数据
const clashConfig = {
"mixed-port": mixedPort,
"socks-port": socksPort,
port: httpPort,
"redir-port": redirPort,
"tproxy-port": tproxyPort,
};
// 提交保存请求
await saveSettings({ clashConfig, vergeConfig });
});
const vergeConfig = {
verge_mixed_port: mixedPort,
verge_socks_port: socksPort,
verge_socks_enabled: socksEnabled,
verge_port: httpPort,
verge_http_enabled: httpEnabled,
verge_redir_port: redirPort,
verge_redir_enabled: redirEnabled,
verge_tproxy_port: tproxyPort,
verge_tproxy_enabled: tproxyEnabled,
};
// 优化的数字输入处理
const handleNumericChange = (setter: (value: number) => void) => (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value.replace(/\D+/, '');
if (value === '') {
// 提交保存请求
await saveSettings({ clashConfig, vergeConfig });
});
// 优化的数字输入处理
const handleNumericChange =
(setter: (value: number) => void) =>
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value.replace(/\D+/, "");
if (value === "") {
setter(0);
return;
}
@@ -157,190 +166,201 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef, ClashPortViewerPro
}
};
return (
<BaseDialog
open={open}
title={t("Port Configuration")}
contentSx={{
width: 400
}}
okBtn={
loading ? (
<Stack direction="row" alignItems="center" spacing={1}>
<CircularProgress size={20} />
{t("Saving...")}
</Stack>
) : t("Save")
}
cancelBtn={t("Cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={onSave}
>
<List sx={{ width: "100%" }}>
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText
primary={t("Mixed Port")}
primaryTypographyProps={{ fontSize: 12 }}
return (
<BaseDialog
open={open}
title={t("Port Configuration")}
contentSx={{
width: 400,
}}
okBtn={
loading ? (
<Stack direction="row" alignItems="center" spacing={1}>
<CircularProgress size={20} />
{t("Saving...")}
</Stack>
) : (
t("Save")
)
}
cancelBtn={t("Cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={onSave}
>
<List sx={{ width: "100%" }}>
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText
primary={t("Mixed Port")}
primaryTypographyProps={{ fontSize: 12 }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
<TextField
size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={mixedPort}
onChange={(e) =>
setMixedPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
}
inputProps={{ style: { fontSize: 12 } }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
<TextField
size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={mixedPort}
onChange={(e) => setMixedPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))}
inputProps={{ style: { fontSize: 12 } }}
/>
<IconButton
size="small"
onClick={() => setMixedPort(generateRandomPort())}
title={t("Random Port")}
sx={{ mr: 0.5 }}
>
<Shuffle fontSize="small" />
</IconButton>
<Switch
size="small"
checked={true}
disabled={true}
sx={{ ml: 0.5, opacity: 0.7 }}
/>
</div>
</ListItem>
<IconButton
size="small"
onClick={() => setMixedPort(generateRandomPort())}
title={t("Random Port")}
sx={{ mr: 0.5 }}
>
<Shuffle fontSize="small" />
</IconButton>
<Switch
size="small"
checked={true}
disabled={true}
sx={{ ml: 0.5, opacity: 0.7 }}
/>
</div>
</ListItem>
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText
primary={t("Socks Port")}
primaryTypographyProps={{ fontSize: 12 }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
<TextField
size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={socksPort}
onChange={(e) =>
setSocksPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
}
disabled={!socksEnabled}
inputProps={{ style: { fontSize: 12 } }}
/>
<IconButton
size="small"
onClick={() => setSocksPort(generateRandomPort())}
title={t("Random Port")}
disabled={!socksEnabled}
sx={{ mr: 0.5 }}
>
<Shuffle fontSize="small" />
</IconButton>
<Switch
size="small"
checked={socksEnabled}
onChange={(_, c) => setSocksEnabled(c)}
sx={{ ml: 0.5 }}
/>
</div>
</ListItem>
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText
primary={t("HTTP Port")}
primaryTypographyProps={{ fontSize: 12 }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
<TextField
size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={httpPort}
onChange={(e) =>
setHttpPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
}
disabled={!httpEnabled}
inputProps={{ style: { fontSize: 12 } }}
/>
<IconButton
size="small"
onClick={() => setHttpPort(generateRandomPort())}
title={t("Random Port")}
disabled={!httpEnabled}
sx={{ mr: 0.5 }}
>
<Shuffle fontSize="small" />
</IconButton>
<Switch
size="small"
checked={httpEnabled}
onChange={(_, c) => setHttpEnabled(c)}
sx={{ ml: 0.5 }}
/>
</div>
</ListItem>
{OS !== "windows" && (
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText
primary={t("Socks Port")}
primary={t("Redir Port")}
primaryTypographyProps={{ fontSize: 12 }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
<TextField
size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={socksPort}
onChange={(e) => setSocksPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))}
disabled={!socksEnabled}
value={redirPort}
onChange={(e) =>
setRedirPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
}
disabled={!redirEnabled}
inputProps={{ style: { fontSize: 12 } }}
/>
<IconButton
size="small"
onClick={() => setSocksPort(generateRandomPort())}
onClick={() => setRedirPort(generateRandomPort())}
title={t("Random Port")}
disabled={!socksEnabled}
disabled={!redirEnabled}
sx={{ mr: 0.5 }}
>
<Shuffle fontSize="small" />
</IconButton>
<Switch
size="small"
checked={socksEnabled}
onChange={(_, c) => setSocksEnabled(c)}
checked={redirEnabled}
onChange={(_, c) => setRedirEnabled(c)}
sx={{ ml: 0.5 }}
/>
</div>
</ListItem>
)}
{OS === "linux" && (
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText
primary={t("HTTP Port")}
primary={t("Tproxy Port")}
primaryTypographyProps={{ fontSize: 12 }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
<TextField
size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={httpPort}
onChange={(e) => setHttpPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))}
disabled={!httpEnabled}
value={tproxyPort}
onChange={(e) =>
setTproxyPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
}
disabled={!tproxyEnabled}
inputProps={{ style: { fontSize: 12 } }}
/>
<IconButton
size="small"
onClick={() => setHttpPort(generateRandomPort())}
onClick={() => setTproxyPort(generateRandomPort())}
title={t("Random Port")}
disabled={!httpEnabled}
disabled={!tproxyEnabled}
sx={{ mr: 0.5 }}
>
<Shuffle fontSize="small" />
</IconButton>
<Switch
size="small"
checked={httpEnabled}
onChange={(_, c) => setHttpEnabled(c)}
checked={tproxyEnabled}
onChange={(_, c) => setTproxyEnabled(c)}
sx={{ ml: 0.5 }}
/>
</div>
</ListItem>
{OS !== "windows" && (
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText
primary={t("Redir Port")}
primaryTypographyProps={{ fontSize: 12 }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
<TextField
size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={redirPort}
onChange={(e) => setRedirPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))}
disabled={!redirEnabled}
inputProps={{ style: { fontSize: 12 } }}
/>
<IconButton
size="small"
onClick={() => setRedirPort(generateRandomPort())}
title={t("Random Port")}
disabled={!redirEnabled}
sx={{ mr: 0.5 }}
>
<Shuffle fontSize="small" />
</IconButton>
<Switch
size="small"
checked={redirEnabled}
onChange={(_, c) => setRedirEnabled(c)}
sx={{ ml: 0.5 }}
/>
</div>
</ListItem>
)}
{OS === "linux" && (
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText
primary={t("Tproxy Port")}
primaryTypographyProps={{ fontSize: 12 }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
<TextField
size="small"
sx={{ width: 80, mr: 0.5, fontSize: 12 }}
value={tproxyPort}
onChange={(e) => setTproxyPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))}
disabled={!tproxyEnabled}
inputProps={{ style: { fontSize: 12 } }}
/>
<IconButton
size="small"
onClick={() => setTproxyPort(generateRandomPort())}
title={t("Random Port")}
disabled={!tproxyEnabled}
sx={{ mr: 0.5 }}
>
<Shuffle fontSize="small" />
</IconButton>
<Switch
size="small"
checked={tproxyEnabled}
onChange={(_, c) => setTproxyEnabled(c)}
sx={{ ml: 0.5 }}
/>
</div>
</ListItem>
)}
</List>
</BaseDialog>
);
}
);
)}
</List>
</BaseDialog>
);
});

View File

@@ -13,7 +13,7 @@ import {
ListItemText,
Snackbar,
TextField,
Tooltip
Tooltip,
} from "@mui/material";
import { useLockFn } from "ahooks";
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
@@ -42,58 +42,72 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
// 保存配置
const onSave = useLockFn(async () => {
if (!controller.trim()) {
showNotice('error', t("Controller address cannot be empty"), 3000);
showNotice("error", t("Controller address cannot be empty"), 3000);
return;
}
if (!secret.trim()) {
showNotice('error', t("Secret cannot be empty"), 3000);
showNotice("error", t("Secret cannot be empty"), 3000);
return;
}
try {
setIsSaving(true);
await patchInfo({ "external-controller": controller, secret });
showNotice('success', t("Configuration saved successfully"), 2000);
showNotice("success", t("Configuration saved successfully"), 2000);
setOpen(false);
} catch (err: any) {
showNotice('error', err.message || t("Failed to save configuration"), 4000);
showNotice(
"error",
err.message || t("Failed to save configuration"),
4000,
);
} finally {
setIsSaving(false);
}
});
// 复制到剪贴板
const handleCopyToClipboard = useLockFn(async (text: string, type: string) => {
try {
await navigator.clipboard.writeText(text);
setCopySuccess(type);
setTimeout(() => setCopySuccess(null), 2000);
} catch (err) {
showNotice('error', t("Failed to copy"), 2000);
}
});
const handleCopyToClipboard = useLockFn(
async (text: string, type: string) => {
try {
await navigator.clipboard.writeText(text);
setCopySuccess(type);
setTimeout(() => setCopySuccess(null), 2000);
} catch (err) {
showNotice("error", t("Failed to copy"), 2000);
}
},
);
return (
<BaseDialog
open={open}
title={t("External Controller")}
contentSx={{ width: 400 }}
okBtn={isSaving ? (
<Box display="flex" alignItems="center" gap={1}>
<CircularProgress size={16} color="inherit" />
{t("Saving...")}
</Box>
) : (
t("Save")
)}
okBtn={
isSaving ? (
<Box display="flex" alignItems="center" gap={1}>
<CircularProgress size={16} color="inherit" />
{t("Saving...")}
</Box>
) : (
t("Save")
)
}
cancelBtn={t("Cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={onSave}
>
<List>
<ListItem sx={{ padding: "5px 2px", display: "flex", justifyContent: "space-between" }}>
<ListItem
sx={{
padding: "5px 2px",
display: "flex",
justifyContent: "space-between",
}}
>
<ListItemText primary={t("External Controller")} />
<Box display="flex" alignItems="center" gap={1}>
<TextField
@@ -101,11 +115,11 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
sx={{
width: 175,
opacity: 1,
pointerEvents: 'auto'
pointerEvents: "auto",
}}
value={controller}
placeholder="Required"
onChange={e => setController(e.target.value)}
onChange={(e) => setController(e.target.value)}
disabled={isSaving}
/>
<Tooltip title={t("Copy to clipboard")}>
@@ -121,7 +135,13 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
</Box>
</ListItem>
<ListItem sx={{ padding: "5px 2px", display: "flex", justifyContent: "space-between" }}>
<ListItem
sx={{
padding: "5px 2px",
display: "flex",
justifyContent: "space-between",
}}
>
<ListItemText primary={t("Core Secret")} />
<Box display="flex" alignItems="center" gap={1}>
<TextField
@@ -129,11 +149,11 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
sx={{
width: 175,
opacity: 1,
pointerEvents: 'auto'
pointerEvents: "auto",
}}
value={secret}
placeholder={t("Recommended")}
onChange={e => setSecret(e.target.value)}
onChange={(e) => setSecret(e.target.value)}
disabled={isSaving}
/>
<Tooltip title={t("Copy to clipboard")}>
@@ -153,13 +173,12 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
<Snackbar
open={copySuccess !== null}
autoHideDuration={2000}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
>
<Alert severity="success">
{copySuccess === "controller"
? t("Controller address copied to clipboard")
: t("Secret copied to clipboard")
}
: t("Secret copied to clipboard")}
</Alert>
</Snackbar>
</BaseDialog>

View File

@@ -59,7 +59,13 @@ const DEFAULT_DNS_CONFIG = {
"*.msftncsi.com",
"www.msftconnecttest.com",
],
"default-nameserver": ["system", "223.6.6.6", "8.8.8.8", "2400:3200::1", "2001:4860:4860::8888"],
"default-nameserver": [
"system",
"223.6.6.6",
"8.8.8.8",
"2400:3200::1",
"2001:4860:4860::8888",
],
nameserver: [
"8.8.8.8",
"https://doh.pub/dns-query",
@@ -70,7 +76,7 @@ const DEFAULT_DNS_CONFIG = {
"proxy-server-nameserver": [
"https://doh.pub/dns-query",
"https://dns.alidns.com/dns-query",
"tls://223.5.5.5"
"tls://223.5.5.5",
],
"direct-nameserver": [],
"direct-nameserver-follow-policy": false,
@@ -219,8 +225,7 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
dnsConfig["respect-rules"] ?? DEFAULT_DNS_CONFIG["respect-rules"],
useHosts: dnsConfig["use-hosts"] ?? DEFAULT_DNS_CONFIG["use-hosts"],
useSystemHosts:
dnsConfig["use-system-hosts"] ??
DEFAULT_DNS_CONFIG["use-system-hosts"],
dnsConfig["use-system-hosts"] ?? DEFAULT_DNS_CONFIG["use-system-hosts"],
ipv6: dnsConfig.ipv6 ?? DEFAULT_DNS_CONFIG.ipv6,
fakeIpFilter:
dnsConfig["fake-ip-filter"]?.join(", ") ??
@@ -229,7 +234,8 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
dnsConfig.nameserver?.join(", ") ??
DEFAULT_DNS_CONFIG.nameserver.join(", "),
fallback:
dnsConfig.fallback?.join(", ") ?? DEFAULT_DNS_CONFIG.fallback.join(", "),
dnsConfig.fallback?.join(", ") ??
DEFAULT_DNS_CONFIG.fallback.join(", "),
defaultNameserver:
dnsConfig["default-nameserver"]?.join(", ") ??
DEFAULT_DNS_CONFIG["default-nameserver"].join(", "),
@@ -299,9 +305,8 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
// 从表单值更新YAML内容
const updateYamlFromValues = () => {
const config: Record<string, any> = {};
const dnsConfig = generateDnsConfig();
if (Object.keys(dnsConfig).length > 0) {
config.dns = dnsConfig;
@@ -311,7 +316,7 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
if (Object.keys(hosts).length > 0) {
config.hosts = hosts;
}
setYamlContent(yaml.dump(config, { forceQuotes: true }));
};
@@ -320,10 +325,10 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
try {
const parsedYaml = yaml.load(yamlContent) as any;
if (!parsedYaml) return;
updateValuesFromConfig(parsedYaml);
} catch (err: any) {
showNotice('error', t("Invalid YAML format"));
showNotice("error", t("Invalid YAML format"));
}
};
@@ -505,7 +510,7 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
if (Object.keys(dnsConfig).length > 0) {
config.dns = dnsConfig;
}
const hosts = parseHosts(values.hosts);
if (Object.keys(hosts).length > 0) {
config.hosts = hosts;
@@ -521,30 +526,41 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
// 保存配置
await invoke("save_dns_config", { dnsConfig: config });
// 验证配置
const [isValid, errorMsg] = await invoke<[boolean, string]>("validate_dns_config", {});
const [isValid, errorMsg] = await invoke<[boolean, string]>(
"validate_dns_config",
{},
);
if (!isValid) {
let cleanErrorMsg = errorMsg;
// 提取关键错误信息
if (errorMsg.includes("level=error")) {
const errorLines = errorMsg.split('\n').filter(line =>
line.includes("level=error") ||
line.includes("level=fatal") ||
line.includes("failed")
);
const errorLines = errorMsg
.split("\n")
.filter(
(line) =>
line.includes("level=error") ||
line.includes("level=fatal") ||
line.includes("failed"),
);
if (errorLines.length > 0) {
cleanErrorMsg = errorLines.map(line => {
const msgMatch = line.match(/msg="([^"]+)"/);
return msgMatch ? msgMatch[1] : line;
}).join(", ");
cleanErrorMsg = errorLines
.map((line) => {
const msgMatch = line.match(/msg="([^"]+)"/);
return msgMatch ? msgMatch[1] : line;
})
.join(", ");
}
}
showNotice('error', t("DNS configuration error") + ": " + cleanErrorMsg);
showNotice(
"error",
t("DNS configuration error") + ": " + cleanErrorMsg,
);
return;
}
@@ -555,9 +571,9 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
}
setOpen(false);
showNotice('success', t("DNS settings saved"));
showNotice("success", t("DNS settings saved"));
} catch (err: any) {
showNotice('error', err.message || err.toString());
showNotice("error", err.message || err.toString());
}
});

View File

@@ -80,7 +80,7 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
});
setOpen(false);
} catch (err: any) {
showNotice('error', err.toString());
showNotice("error", err.toString());
}
});

View File

@@ -88,7 +88,7 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
const onSwitchFormat = (_e: any, value: boolean) => value;
const onError = (err: any) => {
showNotice('error', err.message || err.toString());
showNotice("error", err.message || err.toString());
};
const onChangeData = (patch: Partial<IVergeConfig>) => {
mutateVerge({ ...verge, ...patch }, false);

View File

@@ -44,7 +44,7 @@ export const LiteModeViewer = forwardRef<DialogRef>((props, ref) => {
});
setOpen(false);
} catch (err: any) {
showNotice('error', err.message || err.toString());
showNotice("error", err.message || err.toString());
}
});

View File

@@ -62,7 +62,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
});
setOpen(false);
} catch (err: any) {
showNotice('error', err.toString());
showNotice("error", err.toString());
}
});
@@ -246,7 +246,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
endAdornment: (
<InputAdornment position="end">{t("millis")}</InputAdornment>
),
}
},
}}
/>
</ListItem>

View File

@@ -129,7 +129,7 @@ const AddressDisplay = (props: { label: string; content: string }) => {
size="small"
onClick={async () => {
await writeText(props.content);
showNotice('success', t("Copy Success"));
showNotice("success", t("Copy Success"));
}}
>
<ContentCopyRounded sx={{ fontSize: "18px" }} />

View File

@@ -202,11 +202,14 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
const onSave = useLockFn(async () => {
if (value.duration < 1) {
showNotice('error', t("Proxy Daemon Duration Cannot be Less than 1 Second"));
showNotice(
"error",
t("Proxy Daemon Duration Cannot be Less than 1 Second"),
);
return;
}
if (value.bypass && !validReg.test(value.bypass)) {
showNotice('error', t("Invalid Bypass Format"));
showNotice("error", t("Invalid Bypass Format"));
return;
}
@@ -223,7 +226,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
!ipv6Regex.test(value.proxy_host) &&
!hostnameRegex.test(value.proxy_host)
) {
showNotice('error', t("Invalid Proxy Host Format"));
showNotice("error", t("Invalid Proxy Host Format"));
return;
}
@@ -271,41 +274,44 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
}
// 判断是否需要重置系统代理
const needResetProxy =
value.pac !== proxy_auto_config ||
proxyHost !== proxy_host ||
pacContent !== pac_file_content ||
const needResetProxy =
value.pac !== proxy_auto_config ||
proxyHost !== proxy_host ||
pacContent !== pac_file_content ||
value.bypass !== system_proxy_bypass ||
value.use_default !== use_default_bypass;
await patchVerge(patch);
// 更新系统代理状态以便UI立即反映变化
await Promise.all([mutate("getSystemProxy"), mutate("getAutotemProxy")]);
// 只有在修改了影响系统代理的配置且系统代理当前启用时,才重置系统代理
if (needResetProxy) {
const currentSysProxy = await getSystemProxy();
const currentAutoProxy = await getAutotemProxy();
if (value.pac ? currentAutoProxy?.enable : currentSysProxy?.enable) {
// 临时关闭系统代理
await patchVergeConfig({ enable_system_proxy: false });
// 减少等待时间
await new Promise((resolve) => setTimeout(resolve, 200));
// 重新开启系统代理
await patchVergeConfig({ enable_system_proxy: true });
// 更新UI状态
await Promise.all([mutate("getSystemProxy"), mutate("getAutotemProxy")]);
await Promise.all([
mutate("getSystemProxy"),
mutate("getAutotemProxy"),
]);
}
}
setOpen(false);
} catch (err: any) {
showNotice('error', err.toString());
showNotice("error", err.toString());
} finally {
setSaving(false);
}
@@ -415,7 +421,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
slotProps={{
input: {
endAdornment: <InputAdornment position="end">s</InputAdornment>,
}
},
}}
onChange={(e) => {
setValue((v) => ({
@@ -432,12 +438,14 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
edge="end"
disabled={!enabled}
checked={value.use_default}
onChange={(_, e) => setValue((v) => ({
...v,
use_default: e,
// 当取消选择use_default且当前bypass为空时填充默认值
bypass: (!e && !v.bypass) ? defaultBypass() : v.bypass
}))}
onChange={(_, e) =>
setValue((v) => ({
...v,
use_default: e,
// 当取消选择use_default且当前bypass为空时填充默认值
bypass: !e && !v.bypass ? defaultBypass() : v.bypass,
}))
}
/>
</ListItem>
)}

View File

@@ -49,7 +49,7 @@ export const ThemeViewer = forwardRef<DialogRef>((props, ref) => {
await patchVerge({ theme_setting: theme });
setOpen(false);
} catch (err: any) {
showNotice('error', err.toString());
showNotice("error", err.toString());
}
});

View File

@@ -77,13 +77,13 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
);
try {
await enhanceProfiles();
showNotice('success', t("Settings Applied"));
showNotice("success", t("Settings Applied"));
} catch (err: any) {
showNotice('error', err.message || err.toString());
showNotice("error", err.message || err.toString());
}
setOpen(false);
} catch (err: any) {
showNotice('error', err.message || err.toString());
showNotice("error", err.message || err.toString());
}
});

View File

@@ -1,5 +1,11 @@
import useSWR from "swr";
import { forwardRef, useImperativeHandle, useState, useMemo, useEffect } from "react";
import {
forwardRef,
useImperativeHandle,
useState,
useMemo,
useEffect,
} from "react";
import { useLockFn } from "ahooks";
import { Box, LinearProgress, Button } from "@mui/material";
import { useTranslation } from "react-i18next";
@@ -18,7 +24,8 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [currentProgressListener, setCurrentProgressListener] = useState<UnlistenFn | null>(null);
const [currentProgressListener, setCurrentProgressListener] =
useState<UnlistenFn | null>(null);
const updateState = useUpdateState();
const setUpdateState = useSetUpdateState();
@@ -55,12 +62,12 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
const onUpdate = useLockFn(async () => {
if (portableFlag) {
showNotice('error', t("Portable Updater Error"));
showNotice("error", t("Portable Updater Error"));
return;
}
if (!updateInfo?.body) return;
if (breakChangeFlag) {
showNotice('error', t("Break Change Update Error"));
showNotice("error", t("Break Change Update Error"));
return;
}
if (updateState) return;
@@ -86,7 +93,7 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
await updateInfo.downloadAndInstall();
await relaunch();
} catch (err: any) {
showNotice('error', err?.message || err.toString());
showNotice("error", err?.message || err.toString());
} finally {
setUpdateState(false);
if (progressListener) {

View File

@@ -7,10 +7,7 @@ import { updateGeoData } from "@/services/api";
import { invoke_uwp_tool } from "@/services/cmds";
import { showNotice } from "@/services/noticeService";
import getSystem from "@/utils/get-system";
import {
LanRounded,
SettingsRounded
} from "@mui/icons-material";
import { LanRounded, SettingsRounded } from "@mui/icons-material";
import { MenuItem, Select, TextField, Typography } from "@mui/material";
import { invoke } from "@tauri-apps/api/core";
import { useLockFn } from "ahooks";
@@ -71,9 +68,9 @@ const SettingClash = ({ onError }: Props) => {
const onUpdateGeo = async () => {
try {
await updateGeoData();
showNotice('success', t("GeoData Updated"));
showNotice("success", t("GeoData Updated"));
} catch (err: any) {
showNotice('error', err?.response.data.message || err.toString());
showNotice("error", err?.response.data.message || err.toString());
}
};
@@ -90,7 +87,7 @@ const SettingClash = ({ onError }: Props) => {
} catch (err: any) {
setDnsSettingsEnabled(!enable);
localStorage.setItem("dns_settings_enabled", String(!enable));
showNotice('error', err.message || err.toString());
showNotice("error", err.message || err.toString());
await patchVerge({ enable_dns_settings: !enable }).catch(() => {});
throw err;
}
@@ -203,9 +200,7 @@ const SettingClash = ({ onError }: Props) => {
</GuardState>
</SettingItem>
<SettingItem
label={t("Port Config")}
>
<SettingItem label={t("Port Config")}>
<TextField
autoComplete="new-password"
disabled={false}
@@ -225,7 +220,9 @@ const SettingClash = ({ onError }: Props) => {
<>
{t("External")}
<TooltipIcon
title={t("Enable one-click random API port and key. Click to randomize the port and key")}
title={t(
"Enable one-click random API port and key. Click to randomize the port and key",
)}
sx={{ opacity: "0.7" }}
/>
</>

View File

@@ -47,18 +47,18 @@ const SettingVergeAdvanced = ({ onError }: Props) => {
try {
const info = await checkUpdate();
if (!info?.available) {
showNotice('success', t("Currently on the Latest Version"));
showNotice("success", t("Currently on the Latest Version"));
} else {
updateRef.current?.open();
}
} catch (err: any) {
showNotice('error', err.message || err.toString());
showNotice("error", err.message || err.toString());
}
};
const onExportDiagnosticInfo = useCallback(async () => {
await exportDiagnosticInfo();
showNotice('success', t("Copy Success"), 1000);
showNotice("success", t("Copy Success"), 1000);
}, []);
return (
@@ -110,7 +110,10 @@ const SettingVergeAdvanced = ({ onError }: Props) => {
<SettingItem
label={t("LightWeight Mode Settings")}
extra={
<TooltipIcon title={t("LightWeight Mode Info")} sx={{ opacity: "0.7" }} />
<TooltipIcon
title={t("LightWeight Mode Info")}
sx={{ opacity: "0.7" }}
/>
}
onClick={() => liteModeRef.current?.open()}
/>

View File

@@ -69,7 +69,7 @@ const SettingVergeBasic = ({ onError }: Props) => {
const onCopyClashEnv = useCallback(async () => {
await copyClashEnv();
showNotice('success', t("Copy Success"), 1000);
showNotice("success", t("Copy Success"), 1000);
}, []);
return (

View File

@@ -246,7 +246,10 @@ const ProxyControlSwitches = ({ label, onError }: ProxySwitchProps) => {
onFormat={onSwitchFormat}
onChange={(e) => {
if (isSidecarMode) {
showNotice('error', t("TUN requires Service Mode or Admin Mode"));
showNotice(
"error",
t("TUN requires Service Mode or Admin Mode"),
);
return Promise.reject(
new Error(t("TUN requires Service Mode or Admin Mode")),
);
@@ -255,7 +258,10 @@ const ProxyControlSwitches = ({ label, onError }: ProxySwitchProps) => {
}}
onGuard={(e) => {
if (isSidecarMode) {
showNotice('error', t("TUN requires Service Mode or Admin Mode"));
showNotice(
"error",
t("TUN requires Service Mode or Admin Mode"),
);
return Promise.reject(
new Error(t("TUN requires Service Mode or Admin Mode")),
);

View File

@@ -72,7 +72,7 @@ export const TestItem = (props: Props) => {
try {
onDeleteItem(uid);
} catch (err: any) {
showNotice('error', err.message || err.toString());
showNotice("error", err.message || err.toString());
}
});
@@ -97,7 +97,9 @@ export const TestItem = (props: Props) => {
return () => {
if (unlistenFn) {
console.log(`TestItem for ${props.id} unmounting or url changed, cleaning up test-all listener.`);
console.log(
`TestItem for ${props.id} unmounting or url changed, cleaning up test-all listener.`,
);
unlistenFn();
}
};

View File

@@ -100,7 +100,7 @@ export const TestViewer = forwardRef<TestViewerRef, Props>((props, ref) => {
setLoading(false);
setTimeout(() => formIns.reset(), 500);
} catch (err: any) {
showNotice('error', err.message || err.toString());
showNotice("error", err.message || err.toString());
setLoading(false);
}
}),

View File

@@ -9,7 +9,7 @@ import { getProxies, updateProxy } from "@/services/api";
export const useProfiles = () => {
const { data: profiles, mutate: mutateProfiles } = useSWR(
"getProfiles",
getProfiles
getProfiles,
);
const patchProfiles = async (value: Partial<IProfilesConfig>) => {
@@ -32,7 +32,7 @@ export const useProfiles = () => {
if (!profileData || !proxiesData) return;
const current = profileData.items?.find(
(e) => e && e.uid === profileData.current
(e) => e && e.uid === profileData.current,
);
if (!current) return;
@@ -40,7 +40,7 @@ export const useProfiles = () => {
// init selected array
const { selected = [] } = current;
const selectedMap = Object.fromEntries(
selected.map((each) => [each.name!, each.now!])
selected.map((each) => [each.name!, each.now!]),
);
let hasChange = false;

View File

@@ -1,7 +1,11 @@
import { useTranslation } from "react-i18next";
import { useLockFn } from "ahooks";
import { showNotice } from "@/services/noticeService";
import { installService, isServiceAvailable, restartCore } from "@/services/cmds";
import {
installService,
isServiceAvailable,
restartCore,
} from "@/services/cmds";
import { useSystemState } from "@/hooks/use-system-state";
import { mutate } from "swr";
@@ -11,16 +15,16 @@ export function useServiceInstaller() {
const installServiceAndRestartCore = useLockFn(async () => {
try {
showNotice('info', t("Installing Service..."));
showNotice("info", t("Installing Service..."));
await installService();
showNotice('success', t("Service Installed Successfully"));
showNotice("success", t("Service Installed Successfully"));
showNotice('info', t("Waiting for service to be ready..."));
showNotice("info", t("Waiting for service to be ready..."));
let serviceReady = false;
for (let i = 0; i < 5; i++) {
try {
// 等待1秒再检查
await new Promise(resolve => setTimeout(resolve, 1000));
await new Promise((resolve) => setTimeout(resolve, 1000));
const isAvailable = await isServiceAvailable();
if (isAvailable) {
serviceReady = true;
@@ -29,52 +33,86 @@ export function useServiceInstaller() {
}
// 最后一次尝试不显示重试信息
if (i < 4) {
showNotice('info', t("Service not ready, retrying attempt {count}/{total}...", { count: i + 1, total: 5 }));
showNotice(
"info",
t("Service not ready, retrying attempt {count}/{total}...", {
count: i + 1,
total: 5,
}),
);
}
} catch (error) {
console.error(t("Error checking service status:"), error);
if (i < 4) {
showNotice('error', t("Failed to check service status, retrying attempt {count}/{total}...", { count: i + 1, total: 5 }));
showNotice(
"error",
t(
"Failed to check service status, retrying attempt {count}/{total}...",
{ count: i + 1, total: 5 },
),
);
}
}
}
if (!serviceReady) {
showNotice('info', t("Service did not become ready after attempts. Proceeding with core restart."));
showNotice(
"info",
t(
"Service did not become ready after attempts. Proceeding with core restart.",
),
);
}
showNotice('info', t("Restarting Core..."));
showNotice("info", t("Restarting Core..."));
await restartCore();
// 核心重启后,再次确认并更新相关状态
await mutateRunningMode();
await mutateRunningMode();
const finalServiceStatus = await isServiceAvailable();
mutate("isServiceAvailable", finalServiceStatus, false);
mutate("isServiceAvailable", finalServiceStatus, false);
if (serviceReady && finalServiceStatus) {
showNotice('success', t("Service is ready and core restarted"));
showNotice("success", t("Service is ready and core restarted"));
} else if (finalServiceStatus) {
showNotice('success', t("Core restarted. Service is now available."));
showNotice("success", t("Core restarted. Service is now available."));
} else if (serviceReady) {
showNotice('info', t("Service was ready, but core restart might have issues or service became unavailable. Please check."));
showNotice(
"info",
t(
"Service was ready, but core restart might have issues or service became unavailable. Please check.",
),
);
} else {
showNotice('error', t("Service installation or core restart encountered issues. Service might not be available. Please check system logs."));
showNotice(
"error",
t(
"Service installation or core restart encountered issues. Service might not be available. Please check system logs.",
),
);
}
return finalServiceStatus;
} catch (err: any) {
showNotice('error', err.message || err.toString());
showNotice("error", err.message || err.toString());
// 尝试性回退或最终操作
try {
showNotice('info', t("Attempting to restart core as a fallback..."));
showNotice("info", t("Attempting to restart core as a fallback..."));
await restartCore();
await mutateRunningMode();
await isServiceAvailable().then(status => mutate("isServiceAvailable", status, false));
await isServiceAvailable().then((status) =>
mutate("isServiceAvailable", status, false),
);
} catch (recoveryError: any) {
showNotice('error', t("Fallback core restart also failed: {message}", { message: recoveryError.message }));
showNotice(
"error",
t("Fallback core restart also failed: {message}", {
message: recoveryError.message,
}),
);
}
return false;
}
});
return { installServiceAndRestartCore };
}
}

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />

View File

@@ -559,4 +559,4 @@
"Port Config": "Port-Konfiguration",
"Configuration saved successfully": "Zufalls-Konfiguration erfolgreich gespeichert",
"Enable one-click random API port and key. Click to randomize the port and key": "Einstellsichere Zufalls-API-Port- und Schlüsselgenerierung aktivieren. Klicken Sie, um Port und Schlüssel zu randomisieren"
}
}

View File

@@ -559,4 +559,4 @@
"Port Config": "Configuración de puerto",
"Configuration saved successfully": "Configuración aleatoria guardada correctamente",
"Enable one-click random API port and key. Click to randomize the port and key": "Habilitar la generación de puerto y clave API aleatorios con un solo clic. Haz clic para randomizar el puerto y la clave"
}
}

View File

@@ -562,4 +562,4 @@
"Port Config": "ポート設定",
"Configuration saved successfully": "ランダム設定を保存完了",
"Enable one-click random API port and key. Click to randomize the port and key": "ワンクリックでランダム API ポートとキーを有効化。ポートとキーをランダム化するにはクリックしてください"
}
}

View File

@@ -399,4 +399,4 @@
"View Profile-Merge": "프로필-병합 보기",
"Update Successful": "업데이트 성공",
"Update Failed": "업데이트 실패"
}
}

View File

@@ -562,4 +562,4 @@
"Port Config": "端口設置",
"Configuration saved successfully": "配置保存完成",
"Enable one-click random API port and key. Click to randomize the port and key": "開啟一鍵隨機 API 端口和密鑰,點進去就可以隨機端口和密鑰"
}
}

View File

@@ -26,7 +26,7 @@ const container = document.getElementById(mainElementId);
if (!container) {
throw new Error(
`No container '${mainElementId}' found to render application`
`No container '${mainElementId}' found to render application`,
);
}
@@ -37,7 +37,7 @@ document.addEventListener("keydown", (event) => {
(event.altKey && ["ArrowLeft", "ArrowRight"].includes(event.key)) ||
((event.ctrlKey || event.metaKey) &&
["F", "G", "H", "J", "P", "Q", "R", "U"].includes(
event.key.toUpperCase()
event.key.toUpperCase(),
));
disabledShortcuts && event.preventDefault();
});
@@ -59,5 +59,5 @@ createRoot(container).render(
</AppDataProvider>
</BaseErrorBoundary>
</ComposeContextProvider>
</React.StrictMode>
</React.StrictMode>,
);

View File

@@ -50,86 +50,95 @@ const handleNoticeMessage = (
switch (status) {
case "import_sub_url::ok":
navigate("/profile", { state: { current: msg } });
showNotice('success', t("Import Subscription Successful"));
showNotice("success", t("Import Subscription Successful"));
break;
case "import_sub_url::error":
navigate("/profile");
showNotice('error', msg);
showNotice("error", msg);
break;
case "set_config::error":
showNotice('error', msg);
showNotice("error", msg);
break;
case "update_with_clash_proxy":
showNotice('success', `${t("Update with Clash proxy successfully")} ${msg}`);
showNotice(
"success",
`${t("Update with Clash proxy successfully")} ${msg}`,
);
break;
case "update_retry_with_clash":
showNotice('info', t("Update failed, retrying with Clash proxy..."));
showNotice("info", t("Update failed, retrying with Clash proxy..."));
break;
case "update_failed_even_with_clash":
showNotice('error', `${t("Update failed even with Clash proxy")}: ${msg}`);
showNotice(
"error",
`${t("Update failed even with Clash proxy")}: ${msg}`,
);
break;
case "update_failed":
showNotice('error', msg);
showNotice("error", msg);
break;
case "config_validate::boot_error":
showNotice('error', `${t("Boot Config Validation Failed")} ${msg}`);
showNotice("error", `${t("Boot Config Validation Failed")} ${msg}`);
break;
case "config_validate::core_change":
showNotice('error', `${t("Core Change Config Validation Failed")} ${msg}`);
showNotice(
"error",
`${t("Core Change Config Validation Failed")} ${msg}`,
);
break;
case "config_validate::error":
showNotice('error', `${t("Config Validation Failed")} ${msg}`);
showNotice("error", `${t("Config Validation Failed")} ${msg}`);
break;
case "config_validate::process_terminated":
showNotice('error', t("Config Validation Process Terminated"));
showNotice("error", t("Config Validation Process Terminated"));
break;
case "config_validate::stdout_error":
showNotice('error', `${t("Config Validation Failed")} ${msg}`);
showNotice("error", `${t("Config Validation Failed")} ${msg}`);
break;
case "config_validate::script_error":
showNotice('error', `${t("Script File Error")} ${msg}`);
showNotice("error", `${t("Script File Error")} ${msg}`);
break;
case "config_validate::script_syntax_error":
showNotice('error', `${t("Script Syntax Error")} ${msg}`);
showNotice("error", `${t("Script Syntax Error")} ${msg}`);
break;
case "config_validate::script_missing_main":
showNotice('error', `${t("Script Missing Main")} ${msg}`);
showNotice("error", `${t("Script Missing Main")} ${msg}`);
break;
case "config_validate::file_not_found":
showNotice('error', `${t("File Not Found")} ${msg}`);
showNotice("error", `${t("File Not Found")} ${msg}`);
break;
case "config_validate::yaml_syntax_error":
showNotice('error', `${t("YAML Syntax Error")} ${msg}`);
showNotice("error", `${t("YAML Syntax Error")} ${msg}`);
break;
case "config_validate::yaml_read_error":
showNotice('error', `${t("YAML Read Error")} ${msg}`);
showNotice("error", `${t("YAML Read Error")} ${msg}`);
break;
case "config_validate::yaml_mapping_error":
showNotice('error', `${t("YAML Mapping Error")} ${msg}`);
showNotice("error", `${t("YAML Mapping Error")} ${msg}`);
break;
case "config_validate::yaml_key_error":
showNotice('error', `${t("YAML Key Error")} ${msg}`);
showNotice("error", `${t("YAML Key Error")} ${msg}`);
break;
case "config_validate::yaml_error":
showNotice('error', `${t("YAML Error")} ${msg}`);
showNotice("error", `${t("YAML Error")} ${msg}`);
break;
case "config_validate::merge_syntax_error":
showNotice('error', `${t("Merge File Syntax Error")} ${msg}`);
showNotice("error", `${t("Merge File Syntax Error")} ${msg}`);
break;
case "config_validate::merge_mapping_error":
showNotice('error', `${t("Merge File Mapping Error")} ${msg}`);
showNotice("error", `${t("Merge File Mapping Error")} ${msg}`);
break;
case "config_validate::merge_key_error":
showNotice('error', `${t("Merge File Key Error")} ${msg}`);
showNotice("error", `${t("Merge File Key Error")} ${msg}`);
break;
case "config_validate::merge_error":
showNotice('error', `${t("Merge File Error")} ${msg}`);
showNotice("error", `${t("Merge File Error")} ${msg}`);
break;
case "config_core::change_success":
showNotice('success', `${t("Core Changed Successfully")}: ${msg}`);
showNotice("success", `${t("Core Changed Successfully")}: ${msg}`);
break;
case "config_core::change_error":
showNotice('error', `${t("Failed to Change Core")}: ${msg}`);
showNotice("error", `${t("Failed to Change Core")}: ${msg}`);
break;
default: // Optional: Log unhandled statuses
console.warn(`[通知监听 V2] 未处理的状态: ${status}`);
@@ -190,7 +199,6 @@ const Layout = () => {
mutate("getAutotemProxy");
}),
addListener("verge://notice-message", ({ payload }) =>
handleNotice(payload as [string, string]),
),
@@ -276,7 +284,7 @@ const Layout = () => {
return unlisten;
} catch (err) {
console.error("[Layout] 监听启动完成事件失败:", err);
return () => { };
return () => {};
}
};
@@ -298,7 +306,7 @@ const Layout = () => {
const unlistenPromise = listenStartupCompleted();
return () => {
unlistenPromise.then(unlisten => unlisten());
unlistenPromise.then((unlisten) => unlisten());
};
}, []);
@@ -368,11 +376,11 @@ const Layout = () => {
({ palette }) => ({ bgcolor: palette.background.paper }),
OS === "linux"
? {
borderRadius: "8px",
border: "1px solid var(--divider-color)",
width: "calc(100vw - 4px)",
height: "calc(100vh - 4px)",
}
borderRadius: "8px",
border: "1px solid var(--divider-color)",
width: "calc(100vw - 4px)",
height: "calc(100vh - 4px)",
}
: {},
]}
>
@@ -420,8 +428,7 @@ const Layout = () => {
</div>
<div className="layout__right">
<div className="the-bar">
</div>
<div className="the-bar"></div>
<div className="the-content">
{React.cloneElement(routersEles, { key: location.pathname })}

View File

@@ -43,7 +43,7 @@ const ConnectionsPage = () => {
const isDark = theme.palette.mode === "dark";
const [match, setMatch] = useState(() => (_: string) => true);
const [curOrderOpt, setOrderOpt] = useState("Default");
// 使用全局数据
const { connections } = useAppData();
@@ -69,19 +69,21 @@ const ConnectionsPage = () => {
// 使用全局连接数据
const displayData = useMemo(() => {
if (!pageVisible) return initConn;
if (isPaused) {
return frozenData ?? {
uploadTotal: connections.uploadTotal,
downloadTotal: connections.downloadTotal,
connections: connections.data
};
return (
frozenData ?? {
uploadTotal: connections.uploadTotal,
downloadTotal: connections.downloadTotal,
connections: connections.data,
}
);
}
return {
uploadTotal: connections.uploadTotal,
downloadTotal: connections.downloadTotal,
connections: connections.data
connections: connections.data,
};
}, [isPaused, frozenData, connections, pageVisible]);
@@ -113,7 +115,7 @@ const ConnectionsPage = () => {
setFrozenData({
uploadTotal: connections.uploadTotal,
downloadTotal: connections.downloadTotal,
connections: connections.data
connections: connections.data,
});
} else {
setFrozenData(null);

View File

@@ -398,4 +398,4 @@ const ClashModeEnhancedCard = () => {
);
};
export default HomePage;
export default HomePage;

View File

@@ -80,7 +80,7 @@ const ProfilePage = () => {
for (let file of paths) {
if (!file.endsWith(".yaml") && !file.endsWith(".yml")) {
showNotice('error', t("Only YAML Files Supported"));
showNotice("error", t("Only YAML Files Supported"));
continue;
}
const item = {
@@ -145,31 +145,34 @@ const ProfilePage = () => {
try {
// 尝试正常导入
await importProfile(url);
showNotice('success', t("Profile Imported Successfully"));
showNotice("success", t("Profile Imported Successfully"));
setUrl("");
mutateProfiles();
await onEnhance(false);
} catch (err: any) {
// 首次导入失败,尝试使用自身代理
const errmsg = err.message || err.toString();
showNotice('info', t("Import failed, retrying with Clash proxy..."));
showNotice("info", t("Import failed, retrying with Clash proxy..."));
try {
// 使用自身代理尝试导入
await importProfile(url, {
with_proxy: false,
self_proxy: true
self_proxy: true,
});
// 回退导入成功
showNotice('success', t("Profile Imported with Clash proxy"));
showNotice("success", t("Profile Imported with Clash proxy"));
setUrl("");
mutateProfiles();
await onEnhance(false);
} catch (retryErr: any) {
// 回退导入也失败
const retryErrmsg = retryErr?.message || retryErr.toString();
showNotice('error', `${t("Import failed even with Clash proxy")}: ${retryErrmsg}`);
showNotice(
"error",
`${t("Import failed even with Clash proxy")}: ${retryErrmsg}`,
);
}
} finally {
setDisabled(false);
@@ -199,10 +202,10 @@ const ProfilePage = () => {
closeAllConnections();
await activateSelected();
if (notifySuccess && success) {
showNotice('success', t("Profile Switched"), 1000);
showNotice("success", t("Profile Switched"), 1000);
}
} catch (err: any) {
showNotice('error', err?.message || err.toString(), 4000);
showNotice("error", err?.message || err.toString(), 4000);
} finally {
clearTimeout(reset);
setActivatings([]);
@@ -228,10 +231,10 @@ const ProfilePage = () => {
await enhanceProfiles();
mutateLogs();
if (notifySuccess) {
showNotice('success', t("Profile Reactivated"), 1000);
showNotice("success", t("Profile Reactivated"), 1000);
}
} catch (err: any) {
showNotice('error', err.message || err.toString(), 3000);
showNotice("error", err.message || err.toString(), 3000);
} finally {
setActivatings([]);
}
@@ -246,7 +249,7 @@ const ProfilePage = () => {
mutateLogs();
current && (await onEnhance(false));
} catch (err: any) {
showNotice('error', err?.message || err.toString());
showNotice("error", err?.message || err.toString());
} finally {
setActivatings([]);
}
@@ -300,12 +303,12 @@ const ProfilePage = () => {
let timeoutId: ReturnType<typeof setTimeout> | undefined;
const setupListener = async () => {
unlistenPromise = listen<string>('profile-changed', (event) => {
console.log('Profile changed event received:', event.payload);
unlistenPromise = listen<string>("profile-changed", (event) => {
console.log("Profile changed event received:", event.payload);
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
mutateProfiles();
timeoutId = undefined;
@@ -319,7 +322,7 @@ const ProfilePage = () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
unlistenPromise?.then(unlisten => unlisten());
unlistenPromise?.then((unlisten) => unlisten());
};
}, [mutateProfiles, t]);
@@ -398,7 +401,7 @@ const ProfilePage = () => {
<ClearRounded fontSize="inherit" />
</IconButton>
),
}
},
}}
/>
<LoadingButton

View File

@@ -20,8 +20,8 @@ const ProxyPage = () => {
revalidateOnFocus: false,
revalidateIfStale: true,
dedupingInterval: 1000,
errorRetryInterval: 5000
}
errorRetryInterval: 5000,
},
);
const { verge } = useVerge();

View File

@@ -15,7 +15,7 @@ const SettingPage = () => {
const { t } = useTranslation();
const onError = (err: any) => {
showNotice('error', err?.message || err.toString());
showNotice("error", err?.message || err.toString());
};
const toGithubRepo = useLockFn(() => {

View File

@@ -130,7 +130,9 @@ const UnlockPage = () => {
): Promise<T> => {
return Promise.race([
invoke<T>(cmd, args),
new Promise<T>((_, reject) => setTimeout(() => reject(new Error("Timeout")), timeout)),
new Promise<T>((_, reject) =>
setTimeout(() => reject(new Error("Timeout")), timeout),
),
]);
};
@@ -138,7 +140,8 @@ const UnlockPage = () => {
const checkAllMedia = useLockFn(async () => {
try {
setIsCheckingAll(true);
const result = await invokeWithTimeout<UnlockItem[]>("check_media_unlock");
const result =
await invokeWithTimeout<UnlockItem[]>("check_media_unlock");
const sortedItems = sortItemsByName(result);
setUnlockItems(sortedItems);
@@ -150,7 +153,7 @@ const UnlockPage = () => {
setIsCheckingAll(false);
} catch (err: any) {
setIsCheckingAll(false);
showNotice('error', err?.message || err?.toString() || '检测超时或失败');
showNotice("error", err?.message || err?.toString() || "检测超时或失败");
alert("检测超时或失败: " + (err?.message || err));
console.error("Failed to check media unlock:", err);
}
@@ -160,7 +163,8 @@ const UnlockPage = () => {
const checkSingleMedia = useLockFn(async (name: string) => {
try {
setLoadingItems((prev) => [...prev, name]);
const result = await invokeWithTimeout<UnlockItem[]>("check_media_unlock");
const result =
await invokeWithTimeout<UnlockItem[]>("check_media_unlock");
const targetItem = result.find((item: UnlockItem) => item.name === name);
@@ -181,7 +185,7 @@ const UnlockPage = () => {
setLoadingItems((prev) => prev.filter((item) => item !== name));
} catch (err: any) {
setLoadingItems((prev) => prev.filter((item) => item !== name));
showNotice('error', err?.message || err?.toString() || `检测${name}失败`);
showNotice("error", err?.message || err?.toString() || `检测${name}失败`);
alert("检测超时或失败: " + (err?.message || err));
console.error(`Failed to check ${name}:`, err);
}

View File

@@ -23,7 +23,7 @@
if (eventType !== "change" || typeof listener !== "function") {
console.error(
"Invalid arguments for removeEventListener:",
arguments
arguments,
);
return;
}

View File

@@ -1,7 +1,13 @@
import { createContext, useContext, useMemo } from "react";
import useSWR from "swr";
import useSWRSubscription from "swr/subscription";
import { getProxies, getRules, getClashConfig, getProxyProviders, getRuleProviders } from "@/services/api";
import {
getProxies,
getRules,
getClashConfig,
getProxyProviders,
getRuleProviders,
} from "@/services/api";
import { getSystemProxy, getRunningMode, getAppUptime } from "@/services/cmds";
import { useClashInfo } from "@/hooks/use-clash";
import { createAuthSockette } from "@/utils/websocket";
@@ -23,8 +29,8 @@ interface AppDataContextType {
uploadTotal: number;
downloadTotal: number;
};
traffic: {up: number; down: number};
memory: {inuse: number};
traffic: { up: number; down: number };
memory: { inuse: number };
refreshProxy: () => Promise<any>;
refreshClashConfig: () => Promise<any>;
refreshRules: () => Promise<any>;
@@ -38,33 +44,37 @@ interface AppDataContextType {
const AppDataContext = createContext<AppDataContextType | null>(null);
// 全局数据提供者组件
export const AppDataProvider = ({ children }: { children: React.ReactNode }) => {
export const AppDataProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const { clashInfo } = useClashInfo();
const pageVisible = useVisibility();
// 基础数据 - 中频率更新 (5秒)
const { data: proxiesData, mutate: refreshProxy } = useSWR(
"getProxies",
getProxies,
{
refreshInterval: 5000,
"getProxies",
getProxies,
{
refreshInterval: 5000,
revalidateOnFocus: false,
suspense: false,
errorRetryCount: 3
}
errorRetryCount: 3,
},
);
const { data: clashConfig, mutate: refreshClashConfig } = useSWR(
"getClashConfig",
getClashConfig,
{
refreshInterval: 5000,
"getClashConfig",
getClashConfig,
{
refreshInterval: 5000,
revalidateOnFocus: false,
suspense: false,
errorRetryCount: 3
}
errorRetryCount: 3,
},
);
// 提供者数据
const { data: proxyProviders, mutate: refreshProxyProviders } = useSWR(
"getProxyProviders",
@@ -74,219 +84,291 @@ export const AppDataProvider = ({ children }: { children: React.ReactNode }) =>
revalidateOnReconnect: false,
dedupingInterval: 3000,
suspense: false,
errorRetryCount: 3
}
errorRetryCount: 3,
},
);
const { data: ruleProviders, mutate: refreshRuleProviders } = useSWR(
"getRuleProviders",
getRuleProviders,
{
revalidateOnFocus: false,
suspense: false,
errorRetryCount: 3
}
errorRetryCount: 3,
},
);
// 低频率更新数据
const { data: rulesData, mutate: refreshRules } = useSWR(
"getRules",
"getRules",
getRules,
{
{
revalidateOnFocus: false,
suspense: false,
errorRetryCount: 3
}
errorRetryCount: 3,
},
);
const { data: sysproxy, mutate: refreshSysproxy } = useSWR(
"getSystemProxy",
"getSystemProxy",
getSystemProxy,
{
{
revalidateOnFocus: false,
suspense: false,
errorRetryCount: 3
}
errorRetryCount: 3,
},
);
const { data: runningMode } = useSWR(
"getRunningMode",
getRunningMode,
{
revalidateOnFocus: false,
suspense: false,
errorRetryCount: 3
}
);
const { data: runningMode } = useSWR("getRunningMode", getRunningMode, {
revalidateOnFocus: false,
suspense: false,
errorRetryCount: 3,
});
// 高频率更新数据 (2秒)
const { data: uptimeData } = useSWR(
"appUptime",
getAppUptime,
{
refreshInterval: 2000,
revalidateOnFocus: false,
suspense: false
}
);
const { data: uptimeData } = useSWR("appUptime", getAppUptime, {
refreshInterval: 2000,
revalidateOnFocus: false,
suspense: false,
});
// 连接数据 - 使用WebSocket实时更新
const { data: connectionsData = { connections: [], uploadTotal: 0, downloadTotal: 0 } } =
useSWRSubscription(
clashInfo && pageVisible ? "connections" : null,
(_key, { next }) => {
if (!clashInfo || !pageVisible) return () => {};
const { server = "", secret = "" } = clashInfo;
if (!server) return () => {};
console.log(`[Connections][${AppDataProvider.name}] 正在连接: ${server}/connections`);
const socket = createAuthSockette(`${server}/connections`, secret, {
timeout: 5000,
onmessage(event) {
try {
const data = JSON.parse(event.data);
// 处理连接数据,计算当前上传下载速度
next(null, (prev: any = { connections: [], uploadTotal: 0, downloadTotal: 0 }) => {
const {
data: connectionsData = {
connections: [],
uploadTotal: 0,
downloadTotal: 0,
},
} = useSWRSubscription(
clashInfo && pageVisible ? "connections" : null,
(_key, { next }) => {
if (!clashInfo || !pageVisible) return () => {};
const { server = "", secret = "" } = clashInfo;
if (!server) return () => {};
console.log(
`[Connections][${AppDataProvider.name}] 正在连接: ${server}/connections`,
);
const socket = createAuthSockette(`${server}/connections`, secret, {
timeout: 5000,
onmessage(event) {
try {
const data = JSON.parse(event.data);
// 处理连接数据,计算当前上传下载速度
next(
null,
(
prev: any = {
connections: [],
uploadTotal: 0,
downloadTotal: 0,
},
) => {
const oldConns = prev.connections || [];
const newConns = data.connections || [];
// 计算当前速度
const processedConns = newConns.map((conn: any) => {
const oldConn = oldConns.find((old: any) => old.id === conn.id);
const oldConn = oldConns.find(
(old: any) => old.id === conn.id,
);
if (oldConn) {
return {
...conn,
curUpload: conn.upload - oldConn.upload,
curDownload: conn.download - oldConn.download
curDownload: conn.download - oldConn.download,
};
}
return { ...conn, curUpload: 0, curDownload: 0 };
});
return {
...data,
connections: processedConns
connections: processedConns,
};
});
} catch (err) {
console.error(`[Connections][${AppDataProvider.name}] 解析数据错误:`, err, event.data);
}
},
onopen: (event) => {
console.log(`[Connections][${AppDataProvider.name}] WebSocket 连接已建立`, event);
},
onerror(event) {
console.error(`[Connections][${AppDataProvider.name}] WebSocket 连接错误或达到最大重试次数`, event);
next(null, { connections: [], uploadTotal: 0, downloadTotal: 0 });
},
onclose: (event) => {
console.log(`[Connections][${AppDataProvider.name}] WebSocket 连接关闭`, event.code, event.reason);
if (event.code !== 1000 && event.code !== 1001) {
console.warn(`[Connections][${AppDataProvider.name}] 连接非正常关闭,重置数据`);
next(null, { connections: [], uploadTotal: 0, downloadTotal: 0 });
}
},
);
} catch (err) {
console.error(
`[Connections][${AppDataProvider.name}] 解析数据错误:`,
err,
event.data,
);
}
});
return () => {
console.log(`[Connections][${AppDataProvider.name}] 清理WebSocket连接`);
socket.close();
};
}
);
},
onopen: (event) => {
console.log(
`[Connections][${AppDataProvider.name}] WebSocket 连接已建立`,
event,
);
},
onerror(event) {
console.error(
`[Connections][${AppDataProvider.name}] WebSocket 连接错误或达到最大重试次数`,
event,
);
next(null, { connections: [], uploadTotal: 0, downloadTotal: 0 });
},
onclose: (event) => {
console.log(
`[Connections][${AppDataProvider.name}] WebSocket 连接关闭`,
event.code,
event.reason,
);
if (event.code !== 1000 && event.code !== 1001) {
console.warn(
`[Connections][${AppDataProvider.name}] 连接非正常关闭,重置数据`,
);
next(null, { connections: [], uploadTotal: 0, downloadTotal: 0 });
}
},
});
return () => {
console.log(`[Connections][${AppDataProvider.name}] 清理WebSocket连接`);
socket.close();
};
},
);
// 流量和内存数据 - 通过WebSocket获取实时流量数据
const { data: trafficData = { up: 0, down: 0 } } = useSWRSubscription(
clashInfo && pageVisible ? "traffic" : null,
(_key, { next }) => {
if (!clashInfo || !pageVisible) return () => {};
const { server = "", secret = "" } = clashInfo;
if (!server) return () => {};
console.log(`[Traffic][${AppDataProvider.name}] 正在连接: ${server}/traffic`);
console.log(
`[Traffic][${AppDataProvider.name}] 正在连接: ${server}/traffic`,
);
const socket = createAuthSockette(`${server}/traffic`, secret, {
onmessage(event) {
try {
const data = JSON.parse(event.data);
if (data && typeof data.up === 'number' && typeof data.down === 'number') {
if (
data &&
typeof data.up === "number" &&
typeof data.down === "number"
) {
next(null, data);
} else {
console.warn(`[Traffic][${AppDataProvider.name}] 收到无效数据:`, data);
console.warn(
`[Traffic][${AppDataProvider.name}] 收到无效数据:`,
data,
);
}
} catch (err) {
console.error(`[Traffic][${AppDataProvider.name}] 解析数据错误:`, err, event.data);
console.error(
`[Traffic][${AppDataProvider.name}] 解析数据错误:`,
err,
event.data,
);
}
},
onopen: (event) => {
console.log(`[Traffic][${AppDataProvider.name}] WebSocket 连接已建立`, event);
console.log(
`[Traffic][${AppDataProvider.name}] WebSocket 连接已建立`,
event,
);
},
onerror(event) {
console.error(`[Traffic][${AppDataProvider.name}] WebSocket 连接错误或达到最大重试次数`, event);
console.error(
`[Traffic][${AppDataProvider.name}] WebSocket 连接错误或达到最大重试次数`,
event,
);
next(null, { up: 0, down: 0 });
},
onclose: (event) => {
console.log(`[Traffic][${AppDataProvider.name}] WebSocket 连接关闭`, event.code, event.reason);
if (event.code !== 1000 && event.code !== 1001) {
console.warn(`[Traffic][${AppDataProvider.name}] 连接非正常关闭,重置数据`);
next(null, { up: 0, down: 0 });
}
}
console.log(
`[Traffic][${AppDataProvider.name}] WebSocket 连接关闭`,
event.code,
event.reason,
);
if (event.code !== 1000 && event.code !== 1001) {
console.warn(
`[Traffic][${AppDataProvider.name}] 连接非正常关闭,重置数据`,
);
next(null, { up: 0, down: 0 });
}
},
});
return () => {
console.log(`[Traffic][${AppDataProvider.name}] 清理WebSocket连接`);
socket.close();
};
}
},
);
const { data: memoryData = { inuse: 0 } } = useSWRSubscription(
clashInfo && pageVisible ? "memory" : null,
(_key, { next }) => {
if (!clashInfo || !pageVisible) return () => {};
const { server = "", secret = "" } = clashInfo;
if (!server) return () => {};
console.log(`[Memory][${AppDataProvider.name}] 正在连接: ${server}/memory`);
console.log(
`[Memory][${AppDataProvider.name}] 正在连接: ${server}/memory`,
);
const socket = createAuthSockette(`${server}/memory`, secret, {
onmessage(event) {
try {
const data = JSON.parse(event.data);
if (data && typeof data.inuse === 'number') {
if (data && typeof data.inuse === "number") {
next(null, data);
} else {
console.warn(`[Memory][${AppDataProvider.name}] 收到无效数据:`, data);
console.warn(
`[Memory][${AppDataProvider.name}] 收到无效数据:`,
data,
);
}
} catch (err) {
console.error(`[Memory][${AppDataProvider.name}] 解析数据错误:`, err, event.data);
console.error(
`[Memory][${AppDataProvider.name}] 解析数据错误:`,
err,
event.data,
);
}
},
onopen: (event) => {
console.log(`[Memory][${AppDataProvider.name}] WebSocket 连接已建立`, event);
console.log(
`[Memory][${AppDataProvider.name}] WebSocket 连接已建立`,
event,
);
},
onerror(event) {
console.error(`[Memory][${AppDataProvider.name}] WebSocket 连接错误或达到最大重试次数`, event);
console.error(
`[Memory][${AppDataProvider.name}] WebSocket 连接错误或达到最大重试次数`,
event,
);
next(null, { inuse: 0 });
},
onclose: (event) => {
console.log(`[Memory][${AppDataProvider.name}] WebSocket 连接关闭`, event.code, event.reason);
console.log(
`[Memory][${AppDataProvider.name}] WebSocket 连接关闭`,
event.code,
event.reason,
);
if (event.code !== 1000 && event.code !== 1001) {
console.warn(`[Memory][${AppDataProvider.name}] 连接非正常关闭,重置数据`);
console.warn(
`[Memory][${AppDataProvider.name}] 连接非正常关闭,重置数据`,
);
next(null, { inuse: 0 });
}
}
},
});
return () => {
console.log(`[Memory][${AppDataProvider.name}] 清理WebSocket连接`);
socket.close();
};
}
},
);
// 提供统一的刷新方法
const refreshAll = async () => {
await Promise.all([
@@ -295,66 +377,79 @@ export const AppDataProvider = ({ children }: { children: React.ReactNode }) =>
refreshRules(),
refreshSysproxy(),
refreshProxyProviders(),
refreshRuleProviders()
refreshRuleProviders(),
]);
};
// 聚合所有数据
const value = useMemo(() => ({
// 数据
proxies: proxiesData,
clashConfig,
rules: rulesData || [],
sysproxy,
runningMode,
uptime: uptimeData || 0,
// 提供者数据
proxyProviders: proxyProviders || {},
ruleProviders: ruleProviders || {},
// 连接数据
connections: {
data: connectionsData.connections || [],
count: connectionsData.connections?.length || 0,
uploadTotal: connectionsData.uploadTotal || 0,
downloadTotal: connectionsData.downloadTotal || 0
},
// 实时流量数据
traffic: trafficData,
memory: memoryData,
// 刷新方法
refreshProxy,
refreshClashConfig,
refreshRules,
refreshSysproxy,
refreshProxyProviders,
refreshRuleProviders,
refreshAll
}), [
proxiesData, clashConfig, rulesData, sysproxy,
runningMode, uptimeData, connectionsData,
trafficData, memoryData, proxyProviders, ruleProviders,
refreshProxy, refreshClashConfig, refreshRules, refreshSysproxy,
refreshProxyProviders, refreshRuleProviders
]);
const value = useMemo(
() => ({
// 数据
proxies: proxiesData,
clashConfig,
rules: rulesData || [],
sysproxy,
runningMode,
uptime: uptimeData || 0,
// 提供者数据
proxyProviders: proxyProviders || {},
ruleProviders: ruleProviders || {},
// 连接数据
connections: {
data: connectionsData.connections || [],
count: connectionsData.connections?.length || 0,
uploadTotal: connectionsData.uploadTotal || 0,
downloadTotal: connectionsData.downloadTotal || 0,
},
// 实时流量数据
traffic: trafficData,
memory: memoryData,
// 刷新方法
refreshProxy,
refreshClashConfig,
refreshRules,
refreshSysproxy,
refreshProxyProviders,
refreshRuleProviders,
refreshAll,
}),
[
proxiesData,
clashConfig,
rulesData,
sysproxy,
runningMode,
uptimeData,
connectionsData,
trafficData,
memoryData,
proxyProviders,
ruleProviders,
refreshProxy,
refreshClashConfig,
refreshRules,
refreshSysproxy,
refreshProxyProviders,
refreshRuleProviders,
],
);
return (
<AppDataContext.Provider value={value}>
{children}
</AppDataContext.Provider>
<AppDataContext.Provider value={value}>{children}</AppDataContext.Provider>
);
};
// 自定义Hook访问全局数据
export const useAppData = () => {
const context = useContext(AppDataContext);
if (!context) {
throw new Error("useAppData必须在AppDataProvider内使用");
}
return context;
};
};

View File

@@ -199,10 +199,12 @@ export const getProxyProviders = async () => {
providers: Record<string, IProxyProviderItem>;
}>("get_providers_proxies");
if (!response || !response.providers) {
console.warn("getProxyProviders: Invalid response structure, returning empty object");
console.warn(
"getProxyProviders: Invalid response structure, returning empty object",
);
return {};
}
const providers = response.providers as Record<string, IProxyProviderItem>;
return Object.fromEntries(
@@ -351,65 +353,65 @@ const IP_CHECK_SERVICES: ServiceConfig[] = [
{
url: "https://api.ip.sb/geoip",
mapping: (data) => ({
ip: data.ip || '',
country_code: data.country_code || '',
country: data.country || '',
region: data.region || '',
city: data.city || '',
organization: data.organization || data.isp || '',
ip: data.ip || "",
country_code: data.country_code || "",
country: data.country || "",
region: data.region || "",
city: data.city || "",
organization: data.organization || data.isp || "",
asn: data.asn || 0,
asn_organization: data.asn_organization || '',
asn_organization: data.asn_organization || "",
longitude: data.longitude || 0,
latitude: data.latitude || 0,
timezone: data.timezone || '',
timezone: data.timezone || "",
}),
},
{
url: "https://ipapi.co/json",
mapping: (data) => ({
ip: data.ip || '',
country_code: data.country_code || '',
country: data.country_name || '',
region: data.region || '',
city: data.city || '',
organization: data.org || '',
asn: data.asn? parseInt(data.asn.replace('AS', '')) : 0,
asn_organization: data.org || '',
ip: data.ip || "",
country_code: data.country_code || "",
country: data.country_name || "",
region: data.region || "",
city: data.city || "",
organization: data.org || "",
asn: data.asn ? parseInt(data.asn.replace("AS", "")) : 0,
asn_organization: data.org || "",
longitude: data.longitude || 0,
latitude: data.latitude || 0,
timezone: data.timezone || '',
timezone: data.timezone || "",
}),
},
{
url: "https://api.ipapi.is/",
mapping: (data) => ({
ip: data.ip || '',
country_code: data.location?.country_code || '',
country: data.location?.country || '',
region: data.location?.state || '',
city: data.location?.city || '',
organization: data.asn?.org || data.company?.name || '',
ip: data.ip || "",
country_code: data.location?.country_code || "",
country: data.location?.country || "",
region: data.location?.state || "",
city: data.location?.city || "",
organization: data.asn?.org || data.company?.name || "",
asn: data.asn?.asn || 0,
asn_organization: data.asn?.org || '',
asn_organization: data.asn?.org || "",
longitude: data.location?.longitude || 0,
latitude: data.location?.latitude || 0,
timezone: data.location?.timezone || '',
timezone: data.location?.timezone || "",
}),
},
{
url: "https://ipwho.is/",
mapping: (data) => ({
ip: data.ip || '',
country_code: data.country_code || '',
country: data.country || '',
region: data.region || '',
city: data.city || '',
organization: data.connection?.org || data.connection?.isp || '',
ip: data.ip || "",
country_code: data.country_code || "",
country: data.country || "",
region: data.region || "",
city: data.city || "",
organization: data.connection?.org || data.connection?.isp || "",
asn: data.connection?.asn || 0,
asn_organization: data.connection?.isp || '',
asn_organization: data.connection?.isp || "",
longitude: data.longitude || 0,
latitude: data.latitude || 0,
timezone: data.timezone?.id || '',
timezone: data.timezone?.id || "",
}),
},
];
@@ -418,43 +420,39 @@ const IP_CHECK_SERVICES: ServiceConfig[] = [
function shuffleServices() {
// 过滤无效服务并确保每个元素符合ServiceConfig接口
const validServices = IP_CHECK_SERVICES.filter(
(service): service is ServiceConfig =>
service !== null &&
service !== undefined &&
typeof service.url === 'string' &&
typeof service.mapping === 'function' // 添加对mapping属性的检查
(service): service is ServiceConfig =>
service !== null &&
service !== undefined &&
typeof service.url === "string" &&
typeof service.mapping === "function", // 添加对mapping属性的检查
);
if (validServices.length === 0) {
console.error('No valid services found in IP_CHECK_SERVICES');
console.error("No valid services found in IP_CHECK_SERVICES");
return [];
}
// 使用单一Fisher-Yates洗牌算法增强随机性
const shuffled = [...validServices];
const length = shuffled.length;
// 使用多个种子进行多次洗牌
const seeds = [
Math.random(),
Date.now() / 1000,
performance.now() / 1000
];
const seeds = [Math.random(), Date.now() / 1000, performance.now() / 1000];
for (const seed of seeds) {
const prng = createPrng(seed);
// Fisher-Yates洗牌算法
for (let i = length - 1; i > 0; i--) {
const j = Math.floor(prng() * (i + 1));
// 使用临时变量进行交换,避免解构赋值可能的问题
const temp = shuffled[i];
shuffled[i] = shuffled[j];
shuffled[j] = temp;
}
}
return shuffled;
}
@@ -462,11 +460,11 @@ function shuffleServices() {
function createPrng(seed: number): () => number {
// 使用xorshift32算法
let state = seed >>> 0;
// 如果种子为0设置一个默认值
if (state === 0) state = 123456789;
return function() {
return function () {
state ^= state << 13;
state ^= state >>> 17;
state ^= state << 5;
@@ -522,7 +520,7 @@ export const getIpInfo = async (): Promise<IpInfo> => {
lastError = error;
console.log(
`尝试 ${attempt + 1}/${maxRetries} 失败 (${service.url}):`,
error.message
error.message,
);
if (error.name === "AbortError") {
@@ -530,7 +528,7 @@ export const getIpInfo = async (): Promise<IpInfo> => {
}
if (attempt < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, 1000));
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
}
@@ -539,9 +537,9 @@ export const getIpInfo = async (): Promise<IpInfo> => {
if (lastError) {
throw new Error(`所有IP检测服务都失败: ${lastError.message}`);
} else {
throw new Error('没有可用的IP检测服务');
throw new Error("没有可用的IP检测服务");
}
} finally {
clearTimeout(overallTimeoutId);
}
};
};

View File

@@ -1,8 +1,8 @@
import { ReactNode } from 'react';
import { ReactNode } from "react";
export interface NoticeItem {
id: number;
type: 'success' | 'error' | 'info';
type: "success" | "error" | "info";
message: ReactNode;
duration: number;
timerId?: ReturnType<typeof setTimeout>;
@@ -21,13 +21,13 @@ function notifyListeners() {
// Shows a notification.
export function showNotice(
type: 'success' | 'error' | 'info',
type: "success" | "error" | "info",
message: ReactNode,
duration?: number,
): number {
const id = nextId++;
const effectiveDuration =
duration ?? (type === 'error' ? 8000 : type === 'info' ? 5000 : 3000); // Longer defaults
duration ?? (type === "error" ? 8000 : type === "info" ? 5000 : 3000); // Longer defaults
const newNotice: NoticeItem = {
id,
@@ -38,12 +38,11 @@ export function showNotice(
// Auto-hide timer (only if duration is not null/0)
if (effectiveDuration > 0) {
newNotice.timerId = setTimeout(() => {
hideNotice(id);
}, effectiveDuration);
newNotice.timerId = setTimeout(() => {
hideNotice(id);
}, effectiveDuration);
}
notices = [...notices, newNotice];
notifyListeners();
return id;
@@ -54,7 +53,7 @@ export function showNotice(
export function hideNotice(id: number) {
const notice = notices.find((n) => n.id === id);
if (notice?.timerId) {
clearTimeout(notice.timerId); // Clear timeout if manually closed
clearTimeout(notice.timerId); // Clear timeout if manually closed
}
notices = notices.filter((n) => n.id !== id);
notifyListeners();
@@ -72,9 +71,9 @@ export function subscribeNotices(listener: Listener): () => void {
// Function to clear all notices at once
export function clearAllNotices() {
notices.forEach(n => {
if (n.timerId) clearTimeout(n.timerId);
});
notices = [];
notifyListeners();
}
notices.forEach((n) => {
if (n.timerId) clearTimeout(n.timerId);
});
notices = [];
notifyListeners();
}

View File

@@ -1,6 +1,6 @@
export default function debounce<T extends (...args: any[]) => void>(
func: T,
wait: number
wait: number,
): T {
let timeout: ReturnType<typeof setTimeout> | null = null;
return function (this: any, ...args: Parameters<T>) {

View File

@@ -578,8 +578,8 @@ function URI_VLESS(line: string): IProxyVlessConfig {
proxy.network = "ws";
httpupgrade = true;
} else {
proxy.network = ["tcp", "ws", "http", "grpc", "h2"].includes(params.type)
? (params.type as NetworkType)
proxy.network = ["tcp", "ws", "http", "grpc", "h2"].includes(params.type)
? (params.type as NetworkType)
: "tcp";
}
if (!proxy.network && isShadowrocket && params.obfs) {