mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 14:55:43 +00:00
fix(settings): show excluding state on library index exclude action
Flush UI before the async unbind, disable repeat clicks, cancel an active sync when needed, and label the button "Excluding…" / localized equivalent.
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { flushSync } from 'react-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { DatabaseZap } from 'lucide-react';
|
import { DatabaseZap } from 'lucide-react';
|
||||||
import { useAuthStore } from '../../store/authStore';
|
import { useAuthStore } from '../../store/authStore';
|
||||||
@@ -18,7 +19,7 @@ import {
|
|||||||
bootstrapIndexedServer,
|
bootstrapIndexedServer,
|
||||||
type BindServerResult,
|
type BindServerResult,
|
||||||
} from '../../utils/library/librarySession';
|
} from '../../utils/library/librarySession';
|
||||||
import { enqueueLibrarySync } from '../../utils/library/librarySyncQueue';
|
import { enqueueLibrarySync, waitForLibrarySyncIdle } from '../../utils/library/librarySyncQueue';
|
||||||
import { syncIngestDisplayCount } from '../../utils/library/libraryReady';
|
import { syncIngestDisplayCount } from '../../utils/library/libraryReady';
|
||||||
import { serverListDisplayLabel } from '../../utils/server/serverDisplayName';
|
import { serverListDisplayLabel } from '../../utils/server/serverDisplayName';
|
||||||
import LibraryIndexServerRow, { type LibraryServerConnection } from './LibraryIndexServerRow';
|
import LibraryIndexServerRow, { type LibraryServerConnection } from './LibraryIndexServerRow';
|
||||||
@@ -58,6 +59,7 @@ export default function LibraryIndexSection() {
|
|||||||
const [connectionByServer, setConnectionByServer] = useState<Record<string, LibraryServerConnection>>({});
|
const [connectionByServer, setConnectionByServer] = useState<Record<string, LibraryServerConnection>>({});
|
||||||
const [progressByServer, setProgressByServer] = useState<Record<string, string | null>>({});
|
const [progressByServer, setProgressByServer] = useState<Record<string, string | null>>({});
|
||||||
const [busyServerId, setBusyServerId] = useState<string | null>(null);
|
const [busyServerId, setBusyServerId] = useState<string | null>(null);
|
||||||
|
const [excludingServerId, setExcludingServerId] = useState<string | null>(null);
|
||||||
const [bootstrapping, setBootstrapping] = useState(false);
|
const [bootstrapping, setBootstrapping] = useState(false);
|
||||||
|
|
||||||
const pollTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const pollTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
@@ -130,6 +132,7 @@ export default function LibraryIndexSection() {
|
|||||||
setConnectionByServer({});
|
setConnectionByServer({});
|
||||||
setProgressByServer({});
|
setProgressByServer({});
|
||||||
setBusyServerId(null);
|
setBusyServerId(null);
|
||||||
|
setExcludingServerId(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
void runBootstrap();
|
void runBootstrap();
|
||||||
@@ -263,8 +266,21 @@ export default function LibraryIndexSection() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleExcludeServer = async (serverId: string) => {
|
const handleExcludeServer = async (serverId: string) => {
|
||||||
setBootstrapping(true);
|
if (excludingServerId) return;
|
||||||
|
flushSync(() => setExcludingServerId(serverId));
|
||||||
try {
|
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);
|
await librarySyncClearSession(serverId);
|
||||||
setServerSyncExcluded(serverId, true);
|
setServerSyncExcluded(serverId, true);
|
||||||
setStatusByServer(prev => {
|
setStatusByServer(prev => {
|
||||||
@@ -277,10 +293,18 @@ export default function LibraryIndexSection() {
|
|||||||
delete next[serverId];
|
delete next[serverId];
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
|
setProgressByServer(prev => {
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[serverId];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
if (busyServerId === serverId) {
|
||||||
|
setBusyServerId(null);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast(t('settings.libraryIndexBindError', { error: String(e) }), 5000, 'error');
|
showToast(t('settings.libraryIndexBindError', { error: String(e) }), 5000, 'error');
|
||||||
} finally {
|
} 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 (
|
return (
|
||||||
<SettingsSubSection
|
<SettingsSubSection
|
||||||
@@ -349,7 +373,10 @@ export default function LibraryIndexSection() {
|
|||||||
connection={connectionByServer[srv.id] ?? 'unknown'}
|
connection={connectionByServer[srv.id] ?? 'unknown'}
|
||||||
progressLabel={progressByServer[srv.id] ?? null}
|
progressLabel={progressByServer[srv.id] ?? null}
|
||||||
busy={busyServerId === srv.id}
|
busy={busyServerId === srv.id}
|
||||||
actionsDisabled={globalBusy && busyServerId !== srv.id}
|
excluding={excludingServerId === srv.id}
|
||||||
|
actionsDisabled={
|
||||||
|
(globalBusy && busyServerId !== srv.id) || excludingServerId != null
|
||||||
|
}
|
||||||
onFullSync={() => void runServerAction(srv.id, 'full')}
|
onFullSync={() => void runServerAction(srv.id, 'full')}
|
||||||
onDeltaSync={() => void runServerAction(srv.id, 'delta')}
|
onDeltaSync={() => void runServerAction(srv.id, 'delta')}
|
||||||
onVerify={() => void runServerAction(srv.id, 'verify')}
|
onVerify={() => void runServerAction(srv.id, 'verify')}
|
||||||
@@ -376,7 +403,7 @@ export default function LibraryIndexSection() {
|
|||||||
type="button"
|
type="button"
|
||||||
className="btn btn-surface"
|
className="btn btn-surface"
|
||||||
style={{ fontSize: 12, padding: '4px 10px' }}
|
style={{ fontSize: 12, padding: '4px 10px' }}
|
||||||
disabled={bootstrapping}
|
disabled={bootstrapping || excludingServerId != null}
|
||||||
onClick={() => void handleIncludeServer(srv.id)}
|
onClick={() => void handleIncludeServer(srv.id)}
|
||||||
>
|
>
|
||||||
{t('settings.libraryIndexIncludeServer')}
|
{t('settings.libraryIndexIncludeServer')}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ interface LibraryIndexServerRowProps {
|
|||||||
connection: LibraryServerConnection;
|
connection: LibraryServerConnection;
|
||||||
progressLabel: string | null;
|
progressLabel: string | null;
|
||||||
busy: boolean;
|
busy: boolean;
|
||||||
|
excluding: boolean;
|
||||||
actionsDisabled: boolean;
|
actionsDisabled: boolean;
|
||||||
onFullSync: () => void;
|
onFullSync: () => void;
|
||||||
onDeltaSync: () => void;
|
onDeltaSync: () => void;
|
||||||
@@ -33,6 +34,7 @@ export default function LibraryIndexServerRow({
|
|||||||
connection,
|
connection,
|
||||||
progressLabel,
|
progressLabel,
|
||||||
busy,
|
busy,
|
||||||
|
excluding,
|
||||||
actionsDisabled,
|
actionsDisabled,
|
||||||
onFullSync,
|
onFullSync,
|
||||||
onDeltaSync,
|
onDeltaSync,
|
||||||
@@ -133,11 +135,14 @@ export default function LibraryIndexServerRow({
|
|||||||
type="button"
|
type="button"
|
||||||
className="btn btn-ghost"
|
className="btn btn-ghost"
|
||||||
style={{ fontSize: 12, padding: '4px 10px', color: 'var(--text-muted)' }}
|
style={{ fontSize: 12, padding: '4px 10px', color: 'var(--text-muted)' }}
|
||||||
disabled={actionsDisabled}
|
disabled={actionsDisabled || excluding}
|
||||||
|
aria-busy={excluding}
|
||||||
onClick={onExclude}
|
onClick={onExclude}
|
||||||
>
|
>
|
||||||
<Ban size={13} />
|
<Ban size={13} />
|
||||||
{t('settings.libraryIndexExcludeServer')}
|
{excluding
|
||||||
|
? t('settings.libraryIndexExcludingServer')
|
||||||
|
: t('settings.libraryIndexExcludeServer')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -261,6 +261,7 @@ export const settings = {
|
|||||||
libraryIndexFullResync: 'Vollständige Neusynchronisation',
|
libraryIndexFullResync: 'Vollständige Neusynchronisation',
|
||||||
libraryIndexDeltaSync: 'Delta-Synchronisation',
|
libraryIndexDeltaSync: 'Delta-Synchronisation',
|
||||||
libraryIndexExcludeServer: 'Von Synchronisation ausschließen',
|
libraryIndexExcludeServer: 'Von Synchronisation ausschließen',
|
||||||
|
libraryIndexExcludingServer: 'Wird ausgeschlossen…',
|
||||||
libraryIndexExcludedTitle: 'Von Synchronisation ausgeschlossen',
|
libraryIndexExcludedTitle: 'Von Synchronisation ausgeschlossen',
|
||||||
libraryIndexIncludeServer: 'Wieder einschließen',
|
libraryIndexIncludeServer: 'Wieder einschließen',
|
||||||
libraryIndexStatus: 'Status',
|
libraryIndexStatus: 'Status',
|
||||||
|
|||||||
@@ -264,6 +264,7 @@ export const settings = {
|
|||||||
libraryIndexFullResync: 'Full resync',
|
libraryIndexFullResync: 'Full resync',
|
||||||
libraryIndexDeltaSync: 'Delta sync',
|
libraryIndexDeltaSync: 'Delta sync',
|
||||||
libraryIndexExcludeServer: 'Exclude from sync',
|
libraryIndexExcludeServer: 'Exclude from sync',
|
||||||
|
libraryIndexExcludingServer: 'Excluding…',
|
||||||
libraryIndexExcludedTitle: 'Excluded from sync',
|
libraryIndexExcludedTitle: 'Excluded from sync',
|
||||||
libraryIndexIncludeServer: 'Include again',
|
libraryIndexIncludeServer: 'Include again',
|
||||||
libraryIndexStatus: 'Status',
|
libraryIndexStatus: 'Status',
|
||||||
|
|||||||
@@ -259,6 +259,7 @@ export const settings = {
|
|||||||
libraryIndexFullResync: 'Resincronización completa',
|
libraryIndexFullResync: 'Resincronización completa',
|
||||||
libraryIndexDeltaSync: 'Sincronización delta',
|
libraryIndexDeltaSync: 'Sincronización delta',
|
||||||
libraryIndexExcludeServer: 'Excluir de la sincronización',
|
libraryIndexExcludeServer: 'Excluir de la sincronización',
|
||||||
|
libraryIndexExcludingServer: 'Excluyendo…',
|
||||||
libraryIndexExcludedTitle: 'Excluidos de la sincronización',
|
libraryIndexExcludedTitle: 'Excluidos de la sincronización',
|
||||||
libraryIndexIncludeServer: 'Incluir de nuevo',
|
libraryIndexIncludeServer: 'Incluir de nuevo',
|
||||||
libraryIndexStatus: 'Estado',
|
libraryIndexStatus: 'Estado',
|
||||||
|
|||||||
@@ -257,6 +257,7 @@ export const settings = {
|
|||||||
libraryIndexFullResync: 'Resynchronisation complète',
|
libraryIndexFullResync: 'Resynchronisation complète',
|
||||||
libraryIndexDeltaSync: 'Synchronisation delta',
|
libraryIndexDeltaSync: 'Synchronisation delta',
|
||||||
libraryIndexExcludeServer: 'Exclure de la synchronisation',
|
libraryIndexExcludeServer: 'Exclure de la synchronisation',
|
||||||
|
libraryIndexExcludingServer: 'Exclusion…',
|
||||||
libraryIndexExcludedTitle: 'Exclus de la synchronisation',
|
libraryIndexExcludedTitle: 'Exclus de la synchronisation',
|
||||||
libraryIndexIncludeServer: 'Réinclure',
|
libraryIndexIncludeServer: 'Réinclure',
|
||||||
libraryIndexStatus: 'État',
|
libraryIndexStatus: 'État',
|
||||||
|
|||||||
@@ -256,6 +256,7 @@ export const settings = {
|
|||||||
libraryIndexFullResync: 'Full resynkronisering',
|
libraryIndexFullResync: 'Full resynkronisering',
|
||||||
libraryIndexDeltaSync: 'Delta-synkronisering',
|
libraryIndexDeltaSync: 'Delta-synkronisering',
|
||||||
libraryIndexExcludeServer: 'Ekskluder fra synkronisering',
|
libraryIndexExcludeServer: 'Ekskluder fra synkronisering',
|
||||||
|
libraryIndexExcludingServer: 'Ekskluderer…',
|
||||||
libraryIndexExcludedTitle: 'Ekskludert fra synkronisering',
|
libraryIndexExcludedTitle: 'Ekskludert fra synkronisering',
|
||||||
libraryIndexIncludeServer: 'Inkluder igjen',
|
libraryIndexIncludeServer: 'Inkluder igjen',
|
||||||
libraryIndexStatus: 'Status',
|
libraryIndexStatus: 'Status',
|
||||||
|
|||||||
@@ -257,6 +257,7 @@ export const settings = {
|
|||||||
libraryIndexFullResync: 'Volledige resync',
|
libraryIndexFullResync: 'Volledige resync',
|
||||||
libraryIndexDeltaSync: 'Delta-sync',
|
libraryIndexDeltaSync: 'Delta-sync',
|
||||||
libraryIndexExcludeServer: 'Uitsluiten van synchronisatie',
|
libraryIndexExcludeServer: 'Uitsluiten van synchronisatie',
|
||||||
|
libraryIndexExcludingServer: 'Uitsluiten…',
|
||||||
libraryIndexExcludedTitle: 'Uitgesloten van synchronisatie',
|
libraryIndexExcludedTitle: 'Uitgesloten van synchronisatie',
|
||||||
libraryIndexIncludeServer: 'Weer opnemen',
|
libraryIndexIncludeServer: 'Weer opnemen',
|
||||||
libraryIndexStatus: 'Status',
|
libraryIndexStatus: 'Status',
|
||||||
|
|||||||
@@ -263,6 +263,7 @@ export const settings = {
|
|||||||
libraryIndexFullResync: 'Resincronizare completă',
|
libraryIndexFullResync: 'Resincronizare completă',
|
||||||
libraryIndexDeltaSync: 'Sincronizare delta',
|
libraryIndexDeltaSync: 'Sincronizare delta',
|
||||||
libraryIndexExcludeServer: 'Exclude din sincronizare',
|
libraryIndexExcludeServer: 'Exclude din sincronizare',
|
||||||
|
libraryIndexExcludingServer: 'Se exclude…',
|
||||||
libraryIndexExcludedTitle: 'Excluse din sincronizare',
|
libraryIndexExcludedTitle: 'Excluse din sincronizare',
|
||||||
libraryIndexIncludeServer: 'Include din nou',
|
libraryIndexIncludeServer: 'Include din nou',
|
||||||
libraryIndexStatus: 'Stare',
|
libraryIndexStatus: 'Stare',
|
||||||
|
|||||||
@@ -269,6 +269,7 @@ export const settings = {
|
|||||||
libraryIndexFullResync: 'Полная пересинхронизация',
|
libraryIndexFullResync: 'Полная пересинхронизация',
|
||||||
libraryIndexDeltaSync: 'Быстрая дельта',
|
libraryIndexDeltaSync: 'Быстрая дельта',
|
||||||
libraryIndexExcludeServer: 'Исключить из синхронизации',
|
libraryIndexExcludeServer: 'Исключить из синхронизации',
|
||||||
|
libraryIndexExcludingServer: 'Отключается…',
|
||||||
libraryIndexExcludedTitle: 'Исключены из синхронизации',
|
libraryIndexExcludedTitle: 'Исключены из синхронизации',
|
||||||
libraryIndexIncludeServer: 'Включить снова',
|
libraryIndexIncludeServer: 'Включить снова',
|
||||||
libraryIndexStatus: 'Статус',
|
libraryIndexStatus: 'Статус',
|
||||||
|
|||||||
@@ -256,6 +256,7 @@ export const settings = {
|
|||||||
libraryIndexFullResync: '完全重新同步',
|
libraryIndexFullResync: '完全重新同步',
|
||||||
libraryIndexDeltaSync: '增量同步',
|
libraryIndexDeltaSync: '增量同步',
|
||||||
libraryIndexExcludeServer: '排除同步',
|
libraryIndexExcludeServer: '排除同步',
|
||||||
|
libraryIndexExcludingServer: '正在排除…',
|
||||||
libraryIndexExcludedTitle: '已排除同步',
|
libraryIndexExcludedTitle: '已排除同步',
|
||||||
libraryIndexIncludeServer: '重新纳入',
|
libraryIndexIncludeServer: '重新纳入',
|
||||||
libraryIndexStatus: '状态',
|
libraryIndexStatus: '状态',
|
||||||
|
|||||||
@@ -62,6 +62,25 @@ function waitForServerIdle(serverId: string): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Wait until a server emits `library:sync-idle`, or time out (best-effort). */
|
||||||
|
export function waitForLibrarySyncIdle(serverId: string, timeoutMs = 15_000): Promise<void> {
|
||||||
|
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<void> {
|
async function invokeSync(serverId: string, kind: LibrarySyncQueueKind): Promise<void> {
|
||||||
if (kind === 'verify') {
|
if (kind === 'verify') {
|
||||||
await librarySyncVerifyIntegrity({ serverId });
|
await librarySyncVerifyIntegrity({ serverId });
|
||||||
|
|||||||
Reference in New Issue
Block a user