feat(chrome): shared account menu and global app header
Move account actions (Settings, Downloads, Display preferences, Sign out) out of the library-only header into a shared AccountMenu anchored in a global AppHeader, available on every authenticated non-immersive screen. Add a layoutShell helper deciding where chrome shows, expose serverName/serverUrl auth stores, and a display view-mode preference. The settings page also gains the UR-053 WiFi-only toggle. TRACES: UR-054 | DR-075, DR-076, DR-077
This commit is contained in:
@@ -1,8 +1,13 @@
|
||||
<!-- TRACES: UR-023 | DR-048 -->
|
||||
<!-- TRACES: UR-023, UR-029 | DR-048, DR-077 -->
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { commands } from "$lib/api/bindings";
|
||||
import type { AudioSettings, VideoSettings, VolumeLevel } from "$lib/api/bindings";
|
||||
import type {
|
||||
AudioSettings,
|
||||
CacheConfig,
|
||||
VideoSettings,
|
||||
VolumeLevel,
|
||||
} from "$lib/api/bindings";
|
||||
import {
|
||||
getCacheStats,
|
||||
setCacheLimit,
|
||||
@@ -12,6 +17,13 @@
|
||||
bytesToGb,
|
||||
type ImageCacheStats,
|
||||
} from "$lib/services/imageCache";
|
||||
import { getCacheConfig, updateCacheConfig } from "$lib/services/preload";
|
||||
import SearchGroupOrderList from "$lib/components/settings/SearchGroupOrderList.svelte";
|
||||
import { library, viewMode } from "$lib/stores/library";
|
||||
import {
|
||||
isNetworkDetectionSupported,
|
||||
reportNetworkState,
|
||||
} from "$lib/services/networkType";
|
||||
|
||||
const episodeLimitOptions = [
|
||||
{ value: 0, label: "Unlimited" },
|
||||
@@ -35,6 +47,21 @@
|
||||
autoPlayMaxEpisodes: 0,
|
||||
});
|
||||
|
||||
// Download/caching behaviour, incl. the WiFi-only gate (UR-053).
|
||||
let cacheConfig = $state<CacheConfig>({
|
||||
queuePrecacheEnabled: true,
|
||||
queuePrecacheCount: 3,
|
||||
albumAffinityEnabled: true,
|
||||
albumAffinityThreshold: 3,
|
||||
storageLimit: 10 * 1024 * 1024 * 1024,
|
||||
wifiOnly: false,
|
||||
});
|
||||
|
||||
// Whether the platform can actually detect the network type. On desktop it
|
||||
// can't, so the WiFi-only toggle would be inert — we disable and explain it
|
||||
// rather than offering a switch that does nothing.
|
||||
let networkDetectionSupported = $state(false);
|
||||
|
||||
let loading = $state(true);
|
||||
let saving = $state(false);
|
||||
let saveMessage = $state("");
|
||||
@@ -60,12 +87,15 @@
|
||||
async function loadSettings() {
|
||||
try {
|
||||
loading = true;
|
||||
const [audioResult, videoResult] = await Promise.all([
|
||||
networkDetectionSupported = isNetworkDetectionSupported();
|
||||
const [audioResult, videoResult, cacheResult] = await Promise.all([
|
||||
commands.playerGetAudioSettings(),
|
||||
commands.playerGetVideoSettings(),
|
||||
getCacheConfig(),
|
||||
]);
|
||||
settings = audioResult;
|
||||
videoSettings = videoResult;
|
||||
cacheConfig = cacheResult;
|
||||
// Load cache stats in parallel but don't block on it
|
||||
loadCacheStats();
|
||||
} catch (e) {
|
||||
@@ -130,7 +160,12 @@
|
||||
await Promise.all([
|
||||
commands.playerSetAudioSettings(settings),
|
||||
commands.playerSetVideoSettings(videoSettings),
|
||||
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
|
||||
// rather than at the next network change.
|
||||
await reportNetworkState();
|
||||
saveMessage = "Settings saved successfully!";
|
||||
setTimeout(() => {
|
||||
saveMessage = "";
|
||||
@@ -172,8 +207,8 @@
|
||||
|
||||
<div class="max-w-2xl mx-auto space-y-8 p-6">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-white mb-2">Audio Settings</h1>
|
||||
<p class="text-gray-400">Configure playback and audio processing</p>
|
||||
<h1 class="text-3xl font-bold text-white mb-2">Settings</h1>
|
||||
<p class="text-gray-400">Configure display, playback, and downloads</p>
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
@@ -182,6 +217,47 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-6">
|
||||
<!-- Display — grid/list preference, a second view onto the library
|
||||
viewMode store (same source of truth as the library page-header
|
||||
toggle, so the two stay in sync for free). -->
|
||||
<div id="display" class="scroll-mt-4 bg-[var(--color-surface)] rounded-lg p-6">
|
||||
<div class="mb-4">
|
||||
<h2 class="text-xl font-semibold text-white">Display</h2>
|
||||
<p class="text-sm text-gray-400 mt-1">
|
||||
How your library and collections are laid out
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-sm font-medium text-gray-300 mb-3">Layout</p>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
onclick={() => library.setViewMode("grid")}
|
||||
class="flex items-center justify-center gap-2 py-3 px-4 rounded-lg transition-all {$viewMode ===
|
||||
'grid'
|
||||
? 'bg-[var(--color-jellyfin)] text-white'
|
||||
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'}"
|
||||
aria-pressed={$viewMode === "grid"}
|
||||
>
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M4 4h7v7H4V4zm9 0h7v7h-7V4zM4 13h7v7H4v-7zm9 0h7v7h-7v-7z" />
|
||||
</svg>
|
||||
<span class="font-semibold">Grid</span>
|
||||
</button>
|
||||
<button
|
||||
onclick={() => library.setViewMode("list")}
|
||||
class="flex items-center justify-center gap-2 py-3 px-4 rounded-lg transition-all {$viewMode ===
|
||||
'list'
|
||||
? 'bg-[var(--color-jellyfin)] text-white'
|
||||
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'}"
|
||||
aria-pressed={$viewMode === "list"}
|
||||
>
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M3 5h18v2H3V5zm0 6h18v2H3v-2zm0 6h18v2H3v-2z" />
|
||||
</svg>
|
||||
<span class="font-semibold">List</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Crossfade -->
|
||||
<div class="bg-[var(--color-surface)] rounded-lg p-6">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
@@ -384,6 +460,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Settings -->
|
||||
<div class="border-t border-gray-700 pt-6">
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Search</h2>
|
||||
|
||||
<div class="bg-[var(--color-surface)] rounded-lg p-6">
|
||||
<h3 class="text-lg font-semibold text-white mb-1">Result Group Order</h3>
|
||||
<p class="text-sm text-gray-400 mb-4">
|
||||
Drag or use the arrows to choose the order search result groups appear in.
|
||||
Empty groups are hidden automatically.
|
||||
</p>
|
||||
<SearchGroupOrderList />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image Cache Settings -->
|
||||
<div class="border-t border-gray-700 pt-6">
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Image Cache</h2>
|
||||
@@ -507,12 +597,19 @@
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
class="relative inline-flex h-8 w-14 items-center rounded-full transition-colors bg-[var(--color-jellyfin)]"
|
||||
aria-label="Toggle album affinity (coming soon)"
|
||||
disabled
|
||||
onclick={() =>
|
||||
(cacheConfig.albumAffinityEnabled =
|
||||
!cacheConfig.albumAffinityEnabled)}
|
||||
class="relative inline-flex h-8 w-14 items-center rounded-full transition-colors {cacheConfig.albumAffinityEnabled
|
||||
? 'bg-[var(--color-jellyfin)]'
|
||||
: 'bg-gray-600'}"
|
||||
aria-label="Toggle smart caching"
|
||||
aria-pressed={cacheConfig.albumAffinityEnabled}
|
||||
>
|
||||
<span
|
||||
class="inline-block h-6 w-6 transform rounded-full bg-white transition-transform translate-x-7"
|
||||
class="inline-block h-6 w-6 transform rounded-full bg-white transition-transform {cacheConfig.albumAffinityEnabled
|
||||
? 'translate-x-7'
|
||||
: 'translate-x-1'}"
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -524,16 +621,24 @@
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold text-white">Queue Pre-caching</h3>
|
||||
<p class="text-sm text-gray-400 mt-1">
|
||||
Download next 5 tracks in queue automatically
|
||||
Download the next {cacheConfig.queuePrecacheCount} tracks in the queue
|
||||
automatically
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
class="relative inline-flex h-8 w-14 items-center rounded-full transition-colors bg-[var(--color-jellyfin)]"
|
||||
aria-label="Toggle queue pre-caching (coming soon)"
|
||||
disabled
|
||||
onclick={() =>
|
||||
(cacheConfig.queuePrecacheEnabled =
|
||||
!cacheConfig.queuePrecacheEnabled)}
|
||||
class="relative inline-flex h-8 w-14 items-center rounded-full transition-colors {cacheConfig.queuePrecacheEnabled
|
||||
? 'bg-[var(--color-jellyfin)]'
|
||||
: 'bg-gray-600'}"
|
||||
aria-label="Toggle queue pre-caching"
|
||||
aria-pressed={cacheConfig.queuePrecacheEnabled}
|
||||
>
|
||||
<span
|
||||
class="inline-block h-6 w-6 transform rounded-full bg-white transition-transform translate-x-7"
|
||||
class="inline-block h-6 w-6 transform rounded-full bg-white transition-transform {cacheConfig.queuePrecacheEnabled
|
||||
? 'translate-x-7'
|
||||
: 'translate-x-1'}"
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -545,16 +650,30 @@
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold text-white">WiFi Only</h3>
|
||||
<p class="text-sm text-gray-400 mt-1">
|
||||
Only download when connected to WiFi
|
||||
{#if networkDetectionSupported}
|
||||
Hold downloads unless on an unmetered network. Cellular and
|
||||
metered hotspots are excluded; WiFi and Ethernet are allowed.
|
||||
{:else}
|
||||
Only available on Android — this device has no metered
|
||||
connection to detect.
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
class="relative inline-flex h-8 w-14 items-center rounded-full transition-colors bg-[var(--color-jellyfin)]"
|
||||
aria-label="Toggle WiFi only downloads (coming soon)"
|
||||
disabled
|
||||
onclick={() => (cacheConfig.wifiOnly = !cacheConfig.wifiOnly)}
|
||||
class="relative inline-flex h-8 w-14 items-center rounded-full transition-colors {cacheConfig.wifiOnly
|
||||
? 'bg-[var(--color-jellyfin)]'
|
||||
: 'bg-gray-600'} {networkDetectionSupported
|
||||
? ''
|
||||
: 'opacity-50 cursor-not-allowed'}"
|
||||
aria-label="Toggle WiFi only downloads"
|
||||
aria-pressed={cacheConfig.wifiOnly}
|
||||
disabled={!networkDetectionSupported}
|
||||
>
|
||||
<span
|
||||
class="inline-block h-6 w-6 transform rounded-full bg-white transition-transform translate-x-7"
|
||||
class="inline-block h-6 w-6 transform rounded-full bg-white transition-transform {cacheConfig.wifiOnly
|
||||
? 'translate-x-7'
|
||||
: 'translate-x-1'}"
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user