summaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
authorLarvan2 <[email protected]>2026-06-04 18:43:18 +0800
committerLarvan2 <[email protected]>2026-06-04 18:43:18 +0800
commit120f06c59ef7e1514baa3cdf81dec79c7fa6e1e6 (patch)
tree49d75bb660b922d64a3f6c5e516c740533ed8c75 /src/components
parent4dcd2231ee74f2ea7dd8a9a556208a0e2efc7377 (diff)
feat: proxy group by providers
Diffstat (limited to 'src/components')
-rw-r--r--src/components/proxies/Proxies.tsx2
-rw-r--r--src/components/proxies/ProxyGroup.tsx48
-rw-r--r--src/components/proxies/ProxyList.module.scss13
-rw-r--r--src/components/proxies/ProxyList.tsx60
-rw-r--r--src/components/proxies/Settings.tsx15
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>
</>
);
}