Rename product to Harbor and refine connection timer
This commit is contained in:
@@ -3,8 +3,8 @@ import { flushSync } from 'react-dom';
|
||||
import { api } from '../api.js';
|
||||
import {
|
||||
connectionAction,
|
||||
connectionDurationParts,
|
||||
copyText,
|
||||
formatConnectionDuration,
|
||||
localProxyUrls,
|
||||
subscriptionDomain,
|
||||
subscriptionDaysLeft,
|
||||
@@ -61,6 +61,30 @@ function InstructionBlock({ block, open, onToggle }) {
|
||||
);
|
||||
}
|
||||
|
||||
function DurationPart({ name, children }) {
|
||||
return (
|
||||
<span
|
||||
className={`client-duration-part${name.endsWith('-value') ? ' is-value' : ' is-label'}`}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function HarborBrand({ isGateway }) {
|
||||
const product = isGateway ? 'Gateway' : 'Connect';
|
||||
|
||||
return (
|
||||
<div className={`harbor-brand is-${product.toLowerCase()}`} aria-label={`Harbor ${product}`}>
|
||||
<svg viewBox="0 0 32 32" aria-hidden="true">
|
||||
<circle cx="16" cy="6" r="3" />
|
||||
<path d="M16 9v15M10 14h12M6 20c1.5 5 5 8 10 8s8.5-3 10-8M6 20l5 1M26 20l-5 1" />
|
||||
</svg>
|
||||
<span><strong>Harbor</strong> <em>{product}</em></span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ClientOverviewPage({
|
||||
state,
|
||||
busy,
|
||||
@@ -84,6 +108,7 @@ export function ClientOverviewPage({
|
||||
const showPower = hasSubscription && Boolean(selectedTag);
|
||||
const canStart = Boolean(selectedTag || state?.configExists);
|
||||
const [now, setNow] = useState(Date.now());
|
||||
const [durationMode, setDurationMode] = useState('digital');
|
||||
const [editingSubscription, setEditingSubscription] = useState(!state?.hasSubscription);
|
||||
const [pings, setPings] = useState({});
|
||||
const [accessTab, setAccessTab] = useState('gateway');
|
||||
@@ -103,6 +128,7 @@ export function ClientOverviewPage({
|
||||
const gatewayAddress = isGateway ? window.location.hostname : '127.0.0.1';
|
||||
const proxyUrls = localProxyUrls(state?.proxyPort, gatewayAddress);
|
||||
const usage = subscriptionUsage(state?.userInfo);
|
||||
const duration = connectionDurationParts(state?.singboxStartedAt, now);
|
||||
const [displayedUsed, setDisplayedUsed] = useState(usage.used);
|
||||
const hasUsage = Boolean(
|
||||
state?.userInfo && ['upload', 'download', 'total', 'expire'].some((key) => key in state.userInfo),
|
||||
@@ -288,8 +314,13 @@ export function ClientOverviewPage({
|
||||
document.startViewTransition(update);
|
||||
}
|
||||
|
||||
function toggleDurationMode() {
|
||||
setDurationMode((mode) => mode === 'digital' ? 'words' : 'digital');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="client-shell">
|
||||
<HarborBrand isGateway={isGateway} />
|
||||
{hasSubscription && <button
|
||||
ref={instructionsToggleRef}
|
||||
className={`client-instructions-toggle${instructionsOpen ? ' is-open' : ''}`}
|
||||
@@ -328,9 +359,43 @@ export function ClientOverviewPage({
|
||||
</h2>
|
||||
<div className="client-state-detail">
|
||||
{connected ? (
|
||||
<time key="duration" className="client-duration">
|
||||
{formatConnectionDuration(state?.singboxStartedAt, now)}
|
||||
</time>
|
||||
<button
|
||||
className={`client-duration-toggle${durationMode === 'words' ? ' is-words' : ''}`}
|
||||
type="button"
|
||||
key="duration"
|
||||
aria-label={durationMode === 'digital' ? 'Показать время словами' : 'Показать цифровой таймер'}
|
||||
title={durationMode === 'digital' ? 'Показать время словами' : 'Показать цифровой таймер'}
|
||||
onClick={toggleDurationMode}
|
||||
>
|
||||
<span className="client-duration-stack">
|
||||
<time
|
||||
className={`client-duration${durationMode === 'digital' ? ' is-active' : ''}`}
|
||||
aria-hidden={durationMode !== 'digital'}
|
||||
>
|
||||
<DurationPart name="hours-value">{String(duration.totalHours).padStart(2, '0')}</DurationPart>
|
||||
:<DurationPart name="minutes-value">{String(duration.minutes.value).padStart(2, '0')}</DurationPart>
|
||||
:<DurationPart name="seconds-value">{String(duration.seconds.value).padStart(2, '0')}</DurationPart>
|
||||
</time>
|
||||
<time
|
||||
className={`client-duration${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`}>{part.value}</DurationPart>{' '}
|
||||
<DurationPart name={`${name}-label`}>{part.label}</DurationPart>
|
||||
</span>
|
||||
))}
|
||||
</time>
|
||||
</span>
|
||||
</button>
|
||||
) : (
|
||||
<p key="hint">
|
||||
{canStart ? 'Нажмите, чтобы включить' : 'Добавьте ссылку и выберите сервер'}
|
||||
|
||||
Reference in New Issue
Block a user