Refine client setup state and subscription controls
This commit is contained in:
@@ -123,11 +123,16 @@ function App() {
|
||||
const data = await api.subscription.fetch(subscriptionUrl || state?.subscriptionHost || '');
|
||||
setServers(data.servers || []);
|
||||
if (data.servers?.length) {
|
||||
const nextTag = data.servers.some((server) => server.tag === selectedTag)
|
||||
? selectedTag
|
||||
: data.servers[0].tag;
|
||||
setSelectedTag(nextTag);
|
||||
setPendingTag(nextTag);
|
||||
if (state?.mode === 'client') {
|
||||
setSelectedTag('');
|
||||
setPendingTag('');
|
||||
} else {
|
||||
const nextTag = data.servers.some((server) => server.tag === selectedTag)
|
||||
? selectedTag
|
||||
: data.servers[0].tag;
|
||||
setSelectedTag(nextTag);
|
||||
setPendingTag(nextTag);
|
||||
}
|
||||
}
|
||||
await loadState();
|
||||
});
|
||||
@@ -433,6 +438,7 @@ function App() {
|
||||
setPendingTag={setPendingTag}
|
||||
onFetchSubscription={fetchSubscription}
|
||||
onRefreshSubscriptionInfo={refreshSubscriptionInfo}
|
||||
onForgetSubscription={forgetSubscription}
|
||||
onApply={applyServer}
|
||||
onRestart={restartSingbox}
|
||||
onStop={() => stopSingbox(false)}
|
||||
|
||||
@@ -20,12 +20,15 @@ export function ClientOverviewPage({
|
||||
setPendingTag,
|
||||
onFetchSubscription,
|
||||
onRefreshSubscriptionInfo,
|
||||
onForgetSubscription,
|
||||
onApply,
|
||||
onRestart,
|
||||
onStop,
|
||||
}) {
|
||||
const connected = Boolean(state?.singboxRunning);
|
||||
const hasSubscription = Boolean(state?.hasSubscription);
|
||||
const selectedTag = pendingTag || state?.selectedTag || '';
|
||||
const showPower = hasSubscription && Boolean(selectedTag);
|
||||
const canStart = Boolean(selectedTag || state?.configExists);
|
||||
const [now, setNow] = useState(Date.now());
|
||||
const [editingSubscription, setEditingSubscription] = useState(!state?.hasSubscription);
|
||||
@@ -82,6 +85,10 @@ export function ClientOverviewPage({
|
||||
if (editingSubscription) subscriptionInputRef.current?.focus();
|
||||
}, [editingSubscription]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasSubscription) setEditingSubscription(true);
|
||||
}, [hasSubscription]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editingSubscription || !state?.hasSubscription || subscriptionUrl) return undefined;
|
||||
const timer = setTimeout(() => setEditingSubscription(false), 5000);
|
||||
@@ -89,7 +96,7 @@ export function ClientOverviewPage({
|
||||
}, [editingSubscription, state?.hasSubscription, subscriptionUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editingSubscription) return undefined;
|
||||
if (!editingSubscription || !hasSubscription) return undefined;
|
||||
const closeOnOutsideClick = (event) => {
|
||||
if (subscriptionRef.current?.contains(event.target)) return;
|
||||
setSubscriptionUrl('');
|
||||
@@ -97,7 +104,7 @@ export function ClientOverviewPage({
|
||||
};
|
||||
document.addEventListener('pointerdown', closeOnOutsideClick);
|
||||
return () => document.removeEventListener('pointerdown', closeOnOutsideClick);
|
||||
}, [editingSubscription, setSubscriptionUrl]);
|
||||
}, [editingSubscription, hasSubscription, setSubscriptionUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!state?.hasSubscription) return undefined;
|
||||
@@ -176,7 +183,8 @@ export function ClientOverviewPage({
|
||||
|
||||
return (
|
||||
<div className="client-shell">
|
||||
<main className="client-panel">
|
||||
<main className={`client-panel${showPower ? '' : ' is-setup'}`}>
|
||||
{showPower && (
|
||||
<section className="client-power-section" aria-labelledby="connection-title">
|
||||
<button
|
||||
className="client-power"
|
||||
@@ -232,6 +240,7 @@ export function ClientOverviewPage({
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<div className="client-form">
|
||||
<div
|
||||
@@ -257,6 +266,18 @@ export function ClientOverviewPage({
|
||||
<path d="M21 12a9 9 0 0 0-15.2-6.5L3 8m0-5v5h5M3 12a9 9 0 0 0 15.2 6.5L21 16m0 5v-5h-5" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className="client-subscription-delete"
|
||||
type="button"
|
||||
aria-label="Удалить подписку"
|
||||
title="Удалить подписку"
|
||||
disabled={busy}
|
||||
onClick={onForgetSubscription}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M4 7h16M9 7V4h6v3m-9 0 1 13h10l1-13M10 11v5M14 11v5" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
className="client-subscription-domain-button"
|
||||
@@ -305,7 +326,7 @@ export function ClientOverviewPage({
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{hasUsage && (
|
||||
{hasSubscription && hasUsage && (
|
||||
<section className={`client-usage${usageUpdated ? ' is-updated' : ''}`} aria-label="Статистика подписки">
|
||||
<span>Использовано</span>
|
||||
<strong>
|
||||
@@ -335,7 +356,9 @@ export function ClientOverviewPage({
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="client-servers" aria-label="Серверы">
|
||||
{hasSubscription && (
|
||||
<section className="client-servers" aria-label="Выберите сервер">
|
||||
{!showPower && <span className="client-server-prompt">Выберите сервер</span>}
|
||||
<div
|
||||
className={`client-server-grid${serversLeaving ? ' is-leaving' : ''}`}
|
||||
key={`${serverKey}:${serverRevealVersion}`}
|
||||
@@ -367,6 +390,7 @@ export function ClientOverviewPage({
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
||||
@@ -881,6 +881,11 @@ code, .mono {
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
.client-panel.is-setup .client-form {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.client-power-section p {
|
||||
color: var(--client-muted);
|
||||
font-size: 11px;
|
||||
@@ -891,6 +896,12 @@ code, .mono {
|
||||
justify-items: center;
|
||||
gap: 18px;
|
||||
text-align: center;
|
||||
animation: client-power-arrive 850ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
|
||||
@keyframes client-power-arrive {
|
||||
0% { opacity: 0; filter: blur(8px); transform: scale(0.94); }
|
||||
100% { opacity: 1; filter: blur(0); transform: scale(1); }
|
||||
}
|
||||
|
||||
.client-state-copy {
|
||||
@@ -1029,6 +1040,8 @@ code, .mono {
|
||||
width: min(360px, calc(50% - 154px));
|
||||
display: grid;
|
||||
gap: 36px;
|
||||
transform: translateX(0);
|
||||
transition: left 850ms cubic-bezier(0.16, 1, 0.3, 1), transform 850ms cubic-bezier(0.16, 1, 0.3, 1), width 850ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.client-subscription {
|
||||
@@ -1039,7 +1052,8 @@ code, .mono {
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.client-subscription-refresh {
|
||||
.client-subscription-refresh,
|
||||
.client-subscription-delete {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: grid;
|
||||
@@ -1053,7 +1067,8 @@ code, .mono {
|
||||
transition: color 250ms ease, filter 500ms ease, opacity 300ms ease, transform 600ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.client-subscription-refresh svg {
|
||||
.client-subscription-refresh svg,
|
||||
.client-subscription-delete svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
fill: none;
|
||||
@@ -1080,7 +1095,23 @@ code, .mono {
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.client-subscription.is-editing .client-subscription-refresh {
|
||||
.client-subscription-delete:hover:not(:disabled) {
|
||||
color: oklch(0.68 0.15 28);
|
||||
filter: drop-shadow(0 0 6px oklch(0.68 0.15 28 / 0.45));
|
||||
}
|
||||
|
||||
.client-subscription-delete:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.client-subscription-delete:focus-visible {
|
||||
outline: 2px solid oklch(0.68 0.15 28);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.client-subscription.is-editing .client-subscription-refresh,
|
||||
.client-subscription.is-editing .client-subscription-delete {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -1346,6 +1377,18 @@ code, .mono {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.client-server-prompt {
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
color: var(--client-muted);
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
animation: client-state-reveal 700ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
|
||||
.client-server-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
@@ -1569,6 +1612,11 @@ code, .mono {
|
||||
.client-form {
|
||||
position: static;
|
||||
width: 100%;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.client-panel.is-setup .client-form {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1579,6 +1627,9 @@ code, .mono {
|
||||
.client-power svg,
|
||||
.client-usage-bar i,
|
||||
.client-subscription-refresh,
|
||||
.client-subscription-delete,
|
||||
.client-power-section,
|
||||
.client-form,
|
||||
.client-usage,
|
||||
.client-usage > strong,
|
||||
.client-server,
|
||||
|
||||
Reference in New Issue
Block a user