feat: add test latency & select tag functionality

This commit is contained in:
divocat
2025-10-06 20:58:55 +03:00
parent 6117b0ef9b
commit caf82b096f
8 changed files with 197 additions and 28 deletions

View File

@@ -4,3 +4,4 @@ export * from './getGroupDelay';
export * from './getProxies'; export * from './getProxies';
export * from './getVersion'; export * from './getVersion';
export * from './triggerProxySelector'; export * from './triggerProxySelector';
export * from './triggerLatencyTest';

View File

@@ -0,0 +1,35 @@
import { IBaseApiResponse } from '../types';
import { createBaseApiRequest } from './createBaseApiRequest';
import { getClashApiUrl } from '../../helpers';
export async function triggerLatencyGroupTest(
tag: string,
timeout: number = 2000,
url: string = 'https://www.gstatic.com/generate_204',
): Promise<IBaseApiResponse<void>> {
return createBaseApiRequest<void>(() =>
fetch(
`${getClashApiUrl()}/group/${tag}/delay?url=${encodeURIComponent(url)}&timeout=${timeout}`,
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
},
),
);
}
export async function triggerLatencyProxyTest(
tag: string,
timeout: number = 2000,
url: string = 'https://www.gstatic.com/generate_204',
): Promise<IBaseApiResponse<void>> {
return createBaseApiRequest<void>(() =>
fetch(
`${getClashApiUrl()}/proxies/${tag}/delay?url=${encodeURIComponent(url)}&timeout=${timeout}`,
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
},
),
);
}

View File

