2025-02-17 14:24:33 +08:00
|
|
|
import { useRef, useState, useEffect, useCallback } from "react";
|
2022-11-20 19:46:16 +08:00
|
|
|
import { useLockFn } from "ahooks";
|
|
|
|
|
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso";
|
|
|
|
|
import {
|
|
|
|
|
getConnections,
|
|
|
|
|
providerHealthCheck,
|
|
|
|
|
updateProxy,
|
|
|
|
|
deleteConnection,
|
2024-04-09 13:15:45 +08:00
|
|
|
getGroupProxyDelays,
|
2022-11-20 19:46:16 +08:00
|
|
|
} from "@/services/api";
|
|
|
|
|
import { useProfiles } from "@/hooks/use-profiles";
|
2022-11-20 20:12:58 +08:00
|
|
|
import { useVerge } from "@/hooks/use-verge";
|
2022-12-15 12:23:57 +08:00
|
|
|
import { BaseEmpty } from "../base";
|
2022-11-23 18:27:57 +08:00
|
|
|
import { useRenderList } from "./use-render-list";
|
|
|
|
|
import { ProxyRender } from "./proxy-render";
|
2022-11-20 19:46:16 +08:00
|
|
|
import delayManager from "@/services/delay";
|
2024-06-26 05:33:06 +08:00
|
|
|
import { useTranslation } from "react-i18next";
|
2024-11-22 09:22:44 +08:00
|
|
|
import { ScrollTopButton } from "../layout/scroll-top-button";
|
2022-11-20 19:46:16 +08:00
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
|
mode: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const ProxyGroups = (props: Props) => {
|
2024-06-26 05:33:06 +08:00
|
|
|
const { t } = useTranslation();
|
2022-11-20 19:46:16 +08:00
|
|
|
const { mode } = props;
|
|
|
|
|
|
|
|
|
|
const { renderList, onProxies, onHeadState } = useRenderList(mode);
|
|
|
|
|
|
2022-11-20 20:12:58 +08:00
|
|
|
const { verge } = useVerge();
|
2022-11-20 19:46:16 +08:00
|
|
|
const { current, patchCurrent } = useProfiles();
|
2024-02-18 11:11:22 +08:00
|
|
|
const timeout = verge?.default_latency_timeout || 10000;
|
2022-11-20 19:46:16 +08:00
|
|
|
|
|
|
|
|
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
2025-02-17 14:24:33 +08:00
|
|
|
const scrollPositionRef = useRef<Record<string, number>>({});
|
2024-11-22 09:22:44 +08:00
|
|
|
const [showScrollTop, setShowScrollTop] = useState(false);
|
2025-02-17 14:24:33 +08:00
|
|
|
const scrollerRef = useRef<Element | null>(null);
|
|
|
|
|
|
|
|
|
|
// 从 localStorage 恢复滚动位置
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (renderList.length === 0) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const savedPositions = localStorage.getItem("proxy-scroll-positions");
|
|
|
|
|
if (savedPositions) {
|
|
|
|
|
const positions = JSON.parse(savedPositions);
|
|
|
|
|
scrollPositionRef.current = positions;
|
|
|
|
|
const savedPosition = positions[mode];
|
|
|
|
|
|
|
|
|
|
if (savedPosition !== undefined) {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
virtuosoRef.current?.scrollTo({
|
|
|
|
|
top: savedPosition,
|
|
|
|
|
behavior: "auto",
|
|
|
|
|
});
|
|
|
|
|
}, 100);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error("Error restoring scroll position:", e);
|
|
|
|
|
}
|
|
|
|
|
}, [mode, renderList]);
|
|
|
|
|
|
|
|
|
|
// 使用防抖保存滚动位置
|
|
|
|
|
const saveScrollPosition = useCallback(
|
|
|
|
|
(scrollTop: number) => {
|
|
|
|
|
try {
|
|
|
|
|
scrollPositionRef.current[mode] = scrollTop;
|
|
|
|
|
localStorage.setItem(
|
|
|
|
|
"proxy-scroll-positions",
|
|
|
|
|
JSON.stringify(scrollPositionRef.current),
|
|
|
|
|
);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error("Error saving scroll position:", e);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[mode],
|
|
|
|
|
);
|
2024-11-22 09:22:44 +08:00
|
|
|
|
2025-02-17 14:24:33 +08:00
|
|
|
// 优化滚动处理函数
|
|
|
|
|
const handleScroll = useCallback(
|
|
|
|
|
(e: any) => {
|
|
|
|
|
const scrollTop = e.target.scrollTop;
|
|
|
|
|
setShowScrollTop(scrollTop > 100);
|
|
|
|
|
saveScrollPosition(scrollTop);
|
|
|
|
|
},
|
|
|
|
|
[saveScrollPosition],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 添加和清理滚动事件监听器
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const currentScroller = scrollerRef.current;
|
|
|
|
|
if (currentScroller) {
|
|
|
|
|
currentScroller.addEventListener("scroll", handleScroll, {
|
|
|
|
|
passive: true,
|
|
|
|
|
});
|
|
|
|
|
return () => {
|
|
|
|
|
currentScroller.removeEventListener("scroll", handleScroll);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}, [handleScroll]);
|
2024-11-22 09:22:44 +08:00
|
|
|
|
|
|
|
|
// 滚动到顶部
|
2025-02-17 14:24:33 +08:00
|
|
|
const scrollToTop = useCallback(() => {
|
2024-11-22 09:22:44 +08:00
|
|
|
virtuosoRef.current?.scrollTo?.({
|
|
|
|
|
top: 0,
|
|
|
|
|
behavior: "smooth",
|
|
|
|
|
});
|
2025-02-17 14:24:33 +08:00
|
|
|
saveScrollPosition(0);
|
|
|
|
|
}, [saveScrollPosition]);
|
2024-11-22 09:22:44 +08:00
|
|
|
|
2022-11-20 19:46:16 +08:00
|
|
|
// 切换分组的节点代理
|
|
|
|
|
const handleChangeProxy = useLockFn(
|
|
|
|
|
async (group: IProxyGroupItem, proxy: IProxyItem) => {
|
2024-04-09 13:15:45 +08:00
|
|
|
if (!["Selector", "URLTest", "Fallback"].includes(group.type)) return;
|
2022-11-20 19:46:16 +08:00
|
|
|
|
|
|
|
|
const { name, now } = group;
|
|
|
|
|
await updateProxy(name, proxy.name);
|
|
|
|
|
onProxies();
|
|
|
|
|
|
|
|
|
|
// 断开连接
|
2022-11-20 20:12:58 +08:00
|
|
|
if (verge?.auto_close_connection) {
|
2022-11-20 19:46:16 +08:00
|
|
|
getConnections().then(({ connections }) => {
|
|
|
|
|
connections.forEach((conn) => {
|
|
|
|
|
if (conn.chains.includes(now!)) {
|
|
|
|
|
deleteConnection(conn.id);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 保存到selected中
|
|
|
|
|
if (!current) return;
|
|
|
|
|
if (!current.selected) current.selected = [];
|
|
|
|
|
|
|
|
|
|
const index = current.selected.findIndex(
|
2024-11-22 09:22:44 +08:00
|
|
|
(item) => item.name === group.name,
|
2022-11-20 19:46:16 +08:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (index < 0) {
|
|
|
|
|
current.selected.push({ name, now: proxy.name });
|
|
|
|
|
} else {
|
|
|
|
|
current.selected[index] = { name, now: proxy.name };
|
|
|
|
|
}
|
|
|
|
|
await patchCurrent({ selected: current.selected });
|
2024-11-22 09:22:44 +08:00
|
|
|
},
|
2022-11-20 19:46:16 +08:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 测全部延迟
|
|
|
|
|
const handleCheckAll = useLockFn(async (groupName: string) => {
|
|
|
|
|
const proxies = renderList
|
2022-12-13 17:34:39 +08:00
|
|
|
.filter(
|
2024-11-22 09:22:44 +08:00
|
|
|
(e) => e.group?.name === groupName && (e.type === 2 || e.type === 4),
|
2022-12-13 17:34:39 +08:00
|
|
|
)
|
|
|
|
|
.flatMap((e) => e.proxyCol || e.proxy!)
|
2022-11-20 19:46:16 +08:00
|
|
|
.filter(Boolean);
|
|
|
|
|
|
|
|
|
|
const providers = new Set(proxies.map((p) => p!.provider!).filter(Boolean));
|
|
|
|
|
|
|
|
|
|
if (providers.size) {
|
|
|
|
|
Promise.allSettled(
|
2024-11-22 09:22:44 +08:00
|
|
|
[...providers].map((p) => providerHealthCheck(p)),
|
2022-11-20 19:46:16 +08:00
|
|
|
).then(() => onProxies());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const names = proxies.filter((p) => !p!.provider).map((p) => p!.name);
|
2024-04-09 13:15:45 +08:00
|
|
|
|
|
|
|
|
await Promise.race([
|
|
|
|
|
delayManager.checkListDelay(names, groupName, timeout),
|
|
|
|
|
getGroupProxyDelays(groupName, delayManager.getUrl(groupName), timeout), // 查询group delays 将清除fixed(不关注调用结果)
|
|
|
|
|
]);
|
2022-11-20 19:46:16 +08:00
|
|
|
|
|
|
|
|
onProxies();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 滚到对应的节点
|
|
|
|
|
const handleLocation = (group: IProxyGroupItem) => {
|
|
|
|
|
if (!group) return;
|
|
|
|
|
const { name, now } = group;
|
|
|
|
|
|
|
|
|
|
const index = renderList.findIndex(
|
2022-12-13 17:34:39 +08:00
|
|
|
(e) =>
|
|
|
|
|
e.group?.name === name &&
|
|
|
|
|
((e.type === 2 && e.proxy?.name === now) ||
|
2024-11-22 09:22:44 +08:00
|
|
|
(e.type === 4 && e.proxyCol?.some((p) => p.name === now))),
|
2022-11-20 19:46:16 +08:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (index >= 0) {
|
|
|
|
|
virtuosoRef.current?.scrollToIndex?.({
|
|
|
|
|
index,
|
|
|
|
|
align: "center",
|
|
|
|
|
behavior: "smooth",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2022-11-24 10:35:09 +08:00
|
|
|
if (mode === "direct") {
|
2024-06-26 05:33:06 +08:00
|
|
|
return <BaseEmpty text={t("clash_mode_direct")} />;
|
2022-11-24 10:35:09 +08:00
|
|
|
}
|
|
|
|
|
|
2022-11-20 19:46:16 +08:00
|
|
|
return (
|
2024-11-22 09:22:44 +08:00
|
|
|
<div style={{ position: "relative", height: "100%" }}>
|
|
|
|
|
<Virtuoso
|
|
|
|
|
ref={virtuosoRef}
|
|
|
|
|
style={{ height: "calc(100% - 16px)" }}
|
|
|
|
|
totalCount={renderList.length}
|
|
|
|
|
increaseViewportBy={256}
|
|
|
|
|
scrollerRef={(ref) => {
|
2025-02-17 14:24:33 +08:00
|
|
|
scrollerRef.current = ref;
|
2024-11-22 09:22:44 +08:00
|
|
|
}}
|
|
|
|
|
itemContent={(index) => (
|
2025-02-17 14:24:33 +08:00
|
|
|
<ProxyRender
|
|
|
|
|
key={renderList[index].key}
|
|
|
|
|
item={renderList[index]}
|
|
|
|
|
indent={mode === "rule" || mode === "script"}
|
|
|
|
|
onLocation={handleLocation}
|
|
|
|
|
onCheckAll={handleCheckAll}
|
|
|
|
|
onHeadState={onHeadState}
|
|
|
|
|
onChangeProxy={handleChangeProxy}
|
|
|
|
|
/>
|
2024-11-22 09:22:44 +08:00
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
<ScrollTopButton show={showScrollTop} onClick={scrollToTop} />
|
|
|
|
|
</div>
|
2022-11-20 19:46:16 +08:00
|
|
|
);
|
|
|
|
|
};
|