2025-09-18 23:34:21 +08:00
|
|
|
|
import { Box, Snackbar, Alert } from "@mui/material";
|
2022-11-20 19:46:16 +08:00
|
|
|
|
import { useLockFn } from "ahooks";
|
2025-09-18 23:34:21 +08:00
|
|
|
|
import { useRef, useState, useEffect, useCallback } from "react";
|
|
|
|
|
|
import { useTranslation } from "react-i18next";
|
2022-11-20 19:46:16 +08:00
|
|
|
|
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso";
|
2025-09-18 23:34:21 +08:00
|
|
|
|
|
2022-12-15 12:23:57 +08:00
|
|
|
|
import { BaseEmpty } from "../base";
|
2024-11-22 09:22:44 +08:00
|
|
|
|
import { ScrollTopButton } from "../layout/scroll-top-button";
|
2025-09-18 23:34:21 +08:00
|
|
|
|
|
2025-09-15 07:44:54 +08:00
|
|
|
|
import { ProxyChain } from "./proxy-chain";
|
2025-09-18 23:34:21 +08:00
|
|
|
|
import { ProxyRender } from "./proxy-render";
|
|
|
|
|
|
import { useRenderList } from "./use-render-list";
|
|
|
|
|
|
|
|
|
|
|
|
import { useProxySelection } from "@/hooks/use-proxy-selection";
|
|
|
|
|
|
import { useVerge } from "@/hooks/use-verge";
|
|
|
|
|
|
import { providerHealthCheck, getGroupProxyDelays } from "@/services/cmds";
|
|
|
|
|
|
import delayManager from "@/services/delay";
|
2022-11-20 19:46:16 +08:00
|
|
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
|
|
mode: string;
|
2025-09-15 07:44:54 +08:00
|
|
|
|
isChainMode?: boolean;
|
|
|
|
|
|
chainConfigData?: string | null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface ProxyChainItem {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
type?: string;
|
|
|
|
|
|
delay?: number;
|
2022-11-20 19:46:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const ProxyGroups = (props: Props) => {
|
2024-06-26 05:33:06 +08:00
|
|
|
|
const { t } = useTranslation();
|
2025-09-15 07:44:54 +08:00
|
|
|
|
const { mode, isChainMode = false, chainConfigData } = props;
|
|
|
|
|
|
const [proxyChain, setProxyChain] = useState<ProxyChainItem[]>([]);
|
|
|
|
|
|
const [duplicateWarning, setDuplicateWarning] = useState<{
|
|
|
|
|
|
open: boolean;
|
|
|
|
|
|
message: string;
|
|
|
|
|
|
}>({ open: false, message: "" });
|
|
|
|
|
|
|
|
|
|
|
|
const { renderList, onProxies, onHeadState } = useRenderList(
|
|
|
|
|
|
mode,
|
|
|
|
|
|
isChainMode,
|
|
|
|
|
|
);
|
2022-11-20 19:46:16 +08:00
|
|
|
|
|
2022-11-20 20:12:58 +08:00
|
|
|
|
const { verge } = useVerge();
|
2025-09-01 11:30:27 +08:00
|
|
|
|
|
|
|
|
|
|
// 统代理选择
|
|
|
|
|
|
const { handleProxyGroupChange } = useProxySelection({
|
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
|
onProxies();
|
|
|
|
|
|
},
|
|
|
|
|
|
onError: (error) => {
|
|
|
|
|
|
console.error("代理切换失败", error);
|
|
|
|
|
|
onProxies();
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2025-06-11 13:21:24 +08:00
|
|
|
|
|
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 16:27:06 +08:00
|
|
|
|
const scrollerRef = useRef<Element | null>(null);
|
2025-02-17 14:24:33 +08:00
|
|
|
|
|
|
|
|
|
|
// 从 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]);
|
|
|
|
|
|
|
2025-03-01 08:31:31 +08:00
|
|
|
|
// 改为使用节流函数保存滚动位置
|
2025-02-17 14:24:33 +08:00
|
|
|
|
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-03-01 08:31:31 +08:00
|
|
|
|
// 使用改进的滚动处理
|
2025-02-17 14:24:33 +08:00
|
|
|
|
const handleScroll = useCallback(
|
2025-03-01 08:31:31 +08:00
|
|
|
|
throttle((e: any) => {
|
2025-02-17 14:24:33 +08:00
|
|
|
|
const scrollTop = e.target.scrollTop;
|
|
|
|
|
|
setShowScrollTop(scrollTop > 100);
|
2025-03-01 08:31:31 +08:00
|
|
|
|
// 使用稳定的节流来保存位置,而不是setTimeout
|
2025-02-17 14:24:33 +08:00
|
|
|
|
saveScrollPosition(scrollTop);
|
2025-03-01 08:31:31 +08:00
|
|
|
|
}, 500), // 增加到500ms以确保平滑滚动
|
2025-02-17 14:24:33 +08:00
|
|
|
|
[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
|
|
|
|
|
2025-09-15 07:44:54 +08:00
|
|
|
|
// 关闭重复节点警告
|
|
|
|
|
|
const handleCloseDuplicateWarning = useCallback(() => {
|
|
|
|
|
|
setDuplicateWarning({ open: false, message: "" });
|
|
|
|
|
|
}, []);
|
2025-02-17 16:07:46 +08:00
|
|
|
|
|
2025-09-01 11:30:27 +08:00
|
|
|
|
const handleChangeProxy = useCallback(
|
|
|
|
|
|
(group: IProxyGroupItem, proxy: IProxyItem) => {
|
2025-09-15 07:44:54 +08:00
|
|
|
|
if (isChainMode) {
|
|
|
|
|
|
// 使用函数式更新来避免状态延迟问题
|
|
|
|
|
|
setProxyChain((prev) => {
|
|
|
|
|
|
// 检查是否已经存在相同名称的代理,防止重复添加
|
|
|
|
|
|
if (prev.some((item) => item.name === proxy.name)) {
|
|
|
|
|
|
const warningMessage = t("Proxy node already exists in chain");
|
|
|
|
|
|
setDuplicateWarning({
|
|
|
|
|
|
open: true,
|
|
|
|
|
|
message: warningMessage,
|
|
|
|
|
|
});
|
|
|
|
|
|
return prev; // 返回原来的状态,不做任何更改
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 安全获取延迟数据,如果没有延迟数据则设为 undefined
|
|
|
|
|
|
const delay =
|
|
|
|
|
|
proxy.history && proxy.history.length > 0
|
|
|
|
|
|
? proxy.history[proxy.history.length - 1].delay
|
|
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
|
|
|
|
const chainItem: ProxyChainItem = {
|
|
|
|
|
|
id: `${proxy.name}_${Date.now()}`,
|
|
|
|
|
|
name: proxy.name,
|
|
|
|
|
|
type: proxy.type,
|
|
|
|
|
|
delay: delay,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return [...prev, chainItem];
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-04-09 13:15:45 +08:00
|
|
|
|
if (!["Selector", "URLTest", "Fallback"].includes(group.type)) return;
|
2022-11-20 19:46:16 +08:00
|
|
|
|
|
2025-09-01 11:30:27 +08:00
|
|
|
|
handleProxyGroupChange(group, proxy);
|
2024-11-22 09:22:44 +08:00
|
|
|
|
},
|
2025-09-15 07:44:54 +08:00
|
|
|
|
[handleProxyGroupChange, isChainMode, t],
|
2022-11-20 19:46:16 +08:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 测全部延迟
|
|
|
|
|
|
const handleCheckAll = useLockFn(async (groupName: string) => {
|
2025-03-09 04:22:34 +08:00
|
|
|
|
console.log(`[ProxyGroups] 开始测试所有延迟,组: ${groupName}`);
|
|
|
|
|
|
|
2022-11-20 19:46:16 +08:00
|
|
|
|
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);
|
|
|
|
|
|
|
2025-03-09 04:22:34 +08:00
|
|
|
|
console.log(`[ProxyGroups] 找到代理数量: ${proxies.length}`);
|
|
|
|
|
|
|
2022-11-20 19:46:16 +08:00
|
|
|
|
const providers = new Set(proxies.map((p) => p!.provider!).filter(Boolean));
|
|
|
|
|
|
|
|
|
|
|
|
if (providers.size) {
|
2025-03-09 04:22:34 +08:00
|
|
|
|
console.log(`[ProxyGroups] 发现提供者,数量: ${providers.size}`);
|
2022-11-20 19:46:16 +08:00
|
|
|
|
Promise.allSettled(
|
2024-11-22 09:22:44 +08:00
|
|
|
|
[...providers].map((p) => providerHealthCheck(p)),
|
2025-03-09 04:22:34 +08:00
|
|
|
|
).then(() => {
|
|
|
|
|
|
console.log(`[ProxyGroups] 提供者健康检查完成`);
|
|
|
|
|
|
onProxies();
|
|
|
|
|
|
});
|
2022-11-20 19:46:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const names = proxies.filter((p) => !p!.provider).map((p) => p!.name);
|
2025-03-09 04:22:34 +08:00
|
|
|
|
console.log(`[ProxyGroups] 过滤后需要测试的代理数量: ${names.length}`);
|
2024-04-09 13:15:45 +08:00
|
|
|
|
|
2025-03-09 04:22:34 +08:00
|
|
|
|
const url = delayManager.getUrl(groupName);
|
|
|
|
|
|
console.log(`[ProxyGroups] 测试URL: ${url}, 超时: ${timeout}ms`);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
await Promise.race([
|
|
|
|
|
|
delayManager.checkListDelay(names, groupName, timeout),
|
|
|
|
|
|
getGroupProxyDelays(groupName, url, timeout).then((result) => {
|
|
|
|
|
|
console.log(
|
|
|
|
|
|
`[ProxyGroups] getGroupProxyDelays返回结果数量:`,
|
|
|
|
|
|
Object.keys(result || {}).length,
|
|
|
|
|
|
);
|
|
|
|
|
|
}), // 查询group delays 将清除fixed(不关注调用结果)
|
|
|
|
|
|
]);
|
|
|
|
|
|
console.log(`[ProxyGroups] 延迟测试完成,组: ${groupName}`);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error(`[ProxyGroups] 延迟测试出错,组: ${groupName}`, error);
|
|
|
|
|
|
}
|
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
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-15 07:44:54 +08:00
|
|
|
|
if (isChainMode) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Box sx={{ display: "flex", height: "100%", gap: 2 }}>
|
|
|
|
|
|
<Box sx={{ flex: 1, position: "relative" }}>
|
|
|
|
|
|
<Virtuoso
|
|
|
|
|
|
ref={virtuosoRef}
|
|
|
|
|
|
style={{ height: "calc(100% - 14px)" }}
|
|
|
|
|
|
totalCount={renderList.length}
|
|
|
|
|
|
increaseViewportBy={{ top: 200, bottom: 200 }}
|
|
|
|
|
|
overscan={150}
|
|
|
|
|
|
defaultItemHeight={56}
|
|
|
|
|
|
scrollerRef={(ref) => {
|
|
|
|
|
|
scrollerRef.current = ref as Element;
|
|
|
|
|
|
}}
|
|
|
|
|
|
components={{
|
|
|
|
|
|
Footer: () => <div style={{ height: "8px" }} />,
|
|
|
|
|
|
}}
|
|
|
|
|
|
initialScrollTop={scrollPositionRef.current[mode]}
|
|
|
|
|
|
computeItemKey={(index) => renderList[index].key}
|
|
|
|
|
|
itemContent={(index) => (
|
|
|
|
|
|
<ProxyRender
|
|
|
|
|
|
key={renderList[index].key}
|
|
|
|
|
|
item={renderList[index]}
|
|
|
|
|
|
indent={mode === "rule" || mode === "script"}
|
|
|
|
|
|
onLocation={handleLocation}
|
|
|
|
|
|
onCheckAll={handleCheckAll}
|
|
|
|
|
|
onHeadState={onHeadState}
|
|
|
|
|
|
onChangeProxy={handleChangeProxy}
|
|
|
|
|
|
isChainMode={isChainMode}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<ScrollTopButton show={showScrollTop} onClick={scrollToTop} />
|
|
|
|
|
|
</Box>
|
|
|
|
|
|
|
|
|
|
|
|
<Box sx={{ width: "400px", minWidth: "300px" }}>
|
|
|
|
|
|
<ProxyChain
|
|
|
|
|
|
proxyChain={proxyChain}
|
|
|
|
|
|
onUpdateChain={setProxyChain}
|
|
|
|
|
|
chainConfigData={chainConfigData}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Box>
|
|
|
|
|
|
</Box>
|
|
|
|
|
|
|
|
|
|
|
|
<Snackbar
|
|
|
|
|
|
open={duplicateWarning.open}
|
|
|
|
|
|
autoHideDuration={3000}
|
|
|
|
|
|
onClose={handleCloseDuplicateWarning}
|
|
|
|
|
|
anchorOrigin={{ vertical: "top", horizontal: "center" }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Alert
|
|
|
|
|
|
onClose={handleCloseDuplicateWarning}
|
|
|
|
|
|
severity="warning"
|
|
|
|
|
|
variant="filled"
|
|
|
|
|
|
>
|
|
|
|
|
|
{duplicateWarning.message}
|
|
|
|
|
|
</Alert>
|
|
|
|
|
|
</Snackbar>
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2022-11-20 19:46:16 +08:00
|
|
|
|
return (
|
2025-03-01 08:31:31 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{ position: "relative", height: "100%", willChange: "transform" }}
|
|
|
|
|
|
>
|
2024-11-22 09:22:44 +08:00
|
|
|
|
<Virtuoso
|
|
|
|
|
|
ref={virtuosoRef}
|
2025-02-20 14:21:55 +08:00
|
|
|
|
style={{ height: "calc(100% - 14px)" }}
|
2024-11-22 09:22:44 +08:00
|
|
|
|
totalCount={renderList.length}
|
2025-03-01 08:31:31 +08:00
|
|
|
|
increaseViewportBy={{ top: 200, bottom: 200 }}
|
2025-02-17 14:30:21 +08:00
|
|
|
|
overscan={150}
|
|
|
|
|
|
defaultItemHeight={56}
|
2024-11-22 09:22:44 +08:00
|
|
|
|
scrollerRef={(ref) => {
|
2025-02-17 16:27:06 +08:00
|
|
|
|
scrollerRef.current = ref as Element;
|
2024-11-22 09:22:44 +08:00
|
|
|
|
}}
|
2025-02-17 14:30:21 +08:00
|
|
|
|
components={{
|
2025-03-02 04:08:13 +08:00
|
|
|
|
Footer: () => <div style={{ height: "8px" }} />,
|
2025-02-17 14:30:21 +08:00
|
|
|
|
}}
|
2025-03-01 08:31:31 +08:00
|
|
|
|
// 添加平滑滚动设置
|
|
|
|
|
|
initialScrollTop={scrollPositionRef.current[mode]}
|
|
|
|
|
|
computeItemKey={(index) => renderList[index].key}
|
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
|
|
|
|
);
|
|
|
|
|
|
};
|
2025-02-17 16:07:46 +08:00
|
|
|
|
|
2025-03-01 08:31:31 +08:00
|
|
|
|
// 替换简单防抖函数为更优的节流函数
|
|
|
|
|
|
function throttle<T extends (...args: any[]) => any>(
|
|
|
|
|
|
func: T,
|
|
|
|
|
|
wait: number,
|
|
|
|
|
|
): (...args: Parameters<T>) => void {
|
|
|
|
|
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
|
|
let previous = 0;
|
|
|
|
|
|
|
|
|
|
|
|
return function (...args: Parameters<T>) {
|
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
|
const remaining = wait - (now - previous);
|
|
|
|
|
|
|
|
|
|
|
|
if (remaining <= 0 || remaining > wait) {
|
|
|
|
|
|
if (timer) {
|
|
|
|
|
|
clearTimeout(timer);
|
|
|
|
|
|
timer = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
previous = now;
|
|
|
|
|
|
func(...args);
|
|
|
|
|
|
} else if (!timer) {
|
|
|
|
|
|
timer = setTimeout(() => {
|
|
|
|
|
|
previous = Date.now();
|
|
|
|
|
|
timer = null;
|
|
|
|
|
|
func(...args);
|
|
|
|
|
|
}, remaining);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|