@@ -9,6 +9,11 @@ import { store } from '../store';
import { socket } from '../socket'; import { socket } from '../socket';
import { renderDashboardWidget } from './renderer/renderWidget'; import { renderDashboardWidget } from './renderer/renderWidget';
import { prettyBytes } from '../helpers/prettyBytes'; import { prettyBytes } from '../helpers/prettyBytes';
import {
triggerLatencyGroupTest,
triggerLatencyProxyTest,
triggerProxySelector,
} from '../clash';
// Fetchers // Fetchers
@@ -63,11 +68,40 @@ async function connectToClashSockets() {
// Renderer // Renderer
async function handleChooseOutbound(selector: string, tag: string) {
await triggerProxySelector(selector, tag);
await fetchDashboardSections();
}
async function handleTestGroupLatency(tag: string) {
await triggerLatencyGroupTest(tag);
await fetchDashboardSections();
}
async function handleTestProxyLatency(tag: string) {
await triggerLatencyProxyTest(tag);
await fetchDashboardSections();
}
async function renderDashboardSections() { async function renderDashboardSections() {
const sections = store.get().sections; const sections = store.get().sections;
console.log('render dashboard sections group'); console.log('render dashboard sections group');
const container = document.getElementById('dashboard-sections-grid'); const container = document.getElementById('dashboard-sections-grid');
const renderedOutboundGroups = sections.map(renderOutboundGroup); const renderedOutboundGroups = sections.map((section) =>
renderOutboundGroup({
section,
onTestLatency: (tag) => {
if (section.withTagSelect) {
return handleTestGroupLatency(tag);
}
return handleTestProxyLatency(tag);
},
onChooseOutbound: (selector, tag) => {
handleChooseOutbound(selector, tag);
},
}),
);
container!.replaceChildren(...renderedOutboundGroups); container!.replaceChildren(...renderedOutboundGroups);
} }

View File

@@ -1,12 +1,28 @@
import { Podkop } from '../../podkop/types'; import { Podkop } from '../../podkop/types';
interface IRenderOutboundGroupProps {
section: Podkop.OutboundGroup;
onTestLatency: (tag: string) => void;
onChooseOutbound: (selector: string, tag: string) => void;
}
export function renderOutboundGroup({ export function renderOutboundGroup({
outbounds, section,
displayName, onTestLatency,
}: Podkop.OutboundGroup) { onChooseOutbound,
}: IRenderOutboundGroupProps) {
function testLatency() {
if (section.withTagSelect) {
return onTestLatency(section.code);
}
if (section.outbounds.length) {
return onTestLatency(section.outbounds[0].code);
}
}
function renderOutbound(outbound: Podkop.Outbound) { function renderOutbound(outbound: Podkop.Outbound) {
function getLatencyClass() { function getLatencyClass() {
if (!outbound.latency) { if (!outbound.latency) {
return 'pdk_dashboard-page__outbound-grid__item__latency--empty'; return 'pdk_dashboard-page__outbound-grid__item__latency--empty';
} }
@@ -25,7 +41,10 @@ export function renderOutboundGroup({
return E( return E(
'div', 'div',
{ {
class: `pdk_dashboard-page__outbound-grid__item ${outbound.selected ? 'pdk_dashboard-page__outbound-grid__item--active' : ''}`, class: `pdk_dashboard-page__outbound-grid__item ${outbound.selected ? 'pdk_dashboard-page__outbound-grid__item--active' : ''} ${section.withTagSelect ? 'pdk_dashboard-page__outbound-grid__item--selectable' : ''}`,
click: () =>
section.withTagSelect &&
onChooseOutbound(section.code, outbound.code),
}, },
[ [
E('b', {}, outbound.displayName), E('b', {}, outbound.displayName),
@@ -53,14 +72,14 @@ export function renderOutboundGroup({
{ {
class: 'pdk_dashboard-page__outbound-section__title-section__title', class: 'pdk_dashboard-page__outbound-section__title-section__title',
}, },
displayName, section.displayName,
), ),
E('button', { class: 'btn' }, 'Test latency'), E('button', { class: 'btn', click: () => testLatency() }, 'Test latency'),
]), ]),
E( E(
'div', 'div',
{ class: 'pdk_dashboard-page__outbound-grid' }, { class: 'pdk_dashboard-page__outbound-grid' },
outbounds.map((outbound) => renderOutbound(outbound)), section.outbounds.map((outbound) => renderOutbound(outbound)),
), ),
]); ]);
} }

View File

@@ -28,7 +28,8 @@ export async function getDashboardSections(): Promise<Podkop.OutboundGroup[]> {
); );
return { return {
code: section['.name'], withTagSelect: false,
code: outbound?.code || section['.name'],
displayName: section['.name'], displayName: section['.name'],
outbounds: [ outbounds: [
{ {
@@ -51,7 +52,8 @@ export async function getDashboardSections(): Promise<Podkop.OutboundGroup[]> {
); );
return { return {
code: section['.name'], withTagSelect: false,
code: outbound?.code || section['.name'],
displayName: section['.name'], displayName: section['.name'],
outbounds: [ outbounds: [
{ {
@@ -90,7 +92,8 @@ export async function getDashboardSections(): Promise<Podkop.OutboundGroup[]> {
})); }));
return { return {
code: section['.name'], withTagSelect: true,
code: selector?.code || section['.name'],
displayName: section['.name'], displayName: section['.name'],
outbounds: [ outbounds: [
{ {
@@ -112,7 +115,8 @@ export async function getDashboardSections(): Promise<Podkop.OutboundGroup[]> {
); );
return { return {
code: section['.name'], withTagSelect: false,
code: outbound?.code || section['.name'],
displayName: section['.name'], displayName: section['.name'],
outbounds: [ outbounds: [
{ {
@@ -127,6 +131,7 @@ export async function getDashboardSections(): Promise<Podkop.OutboundGroup[]> {
} }
return { return {
withTagSelect: false,
code: section['.name'], code: section['.name'],
displayName: section['.name'], displayName: section['.name'],
outbounds: [], outbounds: [],

View File

@@ -9,6 +9,7 @@ export namespace Podkop {
} }
export interface OutboundGroup { export interface OutboundGroup {
withTagSelect: boolean;
code: string; code: string;
displayName: string; displayName: string;
outbounds: Outbound[]; outbounds: Outbound[];

View File

@@ -122,13 +122,17 @@ export const GlobalStyles = `
} }
.pdk_dashboard-page__outbound-grid__item { .pdk_dashboard-page__outbound-grid__item {
cursor: pointer;
border: 2px var(--background-color-low) solid; border: 2px var(--background-color-low) solid;
border-radius: 4px; border-radius: 4px;
padding: 10px; padding: 10px;
transition: border 0.2s ease; transition: border 0.2s ease;
} }
.pdk_dashboard-page__outbound-grid__item:hover {
.pdk_dashboard-page__outbound-grid__item--selectable {
cursor: pointer;
}
.pdk_dashboard-page__outbound-grid__item--selectable:hover {
border-color: var(--primary-color-high); border-color: var(--primary-color-high);
} }

View File

@@ -470,13 +470,17 @@ var GlobalStyles = `
} }
.pdk_dashboard-page__outbound-grid__item { .pdk_dashboard-page__outbound-grid__item {
cursor: pointer;
border: 2px var(--background-color-low) solid; border: 2px var(--background-color-low) solid;
border-radius: 4px; border-radius: 4px;
padding: 10px; padding: 10px;
transition: border 0.2s ease; transition: border 0.2s ease;
} }
.pdk_dashboard-page__outbound-grid__item:hover {
.pdk_dashboard-page__outbound-grid__item--selectable {
cursor: pointer;
}
.pdk_dashboard-page__outbound-grid__item--selectable:hover {
border-color: var(--primary-color-high); border-color: var(--primary-color-high);
} }
@@ -855,6 +859,30 @@ async function triggerProxySelector(selector, outbound) {
); );
} }
// src/clash/methods/triggerLatencyTest.ts
async function triggerLatencyGroupTest(tag, timeout = 2e3, url = "https://www.gstatic.com/generate_204") {
return createBaseApiRequest(
() => fetch(
`${getClashApiUrl()}/group/${tag}/delay?url=${encodeURIComponent(url)}&timeout=${timeout}`,
{
method: "GET",
headers: { "Content-Type": "application/json" }
}
)
);
}
async function triggerLatencyProxyTest(tag, timeout = 2e3, url = "https://www.gstatic.com/generate_204") {
return createBaseApiRequest(
() => fetch(
`${getClashApiUrl()}/proxies/${tag}/delay?url=${encodeURIComponent(url)}&timeout=${timeout}`,
{
method: "GET",
headers: { "Content-Type": "application/json" }
}
)
);
}
// src/dashboard/renderDashboard.ts // src/dashboard/renderDashboard.ts
function renderDashboard() { function renderDashboard() {
return E( return E(
@@ -958,7 +986,8 @@ async function getDashboardSections() {
(proxy) => proxy.code === `${section[".name"]}-out` (proxy) => proxy.code === `${section[".name"]}-out`
); );
return { return {
code: section[".name"], withTagSelect: false,
code: outbound?.code || section[".name"],
displayName: section[".name"], displayName: section[".name"],
outbounds: [ outbounds: [
{ {
@@ -976,7 +1005,8 @@ async function getDashboardSections() {
(proxy) => proxy.code === `${section[".name"]}-out` (proxy) => proxy.code === `${section[".name"]}-out`
); );
return { return {
code: section[".name"], withTagSelect: false,
code: outbound?.code || section[".name"],
displayName: section[".name"], displayName: section[".name"],
outbounds: [ outbounds: [
{ {
@@ -1004,7 +1034,8 @@ async function getDashboardSections() {
selected: selector?.value?.now === item?.code selected: selector?.value?.now === item?.code
})); }));
return { return {
code: section[".name"], withTagSelect: true,
code: selector?.code || section[".name"],
displayName: section[".name"], displayName: section[".name"],
outbounds: [ outbounds: [
{ {
@@ -1024,7 +1055,8 @@ async function getDashboardSections() {
(proxy) => proxy.code === `${section[".name"]}-out` (proxy) => proxy.code === `${section[".name"]}-out`
); );
return { return {
code: section[".name"], withTagSelect: false,
code: outbound?.code || section[".name"],
displayName: section[".name"], displayName: section[".name"],
outbounds: [ outbounds: [
{ {
@@ -1038,6 +1070,7 @@ async function getDashboardSections() {
}; };
} }
return { return {
withTagSelect: false,
code: section[".name"], code: section[".name"],
displayName: section[".name"], displayName: section[".name"],
outbounds: [] outbounds: []
@@ -1073,9 +1106,18 @@ async function getSingboxStatus() {
// src/dashboard/renderer/renderOutboundGroup.ts // src/dashboard/renderer/renderOutboundGroup.ts
function renderOutboundGroup({ function renderOutboundGroup({
outbounds, section,
displayName onTestLatency,
onChooseOutbound
}) { }) {
function testLatency() {
if (section.withTagSelect) {
return onTestLatency(section.code);
}
if (section.outbounds.length) {
return onTestLatency(section.outbounds[0].code);
}
}
function renderOutbound(outbound) { function renderOutbound(outbound) {
function getLatencyClass() { function getLatencyClass() {
if (!outbound.latency) { if (!outbound.latency) {
@@ -1092,7 +1134,8 @@ function renderOutboundGroup({
return E( return E(
"div", "div",
{ {
class: `pdk_dashboard-page__outbound-grid__item ${outbound.selected ? "pdk_dashboard-page__outbound-grid__item--active" : ""}` class: `pdk_dashboard-page__outbound-grid__item ${outbound.selected ? "pdk_dashboard-page__outbound-grid__item--active" : ""} ${section.withTagSelect ? "pdk_dashboard-page__outbound-grid__item--selectable" : ""}`,
click: () => section.withTagSelect && onChooseOutbound(section.code, outbound.code)
}, },
[ [
E("b", {}, outbound.displayName), E("b", {}, outbound.displayName),
@@ -1119,14 +1162,14 @@ function renderOutboundGroup({
{ {
class: "pdk_dashboard-page__outbound-section__title-section__title" class: "pdk_dashboard-page__outbound-section__title-section__title"
}, },
displayName section.displayName
), ),
E("button", { class: "btn" }, "Test latency") E("button", { class: "btn", click: () => testLatency() }, "Test latency")
]), ]),
E( E(
"div", "div",
{ class: "pdk_dashboard-page__outbound-grid" }, { class: "pdk_dashboard-page__outbound-grid" },
outbounds.map((outbound) => renderOutbound(outbound)) section.outbounds.map((outbound) => renderOutbound(outbound))
) )
]); ]);
} }
@@ -1343,11 +1386,36 @@ async function connectToClashSockets() {
}); });
}); });
} }
async function handleChooseOutbound(selector, tag) {
await triggerProxySelector(selector, tag);
await fetchDashboardSections();
}
async function handleTestGroupLatency(tag) {
await triggerLatencyGroupTest(tag);
await fetchDashboardSections();
}
async function handleTestProxyLatency(tag) {
await triggerLatencyProxyTest(tag);
await fetchDashboardSections();
}
async function renderDashboardSections() { async function renderDashboardSections() {
const sections = store.get().sections; const sections = store.get().sections;
console.log("render dashboard sections group"); console.log("render dashboard sections group");
const container = document.getElementById("dashboard-sections-grid"); const container = document.getElementById("dashboard-sections-grid");
const renderedOutboundGroups = sections.map(renderOutboundGroup); const renderedOutboundGroups = sections.map(
(section) => renderOutboundGroup({
section,
onTestLatency: (tag) => {
if (section.withTagSelect) {
return handleTestGroupLatency(tag);
}
return handleTestProxyLatency(tag);
},
onChooseOutbound: (selector, tag) => {
handleChooseOutbound(selector, tag);
}
})
);
container.replaceChildren(...renderedOutboundGroups); container.replaceChildren(...renderedOutboundGroups);
} }
async function renderTrafficWidget() { async function renderTrafficWidget() {
@@ -1480,6 +1548,8 @@ return baseclass.extend({
onMount, onMount,
parseValueList, parseValueList,
renderDashboard, renderDashboard,
triggerLatencyGroupTest,
triggerLatencyProxyTest,
triggerProxySelector, triggerProxySelector,
validateDNS, validateDNS,
validateDomain, validateDomain,