Files
minstrel/web/src/lib/components/ReacquisitionSettingsCard.svelte
T

111 lines
4.6 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import { plural } from '#lib/utils/plural.js';
import { onMount } from 'svelte';
import {
getReacquisitionSettings,
updateReacquisitionSettings,
type ReacquisitionSettings
} from '#lib/api/admin.js';
import { errMessage } from '#lib/api/errors.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
import SettingsGroup from '#lib/components/settings/SettingsGroup.svelte';
import SettingRow from '#lib/components/settings/SettingRow.svelte';
// Policy for putting an album whose files went missing back on the wanted
// list (milestone #290, M502 #5520). On Admin › Acquisition with the rest
// of how music is got (#5607); Missing files says what it is set to and
// links here. Searching for a want, and retrying, is the search
// schedule's job, so there is no retry policy here.
const button =
'rounded-md border border-border px-3 py-1.5 text-sm text-text-secondary hover:bg-surface-hover hover:text-text-primary disabled:opacity-50';
const field = 'w-24 rounded-md border border-border bg-background px-2 py-1.5 text-sm';
let saved = $state<ReacquisitionSettings | null>(null);
let form = $state<ReacquisitionSettings | null>(null);
let saving = $state(false);
let loadError = $state(false);
const dirty = $derived(
!!saved && !!form && JSON.stringify(stripDerived(saved)) !== JSON.stringify(stripDerived(form))
);
// unnameable_albums is server-computed and read-only; comparing it would
// make the form look dirty whenever the library changed underneath. Listed
// explicitly rather than destructured-and-spread so no lint rule has to be
// argued with about an intentionally unused binding.
function stripDerived(s: ReacquisitionSettings) {
return {
enabled: s.enabled,
grace_hours: s.grace_hours,
max_per_pass: s.max_per_pass
};
}
async function load() {
try {
saved = await getReacquisitionSettings();
form = { ...saved };
loadError = false;
} catch {
loadError = true;
}
}
onMount(load);
async function save() {
if (!form) return;
saving = true;
try {
saved = await updateReacquisitionSettings(form);
form = { ...saved };
pushToast('Re-acquisition settings saved.');
} catch (e) {
// The server validates the same ranges the database CHECKs enforce and
// names the offending field, so surface its message rather than a
// generic failure. errMessage, not `e.message`: the API client throws a
// plain {code, message} object, never an Error, so an instanceof check
// here silently discarded every reason the server gave (#3937).
pushToast(errMessage(e), 'error');
} finally {
saving = false;
}
}
</script>
<SettingsGroup title="Missing files" id="reacquisition" hint="Put an album back on the wanted list when its files go missing.">
{#if loadError}
<p class="px-4 py-3 text-sm text-error-fg">
Couldn't load re-acquisition settings.
<button type="button" class="underline hover:no-underline" onclick={load}>Try again</button>
</p>
{:else if form === null}
<p class="px-4 py-3 text-sm text-text-secondary">Loading…</p>
{:else}
<SettingRow label="Want missing albums back" for="reacq-enabled" hint="Per album, not per track: a lost folder is one want.">
<input id="reacq-enabled" type="checkbox" role="switch" aria-checked={form.enabled} bind:checked={form.enabled} class="h-4 w-4 cursor-pointer accent-accent" />
</SettingRow>
<SettingRow label="Wait before wanting it back" for="reacq-grace" hint="Rides out a drive that didn't mount in time.">
<input id="reacq-grace" type="number" min="1" max="720" bind:value={form.grace_hours} class={field} />
<span class="text-sm text-text-secondary">hours</span>
</SettingRow>
<SettingRow label="Albums per sweep" for="reacq-max" hint="Each hour, so a large loss trickles in.">
<input id="reacq-max" type="number" min="1" max="200" bind:value={form.max_per_pass} class={field} />
</SettingRow>
{#if saved && saved.unnameable_albums > 0}
<!-- Said outright: otherwise rows that never get wanted read as a broken feature. -->
<p class="px-4 py-3 text-sm text-text-primary">
{plural(saved.unnameable_albums, 'album has', 'albums have')} missing files but no MusicBrainz ID,
so {saved.unnameable_albums === 1 ? 'it' : 'they'} can't be wanted. Tag and re-scan to fix.
</p>
{/if}
<div class="flex justify-end px-4 py-3">
<button type="button" class={button} disabled={!dirty || saving} onclick={save}>
{saving ? 'Saving…' : 'Save'}
</button>
</div>
{/if}
</SettingsGroup>