From 7e291cfcccbba4957efa6b7a961947e21ea84dff Mon Sep 17 00:00:00 2001 From: KeelrLeek <156356974+KeelrLeek@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:39:19 -0500 Subject: [PATCH] settings: one long page of section cards behind a sticky index, segments and switches for the choices, and the Networks card moved in from Community --- src/views/Community.svelte | 114 +-- src/views/Settings.svelte | 1333 +++++++++++++++++------------------- 2 files changed, 632 insertions(+), 815 deletions(-) diff --git a/src/views/Community.svelte b/src/views/Community.svelte index dc8d3a6..ed962f8 100644 --- a/src/views/Community.svelte +++ b/src/views/Community.svelte @@ -434,7 +434,6 @@ let exportDone = $state(null); let confirmRotate = $state(false); let confirmRemove = $state(false); - let disclosuresOpen = $state(false); async function doExport() { const r = await exportPersona(passExport); if (r) { @@ -1473,7 +1472,7 @@ {#if community.busy["holdings:share"]}working…{:else if sv.share_holdings}sharing{:else}not sharing{/if} -
What the room learns: members can tell whether you hold any album they can name, and roughly how many you hold. They never see files, paths, or formats beyond a lossless/lossy word. Turning this off withdraws the list; copies members already read stay with them.
+
What the room learns: members can tell whether you hold any album they can name, and roughly how many you hold. They never see files, paths, or formats beyond a lossless/lossy word. Turning this off withdraws the list; copies members already read stay with them.

Ask this room{#if hv}{hv.sharing} of {sv.members.length} sharing{/if}

{ e.preventDefault(); if (askArtist.trim() && askAlbum.trim()) asked = { artist: askArtist.trim(), album: askAlbum.trim() }; }}> @@ -1809,98 +1808,7 @@

Networks

-
-
-
-
Soulseek {online ? "online" : "offline"}
-
- {#if me}Signed in as {me} · {fmtCount(network.shareFiles)} files shared.{:else}Not signed in — see Settings.{/if} -
- {#if !key} - Create a SoulStream identity to link this account to it. - {:else if !me} - Sign in to Soulseek to link this account to your identity. - {:else if ownLinkCurrent} - Linked to this identity — proof verified {new Date(ownLink.verified_at * 1000).toLocaleString()} through a Soulseek message to yourself. Other SoulStream users who ask over Soulseek now get your signed ID; nothing is published. - {:else} - Not linked to your identity. Linking sends one message to yourself over Soulseek and verifies the signed answer — nothing is published, and from then on other SoulStream users who ask get your signed ID. - {/if} -
- {#if key && me} -
- {#if ownLinkCurrent} - - {:else} - - {#if !nodeUp}needs the node connected{/if} - {/if} -
- {/if} -
-
-
-
-
-
Freenet experimental
-
- {#if freenetOn} - Local node on 127.0.0.1:{community.freenet?.ws_port ?? 7509} · {status.text} - - {#if statusKind(community.freenet?.status) === "node-absent"} -
- {#if community.nodeInfo?.binary} - - runs the installed peer{#if community.nodeInfo.version} ({community.nodeInfo.version}){/if} in the background · telemetry pinned off · data in the app folder - {:else} - No Freenet peer found on this machine — install it (cargo install freenet) and check again. - {/if} -
- {:else if community.nodeInfo?.started_by_app} -
- - started by SoulStream · keeps running when the app closes; this button stops it -
- {/if} - {:else} - Off. Carries your identity and profile; later playlists, friend links and Spaces. Turning it on publishes nothing. Turning it off is instant and touches nothing else in the app. - {/if} -
-
- Before you rely on it — seven things that are true -
    -
  • A desktop background peer must run; it uses network, disk, memory and CPU.
  • -
  • The network is alpha. Things you publish are replicated and cannot be guaranteed erased.
  • -
  • Freenet is not an anonymity service.
  • -
  • The node's own diagnostics are a node setting SoulStream pins off; verify after upgrades.
  • -
  • Nothing is published when you enable this. Publishing is always a button.
  • -
  • Nothing here ever says "online" — a successful fetch is not presence.
  • -
  • Phones and TVs cannot run the same peer today.
  • -
-
- {#if freenetOn} - - {#if key} -
- {#if community.inbox?.published} - encrypted inbox · key {community.inbox.box_key_id} - - {community.inbox.last_check ? `checked ${fmtWhen(community.inbox.last_check * 1000)} · ` : ""}friend requests and shares arrive sealed to a key kept on this device; a persona restored elsewhere publishes a new one - {:else} - - lets other SoulStream users send you friend requests and shares sealed, without Soulseek in between - {/if} -
- {/if} - {/if} -
- -
+

The Soulseek link, the Freenet node, remote images and the encrypted inbox live in now — every switch in one place.

Devices later

@@ -2453,8 +2361,8 @@ .mem .role { font-size: 9.5px; font-weight: 900; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted); } .mem .role.own { color: #d9a6ff; } /* holdings: the disclosure and the who-shares list */ - .disc { display: flex; gap: 8px; align-items: flex-start; padding: 9px 11px; border-radius: 9px; background: color-mix(in srgb, var(--warn) 7%, transparent); border: 1px solid color-mix(in srgb, var(--warn) 25%, transparent); font-size: 11.5px; color: var(--text-secondary); line-height: 1.5; } - .disc b { color: #f0c070; } + .holddisc { display: flex; gap: 8px; align-items: flex-start; padding: 9px 11px; border-radius: 9px; background: color-mix(in srgb, var(--warn) 7%, transparent); border: 1px solid color-mix(in srgb, var(--warn) 25%, transparent); font-size: 11.5px; color: var(--text-secondary); line-height: 1.5; } + .holddisc b { color: #f0c070; } .askinl input { height: 30px; padding: 0 10px; border-radius: 8px; background: var(--surf-hi); border: none; color: var(--text-primary); font-size: 12px; outline: none; } .askinl input::placeholder { color: var(--text-muted); } .hlist { display: flex; flex-direction: column; gap: 2px; margin-top: 8px; } @@ -2522,25 +2430,11 @@ .kv dt { color: var(--text-muted); font-weight: 700; } .kv dd { margin: 0; color: var(--text-secondary); overflow-wrap: anywhere; } .kv dd b { color: var(--text-primary); } - .netcard { display: flex; gap: 12px; align-items: flex-start; margin-top: 12px; } - .nico { width: 36px; height: 36px; border-radius: 10px; display: grid; place-items: center; flex: none; } - .nico.slsk { background: rgba(255, 255, 255, 0.08); color: #fff; } - .nico.fn { background: var(--accent-soft); color: var(--accent-hover); } - .ntx { min-width: 0; flex: 1; } - .netcard .t { font-size: 13.5px; font-weight: 800; display: flex; align-items: center; gap: 8px; } - .netcard .s { font-size: 11.5px; color: var(--text-muted); margin-top: 3px; line-height: 1.5; } - .netcard .s b { color: var(--text-secondary); font-weight: 700; } .tog { display: inline-block; width: 36px; height: 20px; border-radius: 10px; background: rgba(255, 255, 255, 0.16); position: relative; flex: none; transition: background 0.15s; } .tog::after { content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%; background: #fff; transition: left 0.15s; } .tog.on { background: var(--accent); } .tog.on::after { left: 18px; } .tog:disabled { opacity: 0.5; cursor: default; } - .tog.big { margin-top: 6px; } - .swrow { display: flex; align-items: center; gap: 10px; margin-top: 12px; font-size: 12px; color: var(--text-secondary); } - .swrow .hint.r { margin-left: auto; } - .disc { margin-top: 10px; } - .disc summary { font-size: 11.5px; font-weight: 800; color: var(--text-secondary); cursor: pointer; } - .disc ul { margin: 8px 0 0; padding-left: 18px; font-size: 11.5px; color: var(--text-muted); line-height: 1.6; } .danger { display: flex; gap: 8px; align-items: center; margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--hair); flex-wrap: wrap; } /* ── the person drawer (portaled to body — see portal()) ── */ diff --git a/src/views/Settings.svelte b/src/views/Settings.svelte index 69d8129..758b504 100644 --- a/src/views/Settings.svelte +++ b/src/views/Settings.svelte @@ -1,9 +1,36 @@ -
-

Settings

+{#snippet sw(on, words, onchange, disabled = false)} + {on ? words[0] : words[1]} + +{/snippet} +{#snippet seg(options, value, onpick, disabled = false)} + + {#each options as [v, label] (v)} + + {/each} + +{/snippet} + +
{#if !settings}

Loading…

{:else} -
-

Account

-
-
-
Soulseek account
-
- {settings.username - ? `Signed in as ${settings.username}` - : "Not signed in"} - {#if settings.connected} · online{/if} -
-
-
- {#if app.connection !== "online"} - - {/if} - {#if settings.has_password} - - {/if} -
-
-

- - Your password is stored in the Windows Credential Manager, not in a plain file. -

-
- -
-

Appearance

-
-
-
Dark glass
-
Lets whatever is behind the window glow through the app's dark surfaces with a live blur (Windows acrylic). Cards, rows and highlights thin out with it; menus and dialogs stay solid so nothing gets hard to read.
-
- -
-
- -
-

Playback

-
-
-
Audio output
-
System default follows Windows when headphones or speakers change.
-
- -
-
-
-
Normalize volume
-
Plays every track at roughly the same loudness (−14 LUFS, like Spotify), so quiet and loud masters stop needing the volume knob. A track is measured while it first streams and matched exactly from then on; until that finishes SoulStream will turn it down but never up, so a quiet intro can't make the chorus too loud.
+ {/each} +
+ {#if saving}Saving…{:else if savedFlash} Saved{:else if saveError}{saveError}{/if}
- -
-

Changes apply immediately and keep the current track near the same position.

-
+ -
-

Lyrics

-
-
-
Word-by-word fill Experimental
-
- Fills each word as it is sung, karaoke-style. Still experimental: timing - can drift on dense or echo-heavy songs. Off keeps the reliable - line-by-line sync everywhere and pauses the background word analysis. -
-
- -
- {#if settings.word_timing} -
-
-
Precise word timing
-
- Downloads an alignment model (MMS, ~1.26 GB, from Hugging Face) that - listens to each lyric line and times every word against the actual singing, - in most languages. Runs entirely on this computer, in the background, never - against the playing track. Without it, word timing stays a lighter estimate. - Already-timed songs re-prepare automatically the next time you open their - lyrics. -
-
- {#if lyricModel?.present} - Downloaded - {:else} - - {/if} -
- {#if modelError} -

Download failed: {modelError}

- {/if} - {#if lyricModel?.present} -
-
-
Vocal isolation
-
- Downloads a small separation model (UVR MDX-Net Voc FT, ~64 MB, from - the Ultimate Vocal Remover project) that lifts the singing away from - the band before words are timed — noticeably steadier timing on dense - mixes. Same rules as above: runs on this computer, in the background, - never against the playing track. Timed songs re-prepare the next time - you open their lyrics. +
+ +
+

Account

Your Soulseek login and where the password lives.
+
+
+
+
Soulseek account {#if settings.connected || online}online{:else if settings.username}offline{/if}
+
{settings.username ? `Signed in as ${settings.username}.` : "Not signed in."} Your password is stored in the Windows Credential Manager, not in a plain file.
+
+
+ {#if app.connection !== "online"} + + {/if} + {#if settings.has_password} + + {/if}
- {#if sepModel?.present} - Downloaded - {:else} - +
+
+ + +
+

Networks

The two networks this app speaks, and whether they are tied together.
+
+
+
+
Soulseek {#if online}online{:else}offline{/if}{#if ownLinkCurrent}linked{/if}
+
+ {#if me}Signed in as {me} · {fmtCount(network.shareFiles)} files shared.{:else}Not signed in.{/if} + {#if !key} + Create a SoulStream identity on Community → You to link this account to it. + {:else if !me} + Sign in to Soulseek to link this account to your identity. + {:else if ownLinkCurrent} + Linked to your SoulStream identity — proof verified {new Date(ownLink.verified_at * 1000).toLocaleString()} through a message to yourself. Other SoulStream users who ask get a signed answer. + {:else} + Not linked to your identity. Linking sends one message to yourself over Soulseek and verifies the signed answer — nothing is published. + {/if} +
+
+
+ {#if key && me} + {#if ownLinkCurrent} + + {:else} + + {/if} + {/if} +
+
+
+
+
Freenet experimental
+
+ {#if freenetOn} + Local node on 127.0.0.1:{community.freenet?.ws_port ?? 7509} · {status.text} + + {:else} + Off. Carries your identity, profile, playlists, friend links, Spaces and holdings. Turning it on publishes nothing; turning it off is instant and touches nothing else in the app. + {/if} +
+ {#if freenetOn && statusKind(community.freenet?.status) === "node-absent"} +
+ {#if community.nodeInfo?.binary} + + runs the installed peer{#if community.nodeInfo.version} ({community.nodeInfo.version}){/if} in the background · telemetry pinned off · data in the app folder + {:else} + No Freenet peer found on this machine — install it (cargo install freenet) and check again. + {/if} +
+ {:else if freenetOn && community.nodeInfo?.started_by_app} +
+ + started by SoulStream · keeps running when the app closes; this button stops it +
+ {/if} +
+ Before you rely on it — seven things that are true +
    +
  • A desktop background peer must run; it uses network, disk, memory and CPU.
  • +
  • The network is alpha. Things you publish are replicated and cannot be guaranteed erased.
  • +
  • Freenet is not an anonymity service.
  • +
  • The node's own diagnostics are a node setting SoulStream pins off; verify after upgrades.
  • +
  • Nothing is published when you enable this. Publishing is always a button.
  • +
  • Nothing here ever says "online" — a successful fetch is not presence.
  • +
  • Phones and TVs cannot run the same peer today.
  • +
+
+
+
{@render sw(freenetOn, ["on", "off"], (v) => setFreenetEnabled(v), !!community.busy.freenet)}
+
+ {#if freenetOn} +
+
+
Load remote profile images
+
Other people's avatars, fetched from the network — verified, resized, cached by hash. Off shows their identicon only.
+
+
{@render sw(community.freenet?.load_remote_images ?? true, ["on", "off"], (v) => setLoadRemoteImages(v), !!community.busy.images)}
+
+ {#if key} +
+
+
Encrypted inbox {#if community.inbox?.published} key {community.inbox.box_key_id}{/if}
+
{#if community.inbox?.published}Friend requests and shares arrive sealed to a key kept on this device{#if community.inbox.last_check} · checked {fmtWhen(community.inbox.last_check * 1000)}{/if}.{:else}Lets other SoulStream users send you friend requests and shares sealed, without Soulseek in between.{/if}
+
+
+ {#if community.inbox?.published} + + {:else} + + {/if} +
+
+ {/if} + {/if} + {#if community.error}
{community.error}
{/if} +
+
+ + +
+

Appearance

How the window is drawn.
+
+
+
+
Dark glass
+
Lets whatever is behind the window glow through the app's dark surfaces with a live blur (Windows acrylic). Cards, rows and highlights thin out with it; menus and dialogs stay solid so nothing gets hard to read.
+
+
{@render sw(!!settings.glass_mode, ["dark glass", "solid"], (v) => setGlassMode(v), saving)}
+
+
+
+ + +
+

Playback

Output and loudness.
+
+
+
+
Audio output
+
System default follows Windows when headphones or speakers change.
+
+
+ +
+
+
+
+
Normalize volume
+
Plays every track at roughly the same loudness (−14 LUFS, like Spotify), so quiet and loud masters stop needing the volume knob. A track is measured while it first streams and matched exactly from then on; until that finishes SoulStream will turn it down but never up, so a quiet intro can't make the chorus too loud.
+
+
{@render sw(!!settings.volume_normalization, ["on", "off"], (v) => setVolumeNormalization(v), saving)}
+
+
Changes apply immediately and keep the current track near the same position.
+
+
+ + +
+

Lyrics

Line sync is always on; word fill is the experiment.
+
+
+
+
Word-by-word fill experimental
+
Fills each word as it is sung, karaoke-style. Still experimental: timing can drift on dense or echo-heavy songs. Off keeps the reliable line-by-line sync everywhere and pauses the background word analysis.
+
+
{@render sw(!!settings.word_timing, ["on", "off"], (v) => setWordTiming(v), saving)}
+
+ {#if settings.word_timing} +
+
+
Precise word timing
+
Downloads an alignment model (MMS, ~1.26 GB, from Hugging Face) that listens to each lyric line and times every word against the actual singing, in most languages. Runs entirely on this computer, in the background, never against the playing track. Without it, word timing stays a lighter estimate. Already-timed songs re-prepare automatically the next time you open their lyrics.
+ {#if modelError}
Download failed: {modelError}
{/if} +
+
+ {#if lyricModel?.present} + Downloaded + {:else} + + {/if} +
+
+ {#if lyricModel?.present} +
+
+
Vocal isolation
+
Downloads a small separation model (UVR MDX-Net Voc FT, ~64 MB, from the Ultimate Vocal Remover project) that lifts the singing away from the band before words are timed — noticeably steadier timing on dense mixes. Same rules as above: runs on this computer, in the background, never against the playing track.
+ {#if sepError}
Download failed: {sepError}
{/if} +
+
+ {#if sepModel?.present} + Downloaded + {:else} + + {/if} +
+
+ {/if} {/if}
- {#if sepError} -

Download failed: {sepError}

- {/if} - {/if} - {/if} -
+
-
-

Streaming

-
-
-
Keep streamed tracks
-
Streamed songs stay in a private cache for instant replay — not in your library. Choose Forever to add everything you stream to the library instead.
-
- -
-
-
-
Cache size limit
-
Oldest streamed tracks are removed first once the cache passes this size. Saved and downloaded music is never touched.
-
- -
-

Retention counts from the last time a track was played. Tracks you save stay in your library regardless.

-
- -
-

Library & downloads

-
-
-
Library folder
-
{settings.download_dir}
-
- -
-

This is your downloaded-music library and is always shared. Folder changes apply after reconnecting.

-
-
-
Album folder format
-
Use {"{artist}"}, {"{album}"}, {"{date}"}, or {"{year}"}; "\" starts a subfolder. Full dates display as DD-MM-YYYY.
-
- e.key === "Enter" && e.currentTarget.blur()} - onchange={(e) => setLibraryLayout(e.currentTarget.value)} - /> -
-

New downloads are organized into this format; existing files are not moved.

-
- -
-

Sharing

-

- Sharing keeps you a good citizen — leechers get banned. Your download - folder is always shared ({settings.share_files} files indexed). - Add extra folders below. -

- {#if settings.shared_folders.length > 0} -
- {#each settings.shared_folders as folder (folder)} -
- {folder} - + +
+

Streaming

What happens to a track after you streamed it.
+
+
+
+
Keep streamed tracks
+
Streamed songs stay in a private cache for instant replay — not in your library. Forever adds everything you stream to the library instead.
+
+
+ +
- {/each} -
- {/if} - -
- -
-

Album artwork

-

Covers are resolved from Deezer, Apple, and the Cover Art Archive; the artwork shown in the catalog wins, at the highest resolution any source serves. One canonical image is used for the library, the embedded tags, and the album folder.

-
-
-
Art inside downloaded files
-
Replace whatever picture the uploader embedded with the canonical cover, or keep theirs when present.
-
- -
-
-
-
Embedded cover size
-
Longest edge of the copy written into each audio file. The full-resolution original is always kept in the album folder and art cache.
-
- -
-
-
-
Album folder cover
-
Write the canonical cover as cover.jpg and folder.jpg so Explorer and other players show the same artwork.
-
- -
-
-
-
Re-fetch all album covers
-
Upgrade every library album to the canonical high-resolution cover — cache, embedded tags, and folder files. Roughly one album per second.
-
- -
- {#if albumRefreshResult}

{albumRefreshResult}

{/if} -
- -
-

Artist artwork

-
-
-
Hero renderer
-
Live GPU creates one native-4K master, then mip-filters it cleanly for every window size.
-
- -
-

Live GPU applies crop ? EASU ? RCAS ? color/fade/grain once at 3840?2160. Resizing only re-samples that fixed master, preserving grain scale and avoiding baked-file latency.

-
-
-
Refresh artist portraits
-
Fetch portraits for artists already in your library.
-
- -
- {#if artistRefreshResult}

{artistRefreshResult}

{/if} -
- -
-

Source quality

-

- Independent preferences for streaming and downloading. The ceiling and floor - bound which peer files qualify; priority decides whether the resolver picks - the best format or the fastest transfer among them. -

- {#each [ - { key: "stream", label: "Streaming" }, - { key: "download", label: "Downloads" }, - ] as mode (mode.key)} -
-
-
{mode.label} — maximum
-
Skip files above this ceiling (lossy-only avoids huge lossless transfers).
+
+
+
Cache size limit
+
Oldest streamed tracks are removed first once the cache passes this size. Saved and downloaded music is never touched.
+
+
+ +
+
+
Retention counts from the last time a track was played. Tracks you save stay in your library regardless.
- -
-
-
-
{mode.label} — minimum
-
Reject files below this floor. Lossy files must report a bitrate to qualify.
-
- -
-
-
-
{mode.label} — priority
-
- Fastest transfer estimates arrival time from file size ÷ peer speed - (plus queue wait) and picks whatever lands soonest — a large FLAC from a - fast peer can win. Only files inside your min/max window compete. +
+ + +
+

Library & downloads

Where music lands and how it is filed.
+
+
+
+
Library folder
+
{settings.download_dir}
+
Your downloaded-music library, always shared. Folder changes apply after reconnecting.
+
+
+
+
+
+
Album folder format
+
Use {"{artist}"}, {"{album}"}, {"{date}"} or {"{year}"}; "\" starts a subfolder. Full dates display as DD-MM-YYYY. New downloads are organized into this format; existing files are not moved.
+
+
+ e.key === "Enter" && e.currentTarget.blur()} onchange={(e) => setLibraryLayout(e.currentTarget.value)} /> +
- -
- {/each} -
+ -
-

Uploads

-
-
-
Upload slots
-
How many peers can download from you at once.
-
-
- - {settings.upload_slots} - -
+ +
+

Sharing

Sharing keeps you a good citizen — leechers get banned.
+
+
+
+
Shared folders
+
Your library folder is always shared ({fmtCount(settings.share_files)} files indexed). Add extra folders here.
+
+
+
+ {#if settings.shared_folders.length > 0} +
+ {#each settings.shared_folders as folder (folder)} +
{folder}
+ {/each} +
+ {/if} +
+
+
Upload slots
+
How many peers can download from you at once.
+
+
+ + + {settings.upload_slots} + + +
+
+
+
+ + +
+

Source quality

Independent preferences for streaming and downloading.
+
+
The ceiling and floor bound which peer files qualify; priority decides whether the resolver picks the best format or the fastest transfer among them. Fastest estimates arrival from file size ÷ peer speed plus queue wait — a large FLAC from a fast peer can win.
+ {#each [{ key: "stream", label: "Streaming" }, { key: "download", label: "Downloads" }] as mode (mode.key)} +
{mode.label}
+
+
+
Maximum
+
Skip files above this ceiling — lossy-only avoids huge lossless transfers.
+
+
{@render seg([["any", "No limit"], ["lossy_only", "Lossy only"]], sourcePrefsFor(mode.key).max, (v) => setSourcePref(mode.key, "max", v), saving)}
+
+
+
+
Minimum
+
Reject files below this floor. Lossy files must report a bitrate to qualify.
+
+
+ +
+
+
+
+
Priority
+
Only files inside the window above compete.
+
+
{@render seg([["quality", "Best quality"], ["speed", "Fastest transfer"]], sourcePrefsFor(mode.key).priority, (v) => setSourcePref(mode.key, "priority", v), saving)}
+
+ {/each} +
+
+ + +
+

Album artwork

Covers come from Deezer, Apple and the Cover Art Archive; the catalog's wins, at the highest resolution any source serves.
+
+
+
+
Art inside downloaded files
+
Replace whatever picture the uploader embedded with the canonical cover, or keep theirs when present.
+
+
{@render seg([["replace", "Replace with canonical"], ["keep", "Keep uploader's art"]], settings.replace_embedded_art ? "replace" : "keep", (v) => setReplaceEmbeddedArt(v), saving)}
+
+
+
+
Embedded cover size
+
Longest edge of the copy written into each audio file. The full-resolution original is always kept in the album folder and art cache.
+
+
{@render seg([["0", "Original"], ["1500", "1500 px"], ["1000", "1000 px"]], String(settings.embed_art_max_edge), (v) => setEmbedArtMaxEdge(v), saving)}
+
+
+
+
Album folder cover
+
Write the canonical cover as cover.jpg and folder.jpg so Explorer and other players show the same artwork.
+
+
{@render sw(!!settings.write_folder_cover, ["write", "leave"], (v) => setWriteFolderCover(v ? "write" : "skip"), saving)}
+
+
+
+
Re-fetch all album covers
+
Upgrade every library album to the canonical high-resolution cover — cache, embedded tags and folder files. Roughly one album per second.
+ {#if albumRefreshResult}
{albumRefreshResult}
{/if} +
+
+
+
+
+ + +
+

Artist artwork

Portraits and the hero renderer.
+
+
+
+
Hero renderer
+
Live GPU creates one native-4K master (crop → EASU → RCAS → color/fade/grain once at 3840×2160), then mip-filters it cleanly for every window size, preserving grain scale.
+
+
{@render seg([["live_gpu", "Live GPU 4K"], ["fsr_legacy", "Baked FSR 1"], ["lanczos3", "Baked Lanczos3"]], settings.artist_hero_upscaler ?? "live_gpu", (v) => setArtistHeroUpscaler(v), saving)}
+
+
+
+
Refresh artist portraits
+
Fetch portraits for artists already in your library.
+ {#if artistRefreshResult}
{artistRefreshResult}
{/if} +
+
+
+
+
-
- - {#if saving} -

Saving…

- {:else if savedFlash} -

✓ Saved

- {:else if saveError} -

{saveError}

- {/if} +
{/if}