feat(settings): rework settings page; remove unused SkeletonLoader/StorageManagement

Settings page refactor plus supporting docs (requirements, ux-flows,
traceability) and the frontend-domain-model spec with implementation-status
banner. Removes SkeletonLoader and StorageManagement components (no remaining
references).
This commit is contained in:
2026-07-23 22:18:37 +02:00
parent f89b241ad6
commit 9b1c9b3c91
7 changed files with 817 additions and 738 deletions
+70 -52
View File
@@ -1,4 +1,4 @@
<!-- TRACES: UR-023, UR-029 | DR-048, DR-077 -->
<!-- TRACES: UR-023, UR-029, UR-057 | DR-048, DR-077, DR-086 -->
<script lang="ts">
import { onMount } from "svelte";
import { commands } from "$lib/api/bindings";
@@ -63,8 +63,6 @@
let networkDetectionSupported = $state(false);
let loading = $state(true);
let saving = $state(false);
let saveMessage = $state("");
// Image cache state
let cacheStats = $state<ImageCacheStats | null>(null);
@@ -153,55 +151,99 @@
return Math.min(100, (cacheStats.totalSizeBytes / cacheStats.limitBytes) * 100);
}
async function saveSettings() {
// Settings apply the moment the user changes a control — there is no Save
// button. Each helper writes just the settings group it owns so a single
// toggle doesn't re-push unrelated state.
async function persistAudio() {
try {
saving = true;
saveMessage = "";
await Promise.all([
commands.playerSetAudioSettings(settings),
commands.playerSetVideoSettings(videoSettings),
updateCacheConfig(cacheConfig),
]);
await commands.playerSetAudioSettings(settings);
} catch (e) {
console.error("Failed to save audio settings:", e);
}
}
async function persistVideo() {
try {
await commands.playerSetVideoSettings(videoSettings);
} catch (e) {
console.error("Failed to save video settings:", e);
}
}
async function persistCache() {
try {
await updateCacheConfig(cacheConfig);
// Re-report the network so the backend re-evaluates the gate against the
// just-saved wifi-only preference, releasing or holding the queue now
// just-changed wifi-only preference, releasing or holding the queue now
// rather than at the next network change.
await reportNetworkState();
saveMessage = "Settings saved successfully!";
setTimeout(() => {
saveMessage = "";
}, 3000);
} catch (e) {
console.error("Failed to save settings:", e);
saveMessage = "Failed to save settings";
} finally {
saving = false;
console.error("Failed to save download settings:", e);
}
}
// Slider drags fire `input` on every tick; update the live display there but
// only persist on `change` (pointer release) so we don't spam the backend.
function handleCrossfadeInput(e: Event) {
const target = e.target as HTMLInputElement;
settings.crossfadeDuration = parseFloat(target.value);
}
function handleCrossfadeChange(e: Event) {
const target = e.target as HTMLInputElement;
settings.crossfadeDuration = parseFloat(target.value);
persistAudio();
}
function handleGaplessToggle() {
settings.gaplessPlayback = !settings.gaplessPlayback;
persistAudio();
}
function handleNormalizeToggle() {
settings.normalizeVolume = !settings.normalizeVolume;
persistAudio();
}
function handleVolumeLevelChange(level: VolumeLevel) {
settings.volumeLevel = level;
persistAudio();
}
function handleAutoPlayToggle() {
videoSettings.autoPlayNextEpisode = !videoSettings.autoPlayNextEpisode;
persistVideo();
}
function handleCountdownInput(e: Event) {
const target = e.target as HTMLInputElement;
videoSettings.autoPlayCountdownSeconds = parseInt(target.value, 10);
}
function handleCountdownChange(e: Event) {
const target = e.target as HTMLInputElement;
videoSettings.autoPlayCountdownSeconds = parseInt(target.value, 10);
persistVideo();
}
function handleEpisodeLimitChange(value: number) {
videoSettings.autoPlayMaxEpisodes = value;
persistVideo();
}
function handleSmartCachingToggle() {
cacheConfig.albumAffinityEnabled = !cacheConfig.albumAffinityEnabled;
persistCache();
}
function handleQueuePrecacheToggle() {
cacheConfig.queuePrecacheEnabled = !cacheConfig.queuePrecacheEnabled;
persistCache();
}
function handleWifiOnlyToggle() {
cacheConfig.wifiOnly = !cacheConfig.wifiOnly;
persistCache();
}
</script>
@@ -279,7 +321,8 @@
max="12"
step="0.5"
value={settings.crossfadeDuration}
oninput={handleCrossfadeChange}
oninput={handleCrossfadeInput}
onchange={handleCrossfadeChange}
class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer accent-[var(--color-jellyfin)]"
/>
<div class="flex justify-between text-xs text-gray-500 mt-2">
@@ -425,7 +468,8 @@
max="30"
step="5"
value={videoSettings.autoPlayCountdownSeconds}
oninput={handleCountdownChange}
oninput={handleCountdownInput}
onchange={handleCountdownChange}
class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer accent-[var(--color-jellyfin)]"
/>
<div class="flex justify-between text-xs text-gray-500 mt-2">
@@ -445,7 +489,7 @@
<div class="grid grid-cols-3 md:grid-cols-6 gap-2">
{#each episodeLimitOptions as option}
<button
onclick={() => { videoSettings.autoPlayMaxEpisodes = option.value; }}
onclick={() => handleEpisodeLimitChange(option.value)}
class="py-3 px-3 rounded-lg transition-all text-sm
{videoSettings.autoPlayMaxEpisodes === option.value
? 'bg-[var(--color-jellyfin)] text-white'
@@ -597,9 +641,7 @@
</p>
</div>
<button
onclick={() =>
(cacheConfig.albumAffinityEnabled =
!cacheConfig.albumAffinityEnabled)}
onclick={handleSmartCachingToggle}
class="relative inline-flex h-8 w-14 items-center rounded-full transition-colors {cacheConfig.albumAffinityEnabled
? 'bg-[var(--color-jellyfin)]'
: 'bg-gray-600'}"
@@ -626,9 +668,7 @@
</p>
</div>
<button
onclick={() =>
(cacheConfig.queuePrecacheEnabled =
!cacheConfig.queuePrecacheEnabled)}
onclick={handleQueuePrecacheToggle}
class="relative inline-flex h-8 w-14 items-center rounded-full transition-colors {cacheConfig.queuePrecacheEnabled
? 'bg-[var(--color-jellyfin)]'
: 'bg-gray-600'}"
@@ -660,7 +700,7 @@
</p>
</div>
<button
onclick={() => (cacheConfig.wifiOnly = !cacheConfig.wifiOnly)}
onclick={handleWifiOnlyToggle}
class="relative inline-flex h-8 w-14 items-center rounded-full transition-colors {cacheConfig.wifiOnly
? 'bg-[var(--color-jellyfin)]'
: 'bg-gray-600'} {networkDetectionSupported
@@ -680,28 +720,6 @@
</div>
</div>
<!-- Save Button -->
<div class="flex items-center justify-between">
<div class="text-sm">
{#if saveMessage}
<span
class="text-{saveMessage.includes('success')
? 'green'
: 'red'}-400"
>
{saveMessage}
</span>
{/if}
</div>
<button
onclick={saveSettings}
disabled={saving}
class="px-6 py-3 bg-[var(--color-jellyfin)] text-white rounded-lg font-semibold hover:opacity-90 transition-opacity disabled:opacity-50 disabled:cursor-not-allowed"
>
{saving ? "Saving..." : "Save Settings"}
</button>
</div>
<!-- Info Box -->
<div class="bg-blue-900/20 border border-blue-800 rounded-lg p-4">
<div class="flex gap-3">