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:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -157,7 +157,7 @@ export const BaseSearchBox = (props: SearchProps) => {
|
||||
</Tooltip>
|
||||
</Box>
|
||||
),
|
||||
}
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -24,7 +24,7 @@ export const UpdateButton = (props: Props) => {
|
||||
errorRetryCount: 2,
|
||||
revalidateIfStale: false,
|
||||
focusThrottleInterval: 36e5, // 1 hour
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (!updateInfo?.available) return null;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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, // 平滑滚动
|
||||
}}
|
||||
|
||||
@@ -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 },
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -201,7 +201,7 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
|
||||
setOpen(false);
|
||||
fileDataRef.current = null;
|
||||
setTimeout(() => formIns.reset(), 500);
|
||||
} catch { }
|
||||
} catch {}
|
||||
};
|
||||
|
||||
const text = {
|
||||
|
||||
@@ -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, // 平滑滚动
|
||||
}}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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, // 平滑滚动
|
||||
}}
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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 />}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
});
|
||||
setOpen(false);
|
||||
} catch (err: any) {
|
||||
showNotice('error', err.toString());
|
||||
showNotice("error", err.toString());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" }} />
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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" }}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -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()}
|
||||
/>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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")),
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ポートとキーを有効化。ポートとキーをランダム化するにはクリックしてください"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,4 +399,4 @@
|
||||
"View Profile-Merge": "프로필-병합 보기",
|
||||
"Update Successful": "업데이트 성공",
|
||||
"Update Failed": "업데이트 실패"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 端口和密鑰,點進去就可以隨機端口和密鑰"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
);
|
||||
|
||||
@@ -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 })}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -398,4 +398,4 @@ const ClashModeEnhancedCard = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default HomePage;
|
||||
export default HomePage;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -20,8 +20,8 @@ const ProxyPage = () => {
|
||||
revalidateOnFocus: false,
|
||||
revalidateIfStale: true,
|
||||
dedupingInterval: 1000,
|
||||
errorRetryInterval: 5000
|
||||
}
|
||||
errorRetryInterval: 5000,
|
||||
},
|
||||
);
|
||||
|
||||
const { verge } = useVerge();
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
if (eventType !== "change" || typeof listener !== "function") {
|
||||
console.error(
|
||||
"Invalid arguments for removeEventListener:",
|
||||
arguments
|
||||
arguments,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user