import cx from 'clsx';
import * as React from 'react';
import { ChevronDown, Zap } from '~/components/shared/FeatherIcons';
import { useQuery } from 'react-query';
import * as proxiesAPI from '~/api/proxies';
import { fetchVersion } from '~/api/version';
import { useFilteredAndSorted } from '~/modules/proxies/hooks';
import { getProxyLatency } from '~/modules/proxies/utils';
import { fetchProxies, switchProxy } from '~/store/proxies';
import { DelayMapping, DispatchFn, ProxiesMapping, ProxyItem } from '~/store/types';
import { ClashAPIConfig } from '~/types';
import Button from '../Button';
import Collapsible from '../Collapsible';
import CollapsibleSectionHeader from '../CollapsibleSectionHeader';
import { useStoreActions } from '../StateProvider';
import s0 from './ProxyGroup.module.scss';
import { ProxyList, ProxyListGroupedByProvider, ProxyListSummaryView } from './ProxyList';
const { memo, useCallback, useLayoutEffect, useMemo, useRef, useState } = React;
function buildNowChain(proxies: ProxiesMapping, groupName: string): string | null {
const group = proxies[groupName] as ProxyItem & { now?: string };
if (!group?.now) return null;
const parts: string[] = [group.now];
let current = proxies[group.now] as ProxyItem & { now?: string };
let depth = 0;
while (current?.now && depth < 3) {
const next = proxies[current.now];
if (!next) break;
parts.push(current.now);
current = next as ProxyItem & { now?: string };
depth++;
}
return parts.join(' ⊙ ');
}
function countAvailableProxies(names: string[], delay: DelayMapping): number {
return names.filter((name) => {
const d = delay[name];
return d && typeof d.number === 'number' && d.number > 0;
}).length;
}
function getLatencyColor(number: number | undefined, httpsTest: boolean): string {
if (!number || number === 0) return '#909399';
const good = httpsTest ? 800 : 200;
const normal = httpsTest ? 1500 : 500;
if (number < good) return '#67c23a';
if (number < normal) return '#d4b75c';
return '#e67f3c';
}
function ZapWrapper() {
return (
);
}
const ProxyAvailabilityBar = memo(function ProxyAvailabilityBar({
all,
delay,
}: {
all: string[];
delay: DelayMapping;
}) {
const total = all.length;
const available = useMemo(() => countAvailableProxies(all, delay), [all, delay]);
const pct = total > 0 ? Math.round((available / total) * 100) : 0;
return (
);
});
type Props = {
name: string;
delay: DelayMapping;
hideUnavailableProxies: boolean;
proxySortBy: string;
proxies: ProxiesMapping;
isOpen: boolean;
latencyTestUrl: string;
latencyTestTimeout?: number;
apiConfig: ClashAPIConfig;
dispatch: DispatchFn;
proxyGroupByProvider?: boolean;
};
export const ProxyGroup = memo(function ProxyGroup({
name,
delay,
hideUnavailableProxies,
proxySortBy,
proxies,
isOpen,
latencyTestUrl,
latencyTestTimeout = 5000,
apiConfig,
dispatch,
proxyGroupByProvider = false,
}: Props) {
const group = proxies[name] as ProxyItem & { all?: string[]; now?: string };
const { all: allItems = [], type, now } = group || {};
const all = useFilteredAndSorted(allItems, delay, hideUnavailableProxies, proxySortBy, proxies);
const httpsLatencyTest = latencyTestUrl.startsWith('https://');
const nowChain = useMemo(() => buildNowChain(proxies, name), [proxies, name]);
const nowLatency = useMemo(
() => (now ? getProxyLatency(proxies, delay, now) : undefined),
[proxies, delay, now],
);
const availableCount = useMemo(() => countAvailableProxies(allItems, delay), [allItems, delay]);
const qtyLabel = `${availableCount}/${allItems.length}`;
const { data: version } = useQuery(['/version', apiConfig], () =>
fetchVersion('/version', apiConfig),
);
const isSelectable = useMemo(
() => ['Selector', version.meta && 'Fallback', version.meta && 'URLTest'].includes(type),
[type, version.meta],
);
const {
app: { updateCollapsibleIsOpen },
proxies: { requestDelayForProxies },
} = useStoreActions();
const toggle = useCallback(() => {
updateCollapsibleIsOpen('proxyGroup', name, !isOpen);
}, [isOpen, updateCollapsibleIsOpen, name]);
const itemOnTapCallback = useCallback(
(proxyName) => {
if (!isSelectable) return;
dispatch(switchProxy(apiConfig, name, proxyName));
},
[apiConfig, dispatch, name, isSelectable],
);
const [isTestingLatency, setIsTestingLatency] = useState(false);
// measure collapsed container to decide dots vs bar
const summaryContainerRef = useRef(null);
const [containerWidth, setContainerWidth] = useState(0);
useLayoutEffect(() => {
const el = summaryContainerRef.current;
if (!el) return;
// sync read before first paint to avoid flash
const w = el.offsetWidth;
if (w > 0) setContainerWidth(w);
const ro = new ResizeObserver((entries) => {
setContainerWidth(entries[0].contentRect.width);
});
ro.observe(el);
return () => ro.disconnect();
}, []);
// dot slot = 15px width + 10px gap; padding-left:10px eats into available space
// n items fit when 15 + (n-1)*25 <= containerWidth - 10 → n <= containerWidth/25
const dotsPerRow = containerWidth > 0 ? Math.floor(containerWidth / 25) : Infinity;
const showBar = all.length > dotsPerRow;
const testLatency = useCallback(async () => {
setIsTestingLatency(true);
try {
if (version.meta === true) {
await proxiesAPI.requestDelayForProxyGroup(apiConfig, name, latencyTestUrl, latencyTestTimeout);
await dispatch(fetchProxies(apiConfig));
} else {
await requestDelayForProxies(apiConfig, all);
await dispatch(fetchProxies(apiConfig));
}
} catch (err) {}
setIsTestingLatency(false);
}, [all, apiConfig, dispatch, name, version.meta, latencyTestUrl, latencyTestTimeout, requestDelayForProxies]);
return (
{proxyGroupByProvider ? (
) : (
)}
{nowChain && (
⊙ {nowChain}
{nowLatency?.number ? (
{nowLatency.number} ms
) : null}
)}
);
});