From 60aad12cd47f0512755eeedb369067e89bc7769e Mon Sep 17 00:00:00 2001 From: Maxim Isaev Date: Fri, 22 May 2026 14:14:09 +0300 Subject: [PATCH] fix(settings): show excluding state on library index exclude action MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flush UI before the async unbind, disable repeat clicks, cancel an active sync when needed, and label the button "Excluding…" / localized equivalent. --- .../settings/LibraryIndexSection.tsx | 39 ++++++++++++++++--- .../settings/LibraryIndexServerRow.tsx | 9 ++++- src/locales/de/settings.ts | 1 + src/locales/en/settings.ts | 1 + src/locales/es/settings.ts | 1 + src/locales/fr/settings.ts | 1 + src/locales/nb/settings.ts | 1 + src/locales/nl/settings.ts | 1 + src/locales/ro/settings.ts | 1 + src/locales/ru/settings.ts | 1 + src/locales/zh/settings.ts | 1 + src/utils/library/librarySyncQueue.ts | 19 +++++++++ 12 files changed, 68 insertions(+), 8 deletions(-) diff --git a/src/components/settings/LibraryIndexSection.tsx b/src/components/settings/LibraryIndexSection.tsx index b3d578bb..ceda5e13 100644 --- a/src/components/settings/LibraryIndexSection.tsx +++ b/src/components/settings/LibraryIndexSection.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { flushSync } from 'react-dom'; import { useTranslation } from 'react-i18next'; import { DatabaseZap } from 'lucide-react'; import { useAuthStore } from '../../store/authStore'; @@ -18,7 +19,7 @@ import { bootstrapIndexedServer, type BindServerResult, } from '../../utils/library/librarySession'; -import { enqueueLibrarySync } from '../../utils/library/librarySyncQueue'; +import { enqueueLibrarySync, waitForLibrarySyncIdle } from '../../utils/library/librarySyncQueue'; import { syncIngestDisplayCount } from '../../utils/library/libraryReady'; import { serverListDisplayLabel } from '../../utils/server/serverDisplayName'; import LibraryIndexServerRow, { type LibraryServerConnection } from './LibraryIndexServerRow'; @@ -58,6 +59,7 @@ export default function LibraryIndexSection() { const [connectionByServer, setConnectionByServer] = useState>({}); const [progressByServer, setProgressByServer] = useState>({}); const [busyServerId, setBusyServerId] = useState(null); + const [excludingServerId, setExcludingServerId] = useState(null); const [bootstrapping, setBootstrapping] = useState(false); const pollTimer = useRef | null>(null); @@ -130,6 +132,7 @@ export default function LibraryIndexSection() { setConnectionByServer({}); setProgressByServer({}); setBusyServerId(null); + setExcludingServerId(null); return; } void runBootstrap(); @@ -263,8 +266,21 @@ export default function LibraryIndexSection() { }; const handleExcludeServer = async (serverId: string) => { - setBootstrapping(true); + if (excludingServerId) return; + flushSync(() => setExcludingServerId(serverId)); try { + const syncing = + busyServerId === serverId || + statusByServer[serverId]?.syncPhase === 'initial_sync' || + statusByServer[serverId]?.syncPhase === 'probing'; + if (syncing) { + try { + await librarySyncCancel(); + await waitForLibrarySyncIdle(serverId); + } catch { + /* best-effort — proceed with unbind */ + } + } await librarySyncClearSession(serverId); setServerSyncExcluded(serverId, true); setStatusByServer(prev => { @@ -277,10 +293,18 @@ export default function LibraryIndexSection() { delete next[serverId]; return next; }); + setProgressByServer(prev => { + const next = { ...prev }; + delete next[serverId]; + return next; + }); + if (busyServerId === serverId) { + setBusyServerId(null); + } } catch (e) { showToast(t('settings.libraryIndexBindError', { error: String(e) }), 5000, 'error'); } finally { - setBootstrapping(false); + setExcludingServerId(null); } }; @@ -292,7 +316,7 @@ export default function LibraryIndexSection() { } }; - const globalBusy = bootstrapping || busyServerId != null; + const globalBusy = bootstrapping || busyServerId != null || excludingServerId != null; return ( void runServerAction(srv.id, 'full')} onDeltaSync={() => void runServerAction(srv.id, 'delta')} onVerify={() => void runServerAction(srv.id, 'verify')} @@ -376,7 +403,7 @@ export default function LibraryIndexSection() { type="button" className="btn btn-surface" style={{ fontSize: 12, padding: '4px 10px' }} - disabled={bootstrapping} + disabled={bootstrapping || excludingServerId != null} onClick={() => void handleIncludeServer(srv.id)} > {t('settings.libraryIndexIncludeServer')} diff --git a/src/components/settings/LibraryIndexServerRow.tsx b/src/components/settings/LibraryIndexServerRow.tsx index 4156b9a5..38995336 100644 --- a/src/components/settings/LibraryIndexServerRow.tsx +++ b/src/components/settings/LibraryIndexServerRow.tsx @@ -18,6 +18,7 @@ interface LibraryIndexServerRowProps { connection: LibraryServerConnection; progressLabel: string | null; busy: boolean; + excluding: boolean; actionsDisabled: boolean; onFullSync: () => void; onDeltaSync: () => void; @@ -33,6 +34,7 @@ export default function LibraryIndexServerRow({ connection, progressLabel, busy, + excluding, actionsDisabled, onFullSync, onDeltaSync, @@ -133,11 +135,14 @@ export default function LibraryIndexServerRow({ type="button" className="btn btn-ghost" style={{ fontSize: 12, padding: '4px 10px', color: 'var(--text-muted)' }} - disabled={actionsDisabled} + disabled={actionsDisabled || excluding} + aria-busy={excluding} onClick={onExclude} > - {t('settings.libraryIndexExcludeServer')} + {excluding + ? t('settings.libraryIndexExcludingServer') + : t('settings.libraryIndexExcludeServer')} diff --git a/src/locales/de/settings.ts b/src/locales/de/settings.ts index 50343613..d01b26ab 100644 --- a/src/locales/de/settings.ts +++ b/src/locales/de/settings.ts @@ -261,6 +261,7 @@ export const settings = { libraryIndexFullResync: 'Vollständige Neusynchronisation', libraryIndexDeltaSync: 'Delta-Synchronisation', libraryIndexExcludeServer: 'Von Synchronisation ausschließen', + libraryIndexExcludingServer: 'Wird ausgeschlossen…', libraryIndexExcludedTitle: 'Von Synchronisation ausgeschlossen', libraryIndexIncludeServer: 'Wieder einschließen', libraryIndexStatus: 'Status', diff --git a/src/locales/en/settings.ts b/src/locales/en/settings.ts index 9d5910ba..49ba9cad 100644 --- a/src/locales/en/settings.ts +++ b/src/locales/en/settings.ts @@ -264,6 +264,7 @@ export const settings = { libraryIndexFullResync: 'Full resync', libraryIndexDeltaSync: 'Delta sync', libraryIndexExcludeServer: 'Exclude from sync', + libraryIndexExcludingServer: 'Excluding…', libraryIndexExcludedTitle: 'Excluded from sync', libraryIndexIncludeServer: 'Include again', libraryIndexStatus: 'Status', diff --git a/src/locales/es/settings.ts b/src/locales/es/settings.ts index f101ba95..955e08fe 100644 --- a/src/locales/es/settings.ts +++ b/src/locales/es/settings.ts @@ -259,6 +259,7 @@ export const settings = { libraryIndexFullResync: 'Resincronización completa', libraryIndexDeltaSync: 'Sincronización delta', libraryIndexExcludeServer: 'Excluir de la sincronización', + libraryIndexExcludingServer: 'Excluyendo…', libraryIndexExcludedTitle: 'Excluidos de la sincronización', libraryIndexIncludeServer: 'Incluir de nuevo', libraryIndexStatus: 'Estado', diff --git a/src/locales/fr/settings.ts b/src/locales/fr/settings.ts index 575f3348..1c3e546a 100644 --- a/src/locales/fr/settings.ts +++ b/src/locales/fr/settings.ts @@ -257,6 +257,7 @@ export const settings = { libraryIndexFullResync: 'Resynchronisation complète', libraryIndexDeltaSync: 'Synchronisation delta', libraryIndexExcludeServer: 'Exclure de la synchronisation', + libraryIndexExcludingServer: 'Exclusion…', libraryIndexExcludedTitle: 'Exclus de la synchronisation', libraryIndexIncludeServer: 'Réinclure', libraryIndexStatus: 'État', diff --git a/src/locales/nb/settings.ts b/src/locales/nb/settings.ts index 39f94da1..63e8dd91 100644 --- a/src/locales/nb/settings.ts +++ b/src/locales/nb/settings.ts @@ -256,6 +256,7 @@ export const settings = { libraryIndexFullResync: 'Full resynkronisering', libraryIndexDeltaSync: 'Delta-synkronisering', libraryIndexExcludeServer: 'Ekskluder fra synkronisering', + libraryIndexExcludingServer: 'Ekskluderer…', libraryIndexExcludedTitle: 'Ekskludert fra synkronisering', libraryIndexIncludeServer: 'Inkluder igjen', libraryIndexStatus: 'Status', diff --git a/src/locales/nl/settings.ts b/src/locales/nl/settings.ts index a1816bc5..dcbf7c78 100644 --- a/src/locales/nl/settings.ts +++ b/src/locales/nl/settings.ts @@ -257,6 +257,7 @@ export const settings = { libraryIndexFullResync: 'Volledige resync', libraryIndexDeltaSync: 'Delta-sync', libraryIndexExcludeServer: 'Uitsluiten van synchronisatie', + libraryIndexExcludingServer: 'Uitsluiten…', libraryIndexExcludedTitle: 'Uitgesloten van synchronisatie', libraryIndexIncludeServer: 'Weer opnemen', libraryIndexStatus: 'Status', diff --git a/src/locales/ro/settings.ts b/src/locales/ro/settings.ts index ed051bb0..a8d2f356 100644 --- a/src/locales/ro/settings.ts +++ b/src/locales/ro/settings.ts @@ -263,6 +263,7 @@ export const settings = { libraryIndexFullResync: 'Resincronizare completă', libraryIndexDeltaSync: 'Sincronizare delta', libraryIndexExcludeServer: 'Exclude din sincronizare', + libraryIndexExcludingServer: 'Se exclude…', libraryIndexExcludedTitle: 'Excluse din sincronizare', libraryIndexIncludeServer: 'Include din nou', libraryIndexStatus: 'Stare', diff --git a/src/locales/ru/settings.ts b/src/locales/ru/settings.ts index 872b170f..bc51b323 100644 --- a/src/locales/ru/settings.ts +++ b/src/locales/ru/settings.ts @@ -269,6 +269,7 @@ export const settings = { libraryIndexFullResync: 'Полная пересинхронизация', libraryIndexDeltaSync: 'Быстрая дельта', libraryIndexExcludeServer: 'Исключить из синхронизации', + libraryIndexExcludingServer: 'Отключается…', libraryIndexExcludedTitle: 'Исключены из синхронизации', libraryIndexIncludeServer: 'Включить снова', libraryIndexStatus: 'Статус', diff --git a/src/locales/zh/settings.ts b/src/locales/zh/settings.ts index 712eb6d6..e08d7fc6 100644 --- a/src/locales/zh/settings.ts +++ b/src/locales/zh/settings.ts @@ -256,6 +256,7 @@ export const settings = { libraryIndexFullResync: '完全重新同步', libraryIndexDeltaSync: '增量同步', libraryIndexExcludeServer: '排除同步', + libraryIndexExcludingServer: '正在排除…', libraryIndexExcludedTitle: '已排除同步', libraryIndexIncludeServer: '重新纳入', libraryIndexStatus: '状态', diff --git a/src/utils/library/librarySyncQueue.ts b/src/utils/library/librarySyncQueue.ts index c45cb469..6aede145 100644 --- a/src/utils/library/librarySyncQueue.ts +++ b/src/utils/library/librarySyncQueue.ts @@ -62,6 +62,25 @@ function waitForServerIdle(serverId: string): Promise { }); } +/** Wait until a server emits `library:sync-idle`, or time out (best-effort). */ +export function waitForLibrarySyncIdle(serverId: string, timeoutMs = 15_000): Promise { + return new Promise(resolve => { + let unlisten: (() => void) | undefined; + const timer = setTimeout(() => { + unlisten?.(); + resolve(); + }, timeoutMs); + void subscribeLibrarySyncIdle(p => { + if (p.serverId !== serverId) return; + clearTimeout(timer); + unlisten?.(); + resolve(); + }).then(fn => { + unlisten = fn; + }); + }); +} + async function invokeSync(serverId: string, kind: LibrarySyncQueueKind): Promise { if (kind === 'verify') { await librarySyncVerifyIntegrity({ serverId });