diff options
| author | Larvan2 <[email protected]> | 2026-06-04 18:43:18 +0800 |
|---|---|---|
| committer | Larvan2 <[email protected]> | 2026-06-04 18:43:18 +0800 |
| commit | 120f06c59ef7e1514baa3cdf81dec79c7fa6e1e6 (patch) | |
| tree | 49d75bb660b922d64a3f6c5e516c740533ed8c75 /src/components | |
| parent | 4dcd2231ee74f2ea7dd8a9a556208a0e2efc7377 (diff) | |
feat: proxy group by providers
Diffstat (limited to 'src/components')
| -rw-r--r-- | src/components/proxies/Proxies.tsx | 2 | ||||
| -rw-r--r-- | src/components/proxies/ProxyGroup.tsx | 48 | ||||
| -rw-r--r-- | src/components/proxies/ProxyList.module.scss | 13 | ||||
| -rw-r--r-- | src/components/proxies/ProxyList.tsx | 60 | ||||
| -rw-r--r-- | src/components/proxies/Settings.tsx | 15 |
5 files changed, 120 insertions, 18 deletions
diff --git a/src/components/proxies/Proxies.tsx b/src/components/proxies/Proxies.tsx index aae829f..e8e3d4b 100644 --- a/src/components/proxies/Proxies.tsx +++ b/src/components/proxies/Proxies.tsx @@ -27,6 +27,7 @@ type AppConfig = { hideUnavailableProxies: boolean; autoCloseOldConns: boolean; proxiesLayout: string; + proxyGroupByProvider: boolean; }; type Props = { @@ -98,6 +99,7 @@ export default function Proxies({ proxySortBy={appConfig.proxySortBy} isOpen={Boolean(collapsibleIsOpen[`proxyGroup:${name}`])} latencyTestUrl={latencyTestUrl} + proxyGroupByProvider={appConfig.proxyGroupByProvider} /> </div> ))} diff --git a/src/components/proxies/ProxyGroup.tsx b/src/components/proxies/ProxyGroup.tsx index 7b751fb..b493ff1 100644 --- a/src/components/proxies/ProxyGroup.tsx +++ b/src/components/proxies/ProxyGroup.tsx @@ -17,7 +17,7 @@ import CollapsibleSectionHeader from '../CollapsibleSectionHeader'; import { useStoreActions } from '../StateProvider'; import s0 from './ProxyGroup.module.scss'; -import { ProxyList, ProxyListSummaryView } from './ProxyList'; +import { ProxyList, ProxyListGroupedByProvider, ProxyListSummaryView } from './ProxyList'; const { memo, useCallback, useLayoutEffect, useMemo, useRef, useState } = React; @@ -94,6 +94,7 @@ type Props = { latencyTestUrl: string; apiConfig: ClashAPIConfig; dispatch: DispatchFn; + proxyGroupByProvider?: boolean; }; export const ProxyGroup = memo(function ProxyGroup({ @@ -106,6 +107,7 @@ export const ProxyGroup = memo(function ProxyGroup({ latencyTestUrl, apiConfig, dispatch, + proxyGroupByProvider = false, }: Props) { const group = proxies[name] as ProxyItem & { all?: string[]; now?: string }; const { all: allItems = [], type, now } = group || {}; @@ -115,18 +117,18 @@ export const ProxyGroup = memo(function ProxyGroup({ const nowChain = useMemo(() => buildNowChain(proxies, name), [proxies, name]); const nowLatency = useMemo( () => (now ? getProxyLatency(proxies, delay, now) : undefined), - [proxies, delay, now] + [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) + fetchVersion('/version', apiConfig), ); const isSelectable = useMemo( () => ['Selector', version.meta && 'Fallback', version.meta && 'URLTest'].includes(type), - [type, version.meta] + [type, version.meta], ); const { @@ -143,7 +145,7 @@ export const ProxyGroup = memo(function ProxyGroup({ if (!isSelectable) return; dispatch(switchProxy(apiConfig, name, proxyName)); }, - [apiConfig, dispatch, name, isSelectable] + [apiConfig, dispatch, name, isSelectable], ); const [isTestingLatency, setIsTestingLatency] = useState(false); @@ -206,17 +208,31 @@ export const ProxyGroup = memo(function ProxyGroup({ </div> </div> <Collapsible isOpen={isOpen}> - <ProxyList - apiConfig={apiConfig} - all={all} - delay={delay} - dispatch={dispatch} - latencyTestUrl={latencyTestUrl} - now={now} - isSelectable={isSelectable} - itemOnTapCallback={itemOnTapCallback} - proxies={proxies} - /> + {proxyGroupByProvider ? ( + <ProxyListGroupedByProvider + apiConfig={apiConfig} + all={all} + delay={delay} + dispatch={dispatch} + latencyTestUrl={latencyTestUrl} + now={now} + isSelectable={isSelectable} + itemOnTapCallback={itemOnTapCallback} + proxies={proxies} + /> + ) : ( + <ProxyList + apiConfig={apiConfig} + all={all} + delay={delay} + dispatch={dispatch} + latencyTestUrl={latencyTestUrl} + now={now} + isSelectable={isSelectable} + itemOnTapCallback={itemOnTapCallback} + proxies={proxies} + /> + )} </Collapsible> <Collapsible isOpen={!isOpen}> {nowChain && ( diff --git a/src/components/proxies/ProxyList.module.scss b/src/components/proxies/ProxyList.module.scss index f4c8d87..f4b1211 100644 --- a/src/components/proxies/ProxyList.module.scss +++ b/src/components/proxies/ProxyList.module.scss @@ -18,3 +18,16 @@ grid-template-columns: repeat(auto-fill, 15px); padding-left: 10px; } + +.providerGroup { + margin-top: 8px; +} + +.providerLabel { + font-size: 0.75rem; + font-weight: 600; + color: var(--color-text-secondary, #909399); + padding: 2px 0 4px; + border-bottom: 1px solid var(--color-separator); + margin-bottom: 4px; +} diff --git a/src/components/proxies/ProxyList.tsx b/src/components/proxies/ProxyList.tsx index 1782a8f..aee7ac6 100644 --- a/src/components/proxies/ProxyList.tsx +++ b/src/components/proxies/ProxyList.tsx @@ -105,3 +105,63 @@ export function ProxyListSummaryView({ </div> ); } + +export function ProxyListGroupedByProvider({ + all, + proxies, + delay, + latencyTestUrl, + apiConfig, + dispatch, + now, + isSelectable, + itemOnTapCallback, +}: ProxyListProps) { + const httpsLatencyTest = latencyTestUrl.startsWith('https://'); + + // Group proxy names by their providerName + const groups: { label: string; names: string[] }[] = React.useMemo(() => { + const map = new Map<string, string[]>(); + for (const proxyName of all) { + const providerName = proxies[proxyName]?.providerName ?? ''; + if (!map.has(providerName)) map.set(providerName, []); + map.get(providerName)!.push(proxyName); + } + return Array.from(map.entries()).map(([label, names]) => ({ label, names })); + }, [all, proxies]); + + return ( + <div> + {groups.map(({ label, names }) => ( + <div key={label} className={s.providerGroup}> + {label ? <div className={s.providerLabel}>{label}</div> : null} + <div className={cx(s.list, s.detail)}> + {names.map((proxyName) => { + const proxy = proxies[proxyName] || { + name: proxyName, + type: 'Http' as const, + udp: false, + tfo: false, + history: [], + }; + return ( + <Proxy + apiConfig={apiConfig} + dispatch={dispatch} + proxy={proxy} + latency={getProxyLatency(proxies, delay, proxyName)} + httpsLatencyTest={httpsLatencyTest} + key={proxyName} + onClick={itemOnTapCallback} + isSelectable={isSelectable} + name={proxyName} + now={proxyName === now} + /> + ); + })} + </div> + </div> + ))} + </div> + ); +} diff --git a/src/components/proxies/Settings.tsx b/src/components/proxies/Settings.tsx index 5cf90f8..d61925a 100644 --- a/src/components/proxies/Settings.tsx +++ b/src/components/proxies/Settings.tsx @@ -16,6 +16,7 @@ type AppConfig = { hideUnavailableProxies: boolean; autoCloseOldConns: boolean; proxiesLayout: string; + proxyGroupByProvider: boolean; }; type Props = { @@ -31,14 +32,14 @@ export default function Settings({ appConfig }: Props) { (e) => { updateAppConfig('proxySortBy', e.target.value); }, - [updateAppConfig] + [updateAppConfig], ); const handleHideUnavailablesSwitchOnChange = useCallback( (v) => { updateAppConfig('hideUnavailableProxies', v); }, - [updateAppConfig] + [updateAppConfig], ); const { t } = useTranslation(); return ( @@ -86,6 +87,16 @@ export default function Settings({ appConfig }: Props) { /> </div> </div> + <div className={s.labeledInput}> + <span>{t('group_by_provider')}</span> + <div> + <Switch + name="proxyGroupByProvider" + checked={appConfig.proxyGroupByProvider} + onChange={(v) => updateAppConfig('proxyGroupByProvider', v)} + /> + </div> + </div> </> ); } |
