feat(library-slow-mode): Library providers can now declare "slow mode" for high latency operations

This commit is contained in:
barlind
2026-07-08 17:06:19 +02:00
parent 950e348357
commit ee33ac7ba2
9 changed files with 90 additions and 7 deletions
+7 -1
View File
@@ -1871,7 +1871,10 @@ function _setLibraryLoadingMessage(containerId, countId, message) {
const count = document.getElementById(countId);
if (count) count.textContent = 'Loading source...';
if (container) {
container.innerHTML = `<div class="rounded-xl border border-gray-800/50 bg-dark-700/30 px-4 py-6 text-sm text-gray-300">${esc(message || 'Loading library...')}</div>`;
container.innerHTML = `<div role="status" aria-live="polite" class="rounded-xl border border-gray-800/50 bg-dark-700/30 px-4 py-6 text-sm text-gray-300 flex items-center gap-3">
<span class="inline-block h-4 w-4 rounded-full border-2 border-gray-600 border-t-accent animate-spin" aria-hidden="true"></span>
<span>${esc(message || 'Loading library...')}</span>
</div>`;
}
}
@@ -1880,6 +1883,9 @@ function _libraryLoadingText() {
if (!provider || provider.id === 'local' || provider.kind === 'local') {
return 'Loading library...';
}
if (provider.slow === true) {
return `Loading ${provider.label || provider.id}... this source may take a while.`;
}
return `Connecting to ${provider.label || provider.id}...`;
}
+3
View File
@@ -13,6 +13,7 @@
label: 'My Library',
kind: 'local',
capabilities: ['library.read', 'art.read', 'song.play', 'favorite.write', 'metadata.write', 'retune.write'],
slow: false,
default: true,
});
const PROVIDER_OPERATIONS = Object.freeze({
@@ -72,6 +73,7 @@
label: String(provider.label || provider.name || providerId),
kind: String(provider.kind || (providerId === 'local' ? 'local' : 'remote')),
capabilities: _strings(provider.capabilities),
slow: provider.slow === true || provider.is_slow === true || provider.slow_mode === true,
default: provider.default === true || providerId === 'local',
};
}
@@ -141,6 +143,7 @@
label: provider.label || providerId,
kind: provider.kind || (providerId === 'local' ? 'local' : 'remote'),
capabilities: _strings(provider.capabilities),
slow: provider.slow === true,
ownerPluginId: _ownerPluginId(provider) || null,
default: provider.default === true || providerId === 'local',
},
+1 -1
View File
File diff suppressed because one or more lines are too long
+28 -3
View File
@@ -64,6 +64,7 @@
filters: { arr_has: [], arr_lacks: [], stem_has: [], stem_lacks: [], lyrics: '', tunings: [], mastery: [], match: [], genre: [] },
page: 0, total: 0, loading: false, built: false, accuracy: {}, tuningNames: [], genres: [],
providers: [],
artistCatalog: [], renderedHash: '',
scrollBound: false,
songsById: {}, selectMode: false, selected: new Set(),
@@ -442,6 +443,24 @@
async function jget(url) { try { const r = await fetch(url); return r.ok ? r.json() : null; } catch (e) { return null; } }
function activeProvider() {
return (state.providers || []).find((provider) => provider.id === state.provider) || { id: state.provider || 'local', label: 'My Library', kind: 'local' };
}
function providerLoadingText() {
const provider = activeProvider();
if (!provider || provider.id === 'local' || provider.kind === 'local') return 'Loading library...';
if (provider.slow === true) return 'Loading ' + (provider.label || provider.id) + '... this source may take a while.';
return 'Connecting to ' + (provider.label || provider.id) + '...';
}
function loadingPanelHtml() {
return '<div role="status" aria-live="polite" class="rounded-lg border border-fb-border/50 bg-fb-card/50 px-4 py-5 text-sm text-fb-textDim flex items-center gap-3" style="grid-column:1/-1">' +
'<span class="inline-block h-4 w-4 rounded-full border-2 border-fb-border border-t-fb-primary animate-spin" aria-hidden="true"></span>' +
'<span>' + esc(providerLoadingText()) + '</span>' +
'</div>';
}
// ── Provider-aware song helpers ────────────────────────────────────────
// Remote library providers (feedBack-plugin-remote-library-*) expose songs
// by provider-owned id with their own art/sync/play flow. Reuse the legacy
@@ -2090,6 +2109,10 @@
grid.style.top = '0px';
const sizer = _sizerEl();
if (sizer) sizer.style.height = '0px';
const countEl = document.getElementById('v3-songs-count');
if (countEl) countEl.textContent = 'Loading source...';
grid.innerHTML = loadingPanelHtml();
if (sizer) sizer.style.height = grid.offsetHeight + 'px';
}
state.loading = true;
await _loadPage(0);
@@ -2385,7 +2408,7 @@
async function loadAlbums() {
const host = document.getElementById('v3-songs-albums');
if (!host) return;
host.innerHTML = '<p class="text-fb-textDim text-sm">Loading…</p>';
host.innerHTML = '<div role="status" aria-live="polite" class="text-fb-textDim text-sm flex items-center gap-3"><span class="inline-block h-4 w-4 rounded-full border-2 border-fb-border border-t-fb-primary animate-spin" aria-hidden="true"></span><span>' + esc(providerLoadingText()) + '</span></div>';
const data = await jget('/api/library/albums?' + queryParams().toString());
const albums = (data && data.albums) || [];
if (!albums.length) { host.innerHTML = '<p class="text-fb-textDim text-sm py-8 text-center">No albums match.</p>'; return; }
@@ -2770,7 +2793,7 @@
// (e.g. toggling select mode) restores them instead of collapsing all.
const openArtists = new Set(
[...host.querySelectorAll('details[open]')].map((d) => d.getAttribute('data-artist')));
host.innerHTML = '<p class="text-fb-textDim text-sm">Loading…</p>';
host.innerHTML = '<div role="status" aria-live="polite" class="text-fb-textDim text-sm flex items-center gap-3"><span class="inline-block h-4 w-4 rounded-full border-2 border-fb-border border-t-fb-primary animate-spin" aria-hidden="true"></span><span>' + esc(providerLoadingText()) + '</span></div>';
// Page through ALL artists — the endpoint clamps size to 100, so a
// single request would silently truncate libraries with >100 artists.
const artists = [];
@@ -3457,12 +3480,14 @@
const snap = await fn.call(lp);
if (snap && Array.isArray(snap.providers)) {
state.provider = snap.current || (snap.providers[0] && snap.providers[0].id) || 'local';
state.providers = snap.providers;
return snap.providers;
}
}
} catch (e) { /* */ }
const data = await jget('/api/library/providers');
return (data && data.providers) || [{ id: 'local', label: 'My Library' }];
state.providers = (data && data.providers) || [{ id: 'local', label: 'My Library', kind: 'local' }];
return state.providers;
}
async function render() {