mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-01-28 16:30:52 +08:00
refactor: proxy components
This commit is contained in:
@@ -31,7 +31,7 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
useTheme,
|
useTheme,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import {
|
import {
|
||||||
@@ -196,9 +196,10 @@ export const ProxyChain = ({
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { proxies } = useAppData();
|
const { proxies } = useAppData();
|
||||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
|
||||||
const [isConnecting, setIsConnecting] = useState(false);
|
const [isConnecting, setIsConnecting] = useState(false);
|
||||||
const [isConnected, setIsConnected] = useState(false);
|
const markUnsavedChanges = useCallback(() => {
|
||||||
|
onMarkUnsavedChanges?.();
|
||||||
|
}, [onMarkUnsavedChanges]);
|
||||||
|
|
||||||
// 获取当前代理信息以检查连接状态
|
// 获取当前代理信息以检查连接状态
|
||||||
const { data: currentProxies, mutate: mutateProxies } = useSWR(
|
const { data: currentProxies, mutate: mutateProxies } = useSWR(
|
||||||
@@ -211,52 +212,26 @@ export const ProxyChain = ({
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// 检查连接状态
|
const isConnected = useMemo(() => {
|
||||||
useEffect(() => {
|
|
||||||
if (!currentProxies || proxyChain.length < 2) {
|
if (!currentProxies || proxyChain.length < 2) {
|
||||||
setIsConnected(false);
|
return false;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取用户配置的最后一个节点
|
|
||||||
const lastNode = proxyChain[proxyChain.length - 1];
|
const lastNode = proxyChain[proxyChain.length - 1];
|
||||||
|
|
||||||
// 根据模式确定要检查的代理组和当前选中的代理
|
|
||||||
if (mode === "global") {
|
if (mode === "global") {
|
||||||
// 全局模式:检查 global 对象
|
return currentProxies.global?.now === lastNode.name;
|
||||||
if (!currentProxies.global || !currentProxies.global.now) {
|
|
||||||
setIsConnected(false);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查当前选中的代理是否是配置的最后一个节点
|
if (!selectedGroup || !Array.isArray(currentProxies.groups)) {
|
||||||
if (currentProxies.global.now === lastNode.name) {
|
return false;
|
||||||
setIsConnected(true);
|
|
||||||
} else {
|
|
||||||
setIsConnected(false);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 规则模式:检查指定的代理组
|
|
||||||
if (!selectedGroup) {
|
|
||||||
setIsConnected(false);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const proxyChainGroup = currentProxies.groups.find(
|
const proxyChainGroup = currentProxies.groups.find(
|
||||||
(group) => group.name === selectedGroup,
|
(group) => group.name === selectedGroup,
|
||||||
);
|
);
|
||||||
if (!proxyChainGroup || !proxyChainGroup.now) {
|
|
||||||
setIsConnected(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查当前选中的代理是否是配置的最后一个节点
|
return proxyChainGroup?.now === lastNode.name;
|
||||||
if (proxyChainGroup.now === lastNode.name) {
|
|
||||||
setIsConnected(true);
|
|
||||||
} else {
|
|
||||||
setIsConnected(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [currentProxies, proxyChain, mode, selectedGroup]);
|
}, [currentProxies, proxyChain, mode, selectedGroup]);
|
||||||
|
|
||||||
// 监听链的变化,但排除从配置加载的情况
|
// 监听链的变化,但排除从配置加载的情况
|
||||||
@@ -267,10 +242,10 @@ export const ProxyChain = ({
|
|||||||
chainLengthRef.current !== proxyChain.length &&
|
chainLengthRef.current !== proxyChain.length &&
|
||||||
chainLengthRef.current !== 0
|
chainLengthRef.current !== 0
|
||||||
) {
|
) {
|
||||||
setHasUnsavedChanges(true);
|
markUnsavedChanges();
|
||||||
}
|
}
|
||||||
chainLengthRef.current = proxyChain.length;
|
chainLengthRef.current = proxyChain.length;
|
||||||
}, [proxyChain.length]);
|
}, [proxyChain.length, markUnsavedChanges]);
|
||||||
|
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor),
|
useSensor(PointerSensor),
|
||||||
@@ -288,26 +263,21 @@ export const ProxyChain = ({
|
|||||||
const newIndex = proxyChain.findIndex((item) => item.id === over?.id);
|
const newIndex = proxyChain.findIndex((item) => item.id === over?.id);
|
||||||
|
|
||||||
onUpdateChain(arrayMove(proxyChain, oldIndex, newIndex));
|
onUpdateChain(arrayMove(proxyChain, oldIndex, newIndex));
|
||||||
setHasUnsavedChanges(true);
|
markUnsavedChanges();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[proxyChain, onUpdateChain],
|
[proxyChain, onUpdateChain, markUnsavedChanges],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleRemoveProxy = useCallback(
|
const handleRemoveProxy = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
const newChain = proxyChain.filter((item) => item.id !== id);
|
const newChain = proxyChain.filter((item) => item.id !== id);
|
||||||
onUpdateChain(newChain);
|
onUpdateChain(newChain);
|
||||||
setHasUnsavedChanges(true);
|
markUnsavedChanges();
|
||||||
},
|
},
|
||||||
[proxyChain, onUpdateChain],
|
[proxyChain, onUpdateChain, markUnsavedChanges],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleClearAll = useCallback(() => {
|
|
||||||
onUpdateChain([]);
|
|
||||||
setHasUnsavedChanges(true);
|
|
||||||
}, [onUpdateChain]);
|
|
||||||
|
|
||||||
const handleConnect = useCallback(async () => {
|
const handleConnect = useCallback(async () => {
|
||||||
if (isConnected) {
|
if (isConnected) {
|
||||||
// 如果已连接,则断开连接
|
// 如果已连接,则断开连接
|
||||||
@@ -327,10 +297,6 @@ export const ProxyChain = ({
|
|||||||
|
|
||||||
// 清空链式代理配置UI
|
// 清空链式代理配置UI
|
||||||
// onUpdateChain([]);
|
// onUpdateChain([]);
|
||||||
// setHasUnsavedChanges(false);
|
|
||||||
|
|
||||||
// 强制更新连接状态
|
|
||||||
setIsConnected(false);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to disconnect from proxy chain:", error);
|
console.error("Failed to disconnect from proxy chain:", error);
|
||||||
alert(t("Failed to disconnect from proxy chain") || "断开链式代理失败");
|
alert(t("Failed to disconnect from proxy chain") || "断开链式代理失败");
|
||||||
@@ -372,9 +338,6 @@ export const ProxyChain = ({
|
|||||||
|
|
||||||
// 刷新代理信息以更新连接状态
|
// 刷新代理信息以更新连接状态
|
||||||
mutateProxies();
|
mutateProxies();
|
||||||
|
|
||||||
// 清除未保存标记
|
|
||||||
setHasUnsavedChanges(false);
|
|
||||||
console.log("Successfully connected to proxy chain");
|
console.log("Successfully connected to proxy chain");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to connect to proxy chain:", error);
|
console.error("Failed to connect to proxy chain:", error);
|
||||||
@@ -411,7 +374,6 @@ export const ProxyChain = ({
|
|||||||
delay: undefined,
|
delay: undefined,
|
||||||
})) || [];
|
})) || [];
|
||||||
onUpdateChain(chainItems);
|
onUpdateChain(chainItems);
|
||||||
setHasUnsavedChanges(false);
|
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
console.error("Failed to parse YAML:", parseError);
|
console.error("Failed to parse YAML:", parseError);
|
||||||
onUpdateChain([]);
|
onUpdateChain([]);
|
||||||
@@ -435,7 +397,6 @@ export const ProxyChain = ({
|
|||||||
delay: undefined,
|
delay: undefined,
|
||||||
})) || [];
|
})) || [];
|
||||||
onUpdateChain(chainItems);
|
onUpdateChain(chainItems);
|
||||||
setHasUnsavedChanges(false);
|
|
||||||
} catch (jsonError) {
|
} catch (jsonError) {
|
||||||
console.error("Failed to parse as JSON either:", jsonError);
|
console.error("Failed to parse as JSON either:", jsonError);
|
||||||
onUpdateChain([]);
|
onUpdateChain([]);
|
||||||
@@ -448,7 +409,6 @@ export const ProxyChain = ({
|
|||||||
} else if (chainConfigData === "") {
|
} else if (chainConfigData === "") {
|
||||||
// Empty string means no proxies available, show empty state
|
// Empty string means no proxies available, show empty state
|
||||||
onUpdateChain([]);
|
onUpdateChain([]);
|
||||||
setHasUnsavedChanges(false);
|
|
||||||
}
|
}
|
||||||
}, [chainConfigData, onUpdateChain]);
|
}, [chainConfigData, onUpdateChain]);
|
||||||
|
|
||||||
@@ -519,7 +479,6 @@ export const ProxyChain = ({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
updateProxyChainConfigInRuntime(null);
|
updateProxyChainConfigInRuntime(null);
|
||||||
onUpdateChain([]);
|
onUpdateChain([]);
|
||||||
setHasUnsavedChanges(false);
|
|
||||||
}}
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
color: theme.palette.error.main,
|
color: theme.palette.error.main,
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ interface ProxyChainItem {
|
|||||||
delay?: number;
|
delay?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const VirtuosoFooter = () => <div style={{ height: "8px" }} />;
|
||||||
|
|
||||||
export const ProxyGroups = (props: Props) => {
|
export const ProxyGroups = (props: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { mode, isChainMode = false, chainConfigData } = props;
|
const { mode, isChainMode = false, chainConfigData } = props;
|
||||||
@@ -61,23 +63,25 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
|
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge();
|
||||||
const { proxies: proxiesData } = useAppData();
|
const { proxies: proxiesData } = useAppData();
|
||||||
|
const groups = proxiesData?.groups;
|
||||||
|
const availableGroups = useMemo(() => groups ?? [], [groups]);
|
||||||
|
|
||||||
// 当链式代理模式且规则模式下,如果没有选择代理组,默认选择第一个
|
const defaultRuleGroup = useMemo(() => {
|
||||||
useEffect(() => {
|
if (isChainMode && mode === "rule" && availableGroups.length > 0) {
|
||||||
if (
|
return availableGroups[0].name;
|
||||||
isChainMode &&
|
|
||||||
mode === "rule" &&
|
|
||||||
!selectedGroup &&
|
|
||||||
proxiesData?.groups?.length > 0
|
|
||||||
) {
|
|
||||||
setSelectedGroup(proxiesData.groups[0].name);
|
|
||||||
}
|
}
|
||||||
}, [isChainMode, mode, selectedGroup, proxiesData]);
|
return null;
|
||||||
|
}, [availableGroups, isChainMode, mode]);
|
||||||
|
|
||||||
|
const activeSelectedGroup = useMemo(
|
||||||
|
() => selectedGroup ?? defaultRuleGroup,
|
||||||
|
[selectedGroup, defaultRuleGroup],
|
||||||
|
);
|
||||||
|
|
||||||
const { renderList, onProxies, onHeadState } = useRenderList(
|
const { renderList, onProxies, onHeadState } = useRenderList(
|
||||||
mode,
|
mode,
|
||||||
isChainMode,
|
isChainMode,
|
||||||
selectedGroup,
|
activeSelectedGroup,
|
||||||
);
|
);
|
||||||
|
|
||||||
const getGroupHeadState = useCallback(
|
const getGroupHeadState = useCallback(
|
||||||
@@ -112,6 +116,8 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (renderList.length === 0) return;
|
if (renderList.length === 0) return;
|
||||||
|
|
||||||
|
let restoreTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const savedPositions = localStorage.getItem("proxy-scroll-positions");
|
const savedPositions = localStorage.getItem("proxy-scroll-positions");
|
||||||
if (savedPositions) {
|
if (savedPositions) {
|
||||||
@@ -120,7 +126,7 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
const savedPosition = positions[mode];
|
const savedPosition = positions[mode];
|
||||||
|
|
||||||
if (savedPosition !== undefined) {
|
if (savedPosition !== undefined) {
|
||||||
setTimeout(() => {
|
restoreTimer = setTimeout(() => {
|
||||||
virtuosoRef.current?.scrollTo({
|
virtuosoRef.current?.scrollTo({
|
||||||
top: savedPosition,
|
top: savedPosition,
|
||||||
behavior: "auto",
|
behavior: "auto",
|
||||||
@@ -131,6 +137,12 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error restoring scroll position:", e);
|
console.error("Error restoring scroll position:", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (restoreTimer) {
|
||||||
|
clearTimeout(restoreTimer);
|
||||||
|
}
|
||||||
|
};
|
||||||
}, [mode, renderList.length]);
|
}, [mode, renderList.length]);
|
||||||
|
|
||||||
// 改为使用节流函数保存滚动位置
|
// 改为使用节流函数保存滚动位置
|
||||||
@@ -150,9 +162,11 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 使用改进的滚动处理
|
// 使用改进的滚动处理
|
||||||
const handleScroll = useCallback(
|
const handleScroll = useMemo(
|
||||||
throttle((e: any) => {
|
() =>
|
||||||
const scrollTop = e.target.scrollTop;
|
throttle((event: Event) => {
|
||||||
|
const target = event.target as HTMLElement | null;
|
||||||
|
const scrollTop = target?.scrollTop ?? 0;
|
||||||
setShowScrollTop(scrollTop > 100);
|
setShowScrollTop(scrollTop > 100);
|
||||||
// 使用稳定的节流来保存位置,而不是setTimeout
|
// 使用稳定的节流来保存位置,而不是setTimeout
|
||||||
saveScrollPosition(scrollTop);
|
saveScrollPosition(scrollTop);
|
||||||
@@ -162,13 +176,16 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
|
|
||||||
// 添加和清理滚动事件监听器
|
// 添加和清理滚动事件监听器
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!scrollerRef.current) return;
|
const node = scrollerRef.current;
|
||||||
scrollerRef.current.addEventListener("scroll", handleScroll, {
|
if (!node) return;
|
||||||
passive: true,
|
|
||||||
});
|
const listener = handleScroll as EventListener;
|
||||||
|
const options: AddEventListenerOptions = { passive: true };
|
||||||
|
|
||||||
|
node.addEventListener("scroll", listener, options);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
scrollerRef.current?.removeEventListener("scroll", handleScroll);
|
node.removeEventListener("scroll", listener, options);
|
||||||
};
|
};
|
||||||
}, [handleScroll]);
|
}, [handleScroll]);
|
||||||
|
|
||||||
@@ -186,18 +203,14 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
setDuplicateWarning({ open: false, message: "" });
|
setDuplicateWarning({ open: false, message: "" });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 获取当前选中的代理组信息
|
const currentGroup = useMemo(() => {
|
||||||
const getCurrentGroup = useCallback(() => {
|
if (!activeSelectedGroup) return null;
|
||||||
if (!selectedGroup || !proxiesData?.groups) return null;
|
return (
|
||||||
return proxiesData.groups.find(
|
availableGroups.find(
|
||||||
(group: any) => group.name === selectedGroup,
|
(group: any) => group.name === activeSelectedGroup,
|
||||||
|
) ?? null
|
||||||
);
|
);
|
||||||
}, [selectedGroup, proxiesData]);
|
}, [activeSelectedGroup, availableGroups]);
|
||||||
|
|
||||||
// 获取可用的代理组列表
|
|
||||||
const getAvailableGroups = useCallback(() => {
|
|
||||||
return proxiesData?.groups || [];
|
|
||||||
}, [proxiesData]);
|
|
||||||
|
|
||||||
// 处理代理组选择菜单
|
// 处理代理组选择菜单
|
||||||
const handleGroupMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
const handleGroupMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
@@ -220,9 +233,6 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentGroup = getCurrentGroup();
|
|
||||||
const availableGroups = getAvailableGroups();
|
|
||||||
|
|
||||||
const handleChangeProxy = useCallback(
|
const handleChangeProxy = useCallback(
|
||||||
(group: IProxyGroupItem, proxy: IProxyItem) => {
|
(group: IProxyGroupItem, proxy: IProxyItem) => {
|
||||||
if (isChainMode) {
|
if (isChainMode) {
|
||||||
@@ -472,7 +482,7 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
scrollerRef.current = ref as Element;
|
scrollerRef.current = ref as Element;
|
||||||
}}
|
}}
|
||||||
components={{
|
components={{
|
||||||
Footer: () => <div style={{ height: "8px" }} />,
|
Footer: VirtuosoFooter,
|
||||||
}}
|
}}
|
||||||
initialScrollTop={scrollPositionRef.current[mode]}
|
initialScrollTop={scrollPositionRef.current[mode]}
|
||||||
computeItemKey={(index) => renderList[index].key}
|
computeItemKey={(index) => renderList[index].key}
|
||||||
@@ -498,7 +508,7 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
onUpdateChain={setProxyChain}
|
onUpdateChain={setProxyChain}
|
||||||
chainConfigData={chainConfigData}
|
chainConfigData={chainConfigData}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
selectedGroup={selectedGroup}
|
selectedGroup={activeSelectedGroup}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -530,11 +540,11 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{availableGroups.map((group: any, _index: number) => (
|
{availableGroups.map((group: any) => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key={group.name}
|
key={group.name}
|
||||||
onClick={() => handleGroupSelect(group.name)}
|
onClick={() => handleGroupSelect(group.name)}
|
||||||
selected={selectedGroup === group.name}
|
selected={activeSelectedGroup === group.name}
|
||||||
sx={{
|
sx={{
|
||||||
fontSize: "14px",
|
fontSize: "14px",
|
||||||
py: 1,
|
py: 1,
|
||||||
@@ -591,7 +601,7 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
scrollerRef.current = ref as Element;
|
scrollerRef.current = ref as Element;
|
||||||
}}
|
}}
|
||||||
components={{
|
components={{
|
||||||
Footer: () => <div style={{ height: "8px" }} />,
|
Footer: VirtuosoFooter,
|
||||||
}}
|
}}
|
||||||
// 添加平滑滚动设置
|
// 添加平滑滚动设置
|
||||||
initialScrollTop={scrollPositionRef.current[mode]}
|
initialScrollTop={scrollPositionRef.current[mode]}
|
||||||
|
|||||||
@@ -31,8 +31,10 @@ interface Props {
|
|||||||
onHeadState: (val: Partial<HeadState>) => void;
|
onHeadState: (val: Partial<HeadState>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const defaultSx: SxProps = {};
|
||||||
|
|
||||||
export const ProxyHead = ({
|
export const ProxyHead = ({
|
||||||
sx = {},
|
sx = defaultSx,
|
||||||
url,
|
url,
|
||||||
groupName,
|
groupName,
|
||||||
headState,
|
headState,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { CheckCircleOutlineRounded } from "@mui/icons-material";
|
import { CheckCircleOutlineRounded } from "@mui/icons-material";
|
||||||
import { alpha, Box, ListItemButton, styled, Typography } from "@mui/material";
|
import { alpha, Box, ListItemButton, styled, Typography } from "@mui/material";
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useReducer } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { BaseLoading } from "@/components/base";
|
import { BaseLoading } from "@/components/base";
|
||||||
@@ -26,7 +26,7 @@ export const ProxyItemMini = (props: Props) => {
|
|||||||
const isPreset = presetList.includes(proxy.name);
|
const isPreset = presetList.includes(proxy.name);
|
||||||
// -1/<=0 为 不显示
|
// -1/<=0 为 不显示
|
||||||
// -2 为 loading
|
// -2 为 loading
|
||||||
const [delay, setDelay] = useState(-1);
|
const [delay, setDelay] = useReducer((_: number, value: number) => value, -1);
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge();
|
||||||
const timeout = verge?.default_latency_timeout || 10000;
|
const timeout = verge?.default_latency_timeout || 10000;
|
||||||
|
|
||||||
@@ -39,11 +39,15 @@ export const ProxyItemMini = (props: Props) => {
|
|||||||
};
|
};
|
||||||
}, [isPreset, proxy.name, group.name]);
|
}, [isPreset, proxy.name, group.name]);
|
||||||
|
|
||||||
useEffect(() => {
|
const updateDelay = useCallback(() => {
|
||||||
if (!proxy) return;
|
if (!proxy) return;
|
||||||
setDelay(delayManager.getDelayFix(proxy, group.name));
|
setDelay(delayManager.getDelayFix(proxy, group.name));
|
||||||
}, [proxy, group.name]);
|
}, [proxy, group.name]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateDelay();
|
||||||
|
}, [updateDelay]);
|
||||||
|
|
||||||
const onDelay = useLockFn(async () => {
|
const onDelay = useLockFn(async () => {
|
||||||
setDelay(-2);
|
setDelay(-2);
|
||||||
setDelay(await delayManager.checkDelay(proxy.name, group.name, timeout));
|
setDelay(await delayManager.checkDelay(proxy.name, group.name, timeout));
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
Theme,
|
Theme,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useReducer } from "react";
|
||||||
|
|
||||||
import { BaseLoading } from "@/components/base";
|
import { BaseLoading } from "@/components/base";
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
@@ -51,7 +51,7 @@ export const ProxyItem = (props: Props) => {
|
|||||||
const isPreset = presetList.includes(proxy.name);
|
const isPreset = presetList.includes(proxy.name);
|
||||||
// -1/<=0 为 不显示
|
// -1/<=0 为 不显示
|
||||||
// -2 为 loading
|
// -2 为 loading
|
||||||
const [delay, setDelay] = useState(-1);
|
const [delay, setDelay] = useReducer((_: number, value: number) => value, -1);
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge();
|
||||||
const timeout = verge?.default_latency_timeout || 10000;
|
const timeout = verge?.default_latency_timeout || 10000;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -63,10 +63,14 @@ export const ProxyItem = (props: Props) => {
|
|||||||
};
|
};
|
||||||
}, [proxy.name, group.name, isPreset]);
|
}, [proxy.name, group.name, isPreset]);
|
||||||
|
|
||||||
useEffect(() => {
|
const updateDelay = useCallback(() => {
|
||||||
if (!proxy) return;
|
if (!proxy) return;
|
||||||
setDelay(delayManager.getDelayFix(proxy, group.name));
|
setDelay(delayManager.getDelayFix(proxy, group.name));
|
||||||
}, [group.name, proxy]);
|
}, [proxy, group.name]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateDelay();
|
||||||
|
}, [updateDelay]);
|
||||||
|
|
||||||
const onDelay = useLockFn(async () => {
|
const onDelay = useLockFn(async () => {
|
||||||
setDelay(-2);
|
setDelay(-2);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { convertFileSrc } from "@tauri-apps/api/core";
|
import { convertFileSrc } from "@tauri-apps/api/core";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
@@ -49,7 +49,7 @@ export const ProxyRender = (props: RenderProps) => {
|
|||||||
onCheckAll,
|
onCheckAll,
|
||||||
onHeadState,
|
onHeadState,
|
||||||
onChangeProxy,
|
onChangeProxy,
|
||||||
isChainMode = false,
|
isChainMode: _ = false,
|
||||||
} = props;
|
} = props;
|
||||||
const { type, group, headState, proxy, proxyCol } = item;
|
const { type, group, headState, proxy, proxyCol } = item;
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge();
|
||||||
@@ -59,23 +59,42 @@ export const ProxyRender = (props: RenderProps) => {
|
|||||||
const itembackgroundcolor = isDark ? "#282A36" : "#ffffff";
|
const itembackgroundcolor = isDark ? "#282A36" : "#ffffff";
|
||||||
const [iconCachePath, setIconCachePath] = useState("");
|
const [iconCachePath, setIconCachePath] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
const initIconCachePath = useCallback(async () => {
|
||||||
initIconCachePath();
|
|
||||||
}, [group]);
|
|
||||||
|
|
||||||
async function initIconCachePath() {
|
|
||||||
if (group.icon && group.icon.trim().startsWith("http")) {
|
if (group.icon && group.icon.trim().startsWith("http")) {
|
||||||
const fileName =
|
const fileName =
|
||||||
group.name.replaceAll(" ", "") + "-" + getFileName(group.icon);
|
group.name.replaceAll(" ", "") + "-" + getFileName(group.icon);
|
||||||
const iconPath = await downloadIconCache(group.icon, fileName);
|
const iconPath = await downloadIconCache(group.icon, fileName);
|
||||||
setIconCachePath(convertFileSrc(iconPath));
|
setIconCachePath(convertFileSrc(iconPath));
|
||||||
|
} else {
|
||||||
|
setIconCachePath("");
|
||||||
}
|
}
|
||||||
}
|
}, [group.icon, group.name]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initIconCachePath();
|
||||||
|
}, [initIconCachePath]);
|
||||||
|
|
||||||
function getFileName(url: string) {
|
function getFileName(url: string) {
|
||||||
return url.substring(url.lastIndexOf("/") + 1);
|
return url.substring(url.lastIndexOf("/") + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const proxyColItemsMemo = useMemo(() => {
|
||||||
|
if (type !== 4 || !proxyCol) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return proxyCol.map((proxyItem) => (
|
||||||
|
<ProxyItemMini
|
||||||
|
key={`${item.key}-${proxyItem?.name ?? "unknown"}`}
|
||||||
|
group={group}
|
||||||
|
proxy={proxyItem!}
|
||||||
|
selected={group.now === proxyItem?.name}
|
||||||
|
showType={headState?.showType}
|
||||||
|
onClick={() => onChangeProxy(group, proxyItem!)}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
}, [type, proxyCol, item.key, group, headState, onChangeProxy]);
|
||||||
|
|
||||||
if (type === 0) {
|
if (type === 0) {
|
||||||
return (
|
return (
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
@@ -205,18 +224,6 @@ export const ProxyRender = (props: RenderProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (type === 4) {
|
if (type === 4) {
|
||||||
const proxyColItemsMemo = useMemo(() => {
|
|
||||||
return proxyCol?.map((proxy) => (
|
|
||||||
<ProxyItemMini
|
|
||||||
key={item.key + proxy.name}
|
|
||||||
group={group}
|
|
||||||
proxy={proxy!}
|
|
||||||
selected={group.now === proxy.name}
|
|
||||||
showType={headState?.showType}
|
|
||||||
onClick={() => onChangeProxy(group, proxy!)}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
}, [proxyCol, group, headState]);
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useReducer } from "react";
|
||||||
|
|
||||||
import delayManager from "@/services/delay";
|
import delayManager from "@/services/delay";
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ export default function useFilterSort(
|
|||||||
filterText: string,
|
filterText: string,
|
||||||
sortType: ProxySortType,
|
sortType: ProxySortType,
|
||||||
) {
|
) {
|
||||||
const [, setRefresh] = useState({});
|
const [_, bumpRefresh] = useReducer((count: number) => count + 1, 0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let last = 0;
|
let last = 0;
|
||||||
@@ -21,7 +21,7 @@ export default function useFilterSort(
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - last > 666) {
|
if (now - last > 666) {
|
||||||
last = now;
|
last = now;
|
||||||
setRefresh({});
|
bumpRefresh();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useReducer } from "react";
|
||||||
|
|
||||||
import { useProfiles } from "@/hooks/use-profiles";
|
import { useProfiles } from "@/hooks/use-profiles";
|
||||||
|
|
||||||
@@ -25,15 +25,38 @@ export const DEFAULT_STATE: HeadState = {
|
|||||||
testUrl: "",
|
testUrl: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type HeadStateAction =
|
||||||
|
| { type: "reset" }
|
||||||
|
| { type: "replace"; payload: Record<string, HeadState> }
|
||||||
|
| { type: "update"; groupName: string; patch: Partial<HeadState> };
|
||||||
|
|
||||||
|
function headStateReducer(
|
||||||
|
state: Record<string, HeadState>,
|
||||||
|
action: HeadStateAction,
|
||||||
|
): Record<string, HeadState> {
|
||||||
|
switch (action.type) {
|
||||||
|
case "reset":
|
||||||
|
return {};
|
||||||
|
case "replace":
|
||||||
|
return action.payload;
|
||||||
|
case "update": {
|
||||||
|
const prev = state[action.groupName] || DEFAULT_STATE;
|
||||||
|
return { ...state, [action.groupName]: { ...prev, ...action.patch } };
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function useHeadStateNew() {
|
export function useHeadStateNew() {
|
||||||
const { profiles } = useProfiles();
|
const { profiles } = useProfiles();
|
||||||
const current = profiles?.current || "";
|
const current = profiles?.current || "";
|
||||||
|
|
||||||
const [state, setState] = useState<Record<string, HeadState>>({});
|
const [state, dispatch] = useReducer(headStateReducer, {});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!current) {
|
if (!current) {
|
||||||
setState({});
|
dispatch({ type: "reset" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,21 +68,19 @@ export function useHeadStateNew() {
|
|||||||
const value = data[current] || {};
|
const value = data[current] || {};
|
||||||
|
|
||||||
if (value && typeof value === "object") {
|
if (value && typeof value === "object") {
|
||||||
setState(value);
|
dispatch({ type: "replace", payload: value });
|
||||||
} else {
|
} else {
|
||||||
setState({});
|
dispatch({ type: "reset" });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
dispatch({ type: "reset" });
|
||||||
}
|
}
|
||||||
} catch {}
|
|
||||||
}, [current]);
|
}, [current]);
|
||||||
|
|
||||||
const setHeadState = useCallback(
|
useEffect(() => {
|
||||||
(groupName: string, obj: Partial<HeadState>) => {
|
if (!current) return;
|
||||||
setState((old) => {
|
|
||||||
const state = old[groupName] || DEFAULT_STATE;
|
|
||||||
const ret = { ...old, [groupName]: { ...state, ...obj } };
|
|
||||||
|
|
||||||
// 保存到存储中
|
const timer = setTimeout(() => {
|
||||||
setTimeout(() => {
|
|
||||||
try {
|
try {
|
||||||
const item = localStorage.getItem(HEAD_STATE_KEY);
|
const item = localStorage.getItem(HEAD_STATE_KEY);
|
||||||
|
|
||||||
@@ -67,14 +88,19 @@ export function useHeadStateNew() {
|
|||||||
|
|
||||||
if (!data || typeof data !== "object") data = {};
|
if (!data || typeof data !== "object") data = {};
|
||||||
|
|
||||||
data[current] = ret;
|
data[current] = state;
|
||||||
|
|
||||||
localStorage.setItem(HEAD_STATE_KEY, JSON.stringify(data));
|
localStorage.setItem(HEAD_STATE_KEY, JSON.stringify(data));
|
||||||
} catch {}
|
} catch {}
|
||||||
});
|
});
|
||||||
|
|
||||||
return ret;
|
return () => clearTimeout(timer);
|
||||||
});
|
}, [state, current]);
|
||||||
|
|
||||||
|
const setHeadState = useCallback(
|
||||||
|
(groupName: string, obj: Partial<HeadState>) => {
|
||||||
|
if (!current) return;
|
||||||
|
dispatch({ type: "update", groupName, patch: obj });
|
||||||
},
|
},
|
||||||
[current],
|
[current],
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user