Files
NeoTachyon/fe-app-forkop/src/forkop/tabs/dashboard/partials/renderSections.ts
T
2026-07-11 18:23:20 +03:00

515 lines
14 KiB
TypeScript

import {
renderLoaderCircleIcon24,
renderCopyIcon24,
renderLinkIcon24,
renderInfoIcon24,
} from '../../../../icons';
import { isCopyableProxyLink, svgEl } from '../../../../helpers';
import { prettyBytes } from '../../../../helpers/prettyBytes';
import { Forkop } from '../../../types';
interface IRenderSectionsProps {
loading: boolean;
failed: boolean;
section: Forkop.OutboundGroup;
onTestLatency: (tag: string | string[]) => void;
onChooseOutbound: (
sectionName: string,
selector: string,
tag: string,
) => void;
onCopyOutbound: (
section: Forkop.OutboundGroup,
outbound: Forkop.Outbound,
) => void;
onShowUrlTestInfo: (
section: Forkop.OutboundGroup,
outbound: Forkop.Outbound,
) => void;
onShowPriorityInfo: (
section: Forkop.OutboundGroup,
outbound: Forkop.Outbound,
) => void;
onUpdateSubscription: (section: Forkop.OutboundGroup) => void;
latencyFetching: boolean;
latencyProgress?: Forkop.LatencyActionProgress;
subscriptionUpdating: boolean;
selectorSwitchingTag?: string;
}
function renderFailedState() {
return E(
'div',
{
class: 'fkp_dashboard-page__outbound-section centered',
style: 'height: 127px',
},
E('span', {}, [E('span', {}, _('Dashboard currently unavailable'))]),
);
}
function renderLoadingState() {
return E('div', {
id: 'dashboard-sections-grid-skeleton',
class: 'fkp_dashboard-page__outbound-section skeleton',
style: 'height: 127px',
});
}
function isValidHttpUrl(url?: string) {
return Boolean(url && /^https?:\/\/\S+$/i.test(url));
}
function formatBytes(value?: number) {
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
return undefined;
}
return prettyBytes(value);
}
function formatDate(seconds?: number) {
if (
typeof seconds !== 'number' ||
!Number.isFinite(seconds) ||
seconds <= 0
) {
return undefined;
}
const date = new Date(seconds * 1000);
if (Number.isNaN(date.getTime())) {
return undefined;
}
return date.toLocaleDateString(undefined, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
});
}
function renderMetadataAction(label: string, url?: string) {
if (!isValidHttpUrl(url)) {
return undefined;
}
return E(
'a',
{
class: 'btn fkp_dashboard-page__subscription-meta__action',
href: url,
target: '_blank',
rel: 'noopener noreferrer',
title: label,
'aria-label': label,
},
renderLinkIcon24(),
);
}
function renderSubscriptionMetadata(
metadata: Forkop.SubscriptionMetadata | undefined,
) {
if (!metadata || Object.keys(metadata).length <= 1) {
return undefined;
}
const title = metadata.title || metadata.fileName;
const traffic = metadata.traffic;
const used = formatBytes(traffic?.used) || '0 B';
const total = traffic?.isUnlimited
? '∞'
: formatBytes(traffic?.total) || '0 B';
const expire = formatDate(metadata.expire);
const refillDate = formatDate(metadata.refillDate);
const rows = [
traffic
? {
label: _('Traffic'),
value: `${used} / ${total}`,
}
: undefined,
expire ? { label: _('Expires'), value: expire } : undefined,
refillDate ? { label: _('Refill'), value: refillDate } : undefined,
].filter(Boolean) as { label: string; value: string }[];
const actions = [
renderMetadataAction('Profile', metadata.webPageUrl),
renderMetadataAction('Support', metadata.supportUrl),
renderMetadataAction('More details', metadata.announceUrl),
].filter(Boolean) as HTMLElement[];
return E('div', { class: 'fkp_dashboard-page__subscription-meta' }, [
E('div', { class: 'fkp_dashboard-page__subscription-meta__main' }, [
E(
'div',
{ class: 'fkp_dashboard-page__subscription-meta__heading' },
_('Subscription info:'),
),
title
? E(
'div',
{ class: 'fkp_dashboard-page__subscription-meta__title' },
title,
)
: '',
rows.length
? E(
'div',
{ class: 'fkp_dashboard-page__subscription-meta__facts' },
rows.map((row) =>
E(
'div',
{ class: 'fkp_dashboard-page__subscription-meta__fact' },
[
E(
'span',
{
class: 'fkp_dashboard-page__subscription-meta__fact-key',
},
row.label,
),
E(
'span',
{
class:
'fkp_dashboard-page__subscription-meta__fact-value',
},
row.value,
),
],
),
),
)
: '',
actions.length
? E(
'div',
{ class: 'fkp_dashboard-page__subscription-meta__actions' },
actions,
)
: '',
]),
metadata.announce
? E(
'blockquote',
{ class: 'fkp_dashboard-page__subscription-meta__announce' },
metadata.announce,
)
: '',
]);
}
function renderSubscriptionUpdateAction(
section: Forkop.OutboundGroup,
subscriptionUpdating: boolean,
onUpdateSubscription: (section: Forkop.OutboundGroup) => void,
) {
if (!section.subscriptionSourceCount) {
return undefined;
}
return E(
'button',
{
type: 'button',
class: 'btn fkp_dashboard-page__outbound-section__subscription-update',
'aria-label': _('Update subscriptions'),
disabled: subscriptionUpdating ? true : undefined,
click: (event: MouseEvent) => {
event.preventDefault();
event.stopPropagation();
if (subscriptionUpdating) {
return;
}
onUpdateSubscription(section);
},
},
subscriptionUpdating
? [renderLoaderCircleIcon24(), _('Update subscriptions')]
: _('Update subscriptions'),
);
}
export function getLatencyTestLabel(
latencyProgress?: Forkop.LatencyActionProgress,
) {
const total = Math.trunc(Number(latencyProgress?.total ?? 0));
if (!Number.isFinite(total) || total <= 0) {
return _('Test latency');
}
const completedValue = Number(latencyProgress?.completed ?? 0);
const completed = Number.isFinite(completedValue)
? Math.trunc(completedValue)
: 0;
return `${_('Test latency')}: ${Math.min(
Math.max(0, completed),
total,
)}/${total}`;
}
function renderDefaultState({
section,
onChooseOutbound,
onCopyOutbound,
onShowUrlTestInfo,
onShowPriorityInfo,
onTestLatency,
onUpdateSubscription,
latencyFetching,
latencyProgress,
subscriptionUpdating,
selectorSwitchingTag,
}: IRenderSectionsProps) {
function testLatency() {
if (section.withTagSelect) {
return onTestLatency(
section.latencyTestCodes?.length
? section.latencyTestCodes
: section.latencyTestCode || section.code,
);
}
if (section.outbounds.length) {
return onTestLatency(section.outbounds[0].code);
}
}
function renderOutbound(outbound: Forkop.Outbound) {
function getLatencyClass() {
if (!outbound.latency) {
return 'fkp_dashboard-page__outbound-grid__item__latency--empty';
}
if (outbound.latency < 800) {
return 'fkp_dashboard-page__outbound-grid__item__latency--green';
}
if (outbound.latency < 1500) {
return 'fkp_dashboard-page__outbound-grid__item__latency--yellow';
}
return 'fkp_dashboard-page__outbound-grid__item__latency--red';
}
const canCopyLink =
Boolean(outbound.canCopyLink) || isCopyableProxyLink(outbound.link);
const selectorSwitching = Boolean(selectorSwitchingTag);
const outboundSwitching = selectorSwitchingTag === outbound.code;
const canChooseOutbound =
section.withTagSelect &&
outbound.runtimeAvailable !== false &&
!selectorSwitching &&
!outbound.selected;
const className = [
'fkp_dashboard-page__outbound-grid__item',
outbound.selected
? 'fkp_dashboard-page__outbound-grid__item--active'
: '',
canChooseOutbound
? 'fkp_dashboard-page__outbound-grid__item--selectable'
: '',
section.withTagSelect && !canChooseOutbound
? 'fkp_dashboard-page__outbound-grid__item--disabled'
: '',
outboundSwitching
? 'fkp_dashboard-page__outbound-grid__item--switching'
: '',
]
.filter(Boolean)
.join(' ');
return E(
'div',
{
class: className,
'aria-busy': outboundSwitching ? 'true' : undefined,
'aria-disabled':
section.withTagSelect && !canChooseOutbound ? 'true' : undefined,
click: () =>
canChooseOutbound &&
onChooseOutbound(section.sectionName, section.code, outbound.code),
},
[
...(outboundSwitching
? [
svgEl(
'svg',
{ class: 'fkp_dashboard-page__outbound-grid__item__snake' },
[
svgEl('rect', {
width: '100%',
height: '100%',
fill: 'none',
rx: 4,
ry: 4,
pathLength: 100,
}),
],
),
]
: []),
E('div', { class: 'fkp_dashboard-page__outbound-grid__item__header' }, [
E('b', {}, outbound.displayName),
...(canCopyLink
? [
E(
'button',
{
type: 'button',
class:
'btn fkp_dashboard-page__outbound-grid__item__copy-button',
title: _('Copy proxy link'),
'aria-label': _('Copy proxy link'),
click: (event: MouseEvent) => {
event.stopPropagation();
onCopyOutbound(section, outbound);
},
},
renderCopyIcon24(),
),
]
: []),
...(outbound.urlTestInfo
? [
E(
'button',
{
type: 'button',
class:
'btn fkp_dashboard-page__outbound-grid__item__copy-button',
title: _('URLTest details'),
'aria-label': _('URLTest details'),
click: (event: MouseEvent) => {
event.stopPropagation();
onShowUrlTestInfo(section, outbound);
},
},
renderInfoIcon24(),
),
]
: []),
...(outbound.priorityInfo
? [
E(
'button',
{
type: 'button',
class:
'btn fkp_dashboard-page__outbound-grid__item__copy-button',
title: _('Priority details'),
'aria-label': _('Priority details'),
click: (event: MouseEvent) => {
event.stopPropagation();
onShowPriorityInfo(section, outbound);
},
},
renderInfoIcon24(),
),
]
: []),
]),
E('div', { class: 'fkp_dashboard-page__outbound-grid__item__footer' }, [
E(
'div',
{ class: 'fkp_dashboard-page__outbound-grid__item__type' },
[outbound.type].filter(Boolean),
),
E(
'div',
{ class: getLatencyClass() },
outbound.latency ? `${outbound.latency}ms` : 'N/A',
),
]),
],
);
}
const metadataNodes = (section.subscriptionMetadata || [])
.map((metadata) => renderSubscriptionMetadata(metadata))
.filter(Boolean) as HTMLElement[];
const subscriptionUpdateAction = renderSubscriptionUpdateAction(
section,
subscriptionUpdating,
onUpdateSubscription,
);
return E('div', { class: 'fkp_dashboard-page__outbound-section' }, [
// Title with test latency
E('div', { class: 'fkp_dashboard-page__outbound-section__title-section' }, [
E(
'div',
{
class: 'fkp_dashboard-page__outbound-section__title-section__title',
},
section.displayName,
),
E(
'div',
{
class: 'fkp_dashboard-page__outbound-section__title-section__actions',
},
[
...(subscriptionUpdateAction ? [subscriptionUpdateAction] : []),
E(
'button',
{
type: 'button',
class: 'btn dashboard-sections-grid-item-test-latency',
'data-latency-section': section.sectionName,
disabled: latencyFetching ? true : undefined,
click: (event: MouseEvent) => {
event.preventDefault();
event.stopPropagation();
if (latencyFetching) {
return;
}
testLatency();
},
},
latencyFetching
? [
renderLoaderCircleIcon24(),
E(
'span',
{
class: 'dashboard-sections-grid-item-test-latency__label',
},
getLatencyTestLabel(latencyProgress),
),
]
: E(
'span',
{
class: 'dashboard-sections-grid-item-test-latency__label',
},
_('Test latency'),
),
),
],
),
]),
E('div', { class: 'fkp_dashboard-page__outbound-grid' }, [
...metadataNodes,
...section.outbounds.map((outbound) => renderOutbound(outbound)),
]),
]);
}
export function renderSections(props: IRenderSectionsProps) {
if (props.failed) {
return renderFailedState();
}
if (props.loading) {
return renderLoadingState();
}
return renderDefaultState(props);
}