Simplify client overview access controls and layout
This commit is contained in:
@@ -2,7 +2,6 @@ import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { flushSync } from 'react-dom';
|
||||
import { api } from '../api.js';
|
||||
import {
|
||||
accessTabForKey,
|
||||
connectionAction,
|
||||
connectionDurationParts,
|
||||
copyText,
|
||||
@@ -27,10 +26,6 @@ import {
|
||||
|
||||
const SUBSCRIPTION_REVEAL_DELAY_MS = 1350;
|
||||
const DURATION_MODE_STORAGE_KEY = 'harbor-duration-mode';
|
||||
const ACCESS_TABS = [
|
||||
['gateway', 'Gateway'],
|
||||
['proxy', 'Gateway Proxy'],
|
||||
];
|
||||
|
||||
function CloudTooltip({ children }) {
|
||||
return <span className="client-tooltip" role="tooltip">{children}</span>;
|
||||
@@ -632,7 +627,6 @@ export function ClientOverviewPage({
|
||||
const [editingSubscription, setEditingSubscription] = useState(!state?.hasSubscription);
|
||||
const [showIntro, setShowIntro] = useState(!hasSubscription);
|
||||
const [subscriptionContentReady, setSubscriptionContentReady] = useState(hasSubscription);
|
||||
const [accessTab, setAccessTab] = useState('gateway');
|
||||
const [copyFeedback, setCopyFeedback] = useState(null);
|
||||
const [subscriptionValidation, setSubscriptionValidation] = useState({
|
||||
url: '',
|
||||
@@ -666,6 +660,11 @@ export function ClientOverviewPage({
|
||||
const proxyUrls = localProxyUrls(state?.proxyPort, gatewayAddress);
|
||||
const usage = subscriptionUsage(state?.userInfo);
|
||||
const duration = connectionDurationParts(state?.singboxStartedAt, now);
|
||||
const wordClockDuration = [
|
||||
['hours', duration.hours],
|
||||
['minutes', duration.minutes],
|
||||
['seconds', duration.seconds],
|
||||
].filter(([name, part]) => duration.days.value || part.value || name === 'seconds');
|
||||
const connectionTitle = connected
|
||||
? gatewayDirect ? 'Gateway подключён' : 'VPN включён'
|
||||
: 'Подключение выключено';
|
||||
@@ -932,14 +931,6 @@ export function ClientOverviewPage({
|
||||
copyTimerRef.current = setTimeout(() => setCopyFeedback(null), 800);
|
||||
}
|
||||
|
||||
function navigateAccessTabs(event, current) {
|
||||
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
|
||||
event.preventDefault();
|
||||
const next = accessTabForKey(ACCESS_TABS.map(([tab]) => tab), current, event.key);
|
||||
setAccessTab(next);
|
||||
requestAnimationFrame(() => document.getElementById(`client-access-tab-${next}`)?.focus());
|
||||
}
|
||||
|
||||
async function refreshSubscription() {
|
||||
const startedAt = performance.now();
|
||||
setRefreshingInfo(true);
|
||||
@@ -1158,24 +1149,27 @@ export function ClientOverviewPage({
|
||||
:<DurationPart name="seconds-value"><AnimatedSeconds value={duration.seconds.value} /></DurationPart>
|
||||
</time>
|
||||
<time
|
||||
className={`client-duration${durationMode === 'words' ? ' is-active' : ''}`}
|
||||
className={`client-duration client-duration-words${durationMode === 'words' ? ' is-active' : ''}`}
|
||||
aria-hidden={durationMode !== 'words'}
|
||||
>
|
||||
{[
|
||||
['days', duration.days],
|
||||
['hours', duration.hours],
|
||||
['minutes', duration.minutes],
|
||||
['seconds', duration.seconds],
|
||||
]
|
||||
.filter(([name, part]) => part.value || name === 'seconds')
|
||||
.map(([name, part]) => (
|
||||
<span className="client-duration-unit" data-unit={name} key={name}>
|
||||
<DurationPart name={`${name}-value`}>{name === 'seconds'
|
||||
? <AnimatedSeconds value={part.value} padded={false} />
|
||||
: part.value}</DurationPart>{' '}
|
||||
<DurationPart name={`${name}-label`}>{part.label}</DurationPart>
|
||||
{duration.days.value > 0 && (
|
||||
<span className="client-duration-word-row is-calendar">
|
||||
<span className="client-duration-unit" data-unit="days">
|
||||
<DurationPart name="days-value">{duration.days.value}</DurationPart>{' '}
|
||||
<DurationPart name="days-label">{duration.days.label}</DurationPart>
|
||||
</span>
|
||||
</span>
|
||||
))}
|
||||
)}
|
||||
<span className="client-duration-word-row is-clock">
|
||||
{wordClockDuration.map(([name, part]) => (
|
||||
<span className="client-duration-unit" data-unit={name} key={name}>
|
||||
<DurationPart name={`${name}-value`}>{name === 'seconds'
|
||||
? <AnimatedSeconds value={part.value} padded={false} />
|
||||
: part.value}</DurationPart>{' '}
|
||||
<DurationPart name={`${name}-label`}>{part.label}</DurationPart>
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
</time>
|
||||
</span>
|
||||
<CloudTooltip>{durationMode === 'digital' ? 'Показать время словами' : 'Показать цифровой таймер'}</CloudTooltip>
|
||||
@@ -1188,54 +1182,8 @@ export function ClientOverviewPage({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className={`client-proxies${isGateway ? ' has-tabs' : ''}`} aria-label={isGateway ? 'Gateway и Gateway Proxy' : 'Локальный прокси'}>
|
||||
{isGateway && (
|
||||
<div className="client-access-tabs" role="tablist" aria-label="Способ подключения">
|
||||
{ACCESS_TABS.map(([tab, label]) => (
|
||||
<button
|
||||
id={`client-access-tab-${tab}`}
|
||||
className={`client-access-tab${accessTab === tab ? ' is-active' : ''}`}
|
||||
type="button"
|
||||
role="tab"
|
||||
key={tab}
|
||||
aria-selected={accessTab === tab}
|
||||
aria-controls={`client-access-panel-${tab}`}
|
||||
tabIndex={accessTab === tab ? 0 : -1}
|
||||
onClick={() => setAccessTab(tab)}
|
||||
onKeyDown={(event) => navigateAccessTabs(event, tab)}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{isGateway && (
|
||||
<div
|
||||
id="client-access-panel-gateway"
|
||||
className="client-access-point"
|
||||
role="tabpanel"
|
||||
aria-labelledby="client-access-tab-gateway"
|
||||
hidden={accessTab !== 'gateway'}
|
||||
>
|
||||
<strong className="client-proxy-address">{gatewayAddress}</strong>
|
||||
<button
|
||||
className={`client-copy-button${copyFeedback?.kind === 'gateway' ? copyFeedback.failed ? ' is-copy-error' : ' is-copied' : ''}`}
|
||||
type="button"
|
||||
aria-label={`Скопировать Gateway: ${gatewayAddress}`}
|
||||
onClick={() => copyProxy('gateway')}
|
||||
>
|
||||
<span className="client-copy-label">КОПИРОВАТЬ</span>
|
||||
{copyFeedback?.kind === 'gateway' && <span className="client-copy-feedback" aria-hidden="true">{copyFeedback.failed ? 'ОШИБКА' : 'ГОТОВО'}</span>}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
id={isGateway ? 'client-access-panel-proxy' : undefined}
|
||||
className="client-access-point"
|
||||
role={isGateway ? 'tabpanel' : undefined}
|
||||
aria-labelledby={isGateway ? 'client-access-tab-proxy' : undefined}
|
||||
hidden={isGateway && accessTab !== 'proxy'}
|
||||
>
|
||||
<section className={`client-proxies${isGateway ? ' is-gateway' : ''}`} aria-label={isGateway ? 'Gateway и Gateway Proxy' : 'Локальный прокси'}>
|
||||
<div className="client-access-point">
|
||||
{!isGateway && (
|
||||
<span className={`client-proxy-label${gatewayDirect ? ' is-gateway' : ''}`}>
|
||||
<span className={!gatewayDirect ? 'is-active' : ''}>Локальный VPN</span>
|
||||
@@ -1245,24 +1193,28 @@ export function ClientOverviewPage({
|
||||
</span>
|
||||
)}
|
||||
<strong className="client-proxy-address">
|
||||
{proxyUrls.http.replace(/^https?:\/\//, '')}
|
||||
{isGateway ? gatewayAddress : proxyUrls.http.replace(/^https?:\/\//, '')}
|
||||
</strong>
|
||||
<div className="client-proxy-actions">
|
||||
{[
|
||||
['socks5', 'SOCKS5'],
|
||||
['http', 'HTTP'],
|
||||
].map(([kind, label]) => (
|
||||
<button
|
||||
className={`client-copy-button${copyFeedback?.kind === kind ? copyFeedback.failed ? ' is-copy-error' : ' is-copied' : ''}`}
|
||||
type="button"
|
||||
key={kind}
|
||||
aria-label={`Скопировать ${label}: ${proxyUrls[kind]}`}
|
||||
onClick={() => copyProxy(kind)}
|
||||
>
|
||||
<span className="client-copy-label">{label}</span>
|
||||
{copyFeedback?.kind === kind && <span className="client-copy-feedback" aria-hidden="true">{copyFeedback.failed ? 'ОШИБКА' : 'ГОТОВО'}</span>}
|
||||
</button>
|
||||
))}
|
||||
{(isGateway ? [
|
||||
['gateway', 'GATEWAY'],
|
||||
['socks5', 'SOCKS5'],
|
||||
['http', 'HTTP'],
|
||||
] : [
|
||||
['socks5', 'SOCKS5'],
|
||||
['http', 'HTTP'],
|
||||
]).map(([kind, label]) => (
|
||||
<button
|
||||
className={`client-copy-button${copyFeedback?.kind === kind ? copyFeedback.failed ? ' is-copy-error' : ' is-copied' : ''}`}
|
||||
type="button"
|
||||
key={kind}
|
||||
aria-label={`Скопировать ${label}: ${kind === 'gateway' ? gatewayAddress : proxyUrls[kind]}`}
|
||||
onClick={() => copyProxy(kind)}
|
||||
>
|
||||
<span className="client-copy-label">{label}</span>
|
||||
{copyFeedback?.kind === kind && <span className="client-copy-feedback" aria-hidden="true">{copyFeedback.failed ? 'ОШИБКА' : 'ГОТОВО'}</span>}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user