android: the repair sheet, browse, every long-press sheet, You's last sections, the lyrics tools — and the walk's audio check made honest
The playlist repair (Find a local match: the library searched by the row's name, a pick relinks it through playlist_relink_item; an unknown row takes the track's identity). A peer's sheet on the raw results (play or save the folder, browse their files, message, buddy) and the Browse screen — the hub's Browse tab: a user's whole share with sizes and totals, a folder played or saved whole; a share can hold thousands of folders, so it filters and renders 80 at a time. "Look up a user" on Community → Buddies. The sheets the phone lacked: a queue row's (Play now, Move to next, Remove from queue), a catalog song's, a catalog card's and the record page's own, a raw file's (in the results and in Browse), a room name's, a buddy's, the now-playing Save (the desktop's Save menu for a streamed track or record). The artwork viewer with pinch zoom, one layer the back gesture peels. Search's recent searches and its spelling suggestion from the names this phone knows. You → Streaming, Downloads, Sharing, Lyrics, Album artwork, and Clear play history; a record's Re-fetch the cover. The lyrics tools on the sheet's face: the offset nudged and kept per track, another LRCLIB candidate chosen, the lookup forgotten and done again (lyrics.svelte.js, shared by the face and the lyrics sheet). The harness: every CDP call times out and the reload step re-attaches (a WebView that stopped answering hung a walk for ten minutes); the audio check counts SoulStream's own tracks' underruns from the mixer dump instead of the thread-wide counters that any app's notification moves — the afternoon's "dropouts" were other apps' sounds, and the browse of a 49,544-file share measured at zero; __settle resets every tab to its root through the nav; an adb step kind; the walks cover all of the above on the mock and on the phone.
This commit is contained in:
1 parent
6e39dc43f3
commit
26928b706d
21 files changed
+1294
-92
No files matched your search
@@ -3,8 +3,9 @@
|
||||
running. Playback, storage and the network settings come next. */
|
||||
import Icon from "../../components/Icon.svelte";
|
||||
import * as api from "../../lib/api.js";
|
||||
import { lib, loadLibrary } from "../lib.svelte.js";
|
||||
import { push } from "../nav.svelte.js";
|
||||
import { lib, loadLibrary, refreshRecents } from "../lib.svelte.js";
|
||||
import { push, openHalf } from "../nav.svelte.js";
|
||||
import { note } from "../notice.svelte.js";
|
||||
import { transfers, active } from "../transfers.svelte.js";
|
||||
import { player } from "../player.svelte.js";
|
||||
import { settings, setSetting } from "../prefs.svelte.js";
|
||||
@@ -47,6 +48,16 @@
|
||||
if (!/cancelled/.test(String(e))) player.error = String(e);
|
||||
} finally { picking = false; }
|
||||
}
|
||||
function clearHistory() {
|
||||
openHalf("confirm", {
|
||||
title: "Clear the play history?",
|
||||
body: "Jump back in and the play counts start over. The library and the playlists stay.",
|
||||
ok: "Clear", danger: true,
|
||||
onconfirm: async () => {
|
||||
try { await api.clearPlayHistory(); await refreshRecents(); note("Play history cleared"); } catch (e) { player.error = String(e); }
|
||||
},
|
||||
});
|
||||
}
|
||||
async function rescan() {
|
||||
if (scanning) return;
|
||||
scanning = true;
|
||||
@@ -61,6 +72,22 @@
|
||||
const MIN_Q = [["any", "Any quality"], ["kbps192", "192 kbps and up"], ["kbps256", "256 kbps and up"], ["kbps320", "320 kbps and up"], ["lossless", "Lossless only"]];
|
||||
const PRI = [["quality", "Best quality"], ["speed", "Fastest transfer"]];
|
||||
const PREF_DEFAULTS = { max: "any", min: "any", priority: "quality" };
|
||||
/* the desktop's Streaming settings: what happens to a streamed track */
|
||||
const RETENTION = [["1", "1 day"], ["3", "3 days"], ["7", "1 week"], ["14", "2 weeks"], ["30", "1 month"], ["90", "3 months"], ["forever", "Forever — save to library"]];
|
||||
const CACHE = [["512", "512 MB"], ["1024", "1 GB"], ["2048", "2 GB"], ["5120", "5 GB"], ["10240", "10 GB"], ["20480", "20 GB"], ["0", "Unlimited"]];
|
||||
const retention = $derived(!cfg ? "30" : cfg.stream_keep_forever ? "forever" : String(cfg.stream_retention_days ?? 30));
|
||||
const cacheMax = $derived(!cfg ? "2048" : String(cfg.stream_cache_max_mb ?? 2048));
|
||||
async function setRetention(v) {
|
||||
if (v === retention) return;
|
||||
try {
|
||||
await api.setSettings(v === "forever" ? { streamKeepForever: true } : { streamKeepForever: false, streamRetentionDays: Number(v) });
|
||||
cfg = await api.getSettings();
|
||||
} catch (e) { player.error = String(e); }
|
||||
}
|
||||
async function setCacheMax(v) {
|
||||
if (v === cacheMax) return;
|
||||
try { await api.setSettings({ streamCacheMaxMb: Number(v) }); cfg = await api.getSettings(); } catch (e) { player.error = String(e); }
|
||||
}
|
||||
const prefsOf = (mode) => ({ ...PREF_DEFAULTS, ...((mode === "stream" ? cfg?.stream_source_prefs : cfg?.download_source_prefs) ?? {}) });
|
||||
async function setPref(mode, field, value) {
|
||||
if (prefsOf(mode)[field] === value) return;
|
||||
@@ -72,6 +99,43 @@
|
||||
} catch (e) { player.error = String(e); }
|
||||
}
|
||||
|
||||
/* the desktop's Library & downloads / Sharing / Lyrics / Album artwork
|
||||
settings, on the phone's components; every save camelCase */
|
||||
const ART_MODE = [["replace", "Canonical cover"], ["keep", "Uploader's art"]];
|
||||
const ART_EDGE = [["0", "Original"], ["1500", "1500 px"], ["1000", "1000 px"]];
|
||||
const artMode = $derived(cfg?.replace_embedded_art === false ? "keep" : "replace");
|
||||
const artEdge = $derived(String(cfg?.embed_art_max_edge ?? 1500));
|
||||
let refetching = $state(false);
|
||||
async function saveCfg(patch) {
|
||||
try { await api.setSettings(patch); cfg = await api.getSettings(); } catch (e) { player.error = String(e); }
|
||||
}
|
||||
const setFlag = (key, v) => saveCfg({ [key]: !!v });
|
||||
const setNumber = (key, v) => saveCfg({ [key]: v });
|
||||
const setSlots = (n) => saveCfg({ uploadSlots: Math.max(1, Math.min(10, n)) });
|
||||
function layoutPrompt() {
|
||||
openHalf("prompt", {
|
||||
title: "Album folder format", value: cfg?.library_layout || "", placeholder: "{artist}\\{album} ({year})", ok: "Save",
|
||||
onsubmit: (v) => { if (v.trim()) void saveCfg({ libraryLayout: v.trim() }); },
|
||||
});
|
||||
}
|
||||
async function addShared() {
|
||||
try {
|
||||
const r = await api.pickLibraryFolder();
|
||||
if (!r?.path) { player.error = "That folder has no path the phone can share — pick one on this phone's storage."; return; }
|
||||
const have = cfg?.shared_folders ?? [];
|
||||
if (!have.includes(r.path)) await saveCfg({ sharedFolders: [...have, r.path] });
|
||||
} catch (e) {
|
||||
if (!/cancelled/.test(String(e))) player.error = String(e);
|
||||
}
|
||||
}
|
||||
const removeShared = (folder) => saveCfg({ sharedFolders: (cfg?.shared_folders ?? []).filter((f) => f !== folder) });
|
||||
async function refetchCovers() {
|
||||
if (refetching) return;
|
||||
refetching = true;
|
||||
try { const n = await api.refreshAllAlbumArt(); note(`${n ?? ""} cover${n === 1 ? "" : "s"} re-fetched`.trim()); }
|
||||
catch (e) { player.error = String(e); }
|
||||
finally { refetching = false; }
|
||||
}
|
||||
const ROWS = [
|
||||
["ambience", "image", "Ambience", "the artwork in play, blurred, under every screen"],
|
||||
["accent", "sparkle", "Accent from artwork", "the app takes the record's colour"],
|
||||
@@ -122,6 +186,10 @@
|
||||
<span class="lead"><Icon name="refresh" size={20} /></span>
|
||||
<span class="b"><span class="t">{scanning ? "Scanning…" : "Rescan library"}</span><span class="m">looks for new files in the folder, forgets the ones that are gone</span></span>
|
||||
</button>
|
||||
<button class="row" onclick={clearHistory}>
|
||||
<span class="lead"><Icon name="history" size={20} /></span>
|
||||
<span class="b"><span class="t">Clear play history</span><span class="m">what Jump back in and the play counts are built from</span></span>
|
||||
</button>
|
||||
<button class="row" onclick={() => push("transfers", {})}>
|
||||
<span class="lead"><Icon name="download" size={20} /></span>
|
||||
<span class="b"><span class="t">Transfers</span><span class="m">{active().length ? `${active().length} in flight` : Object.keys(transfers.items).length ? "nothing in flight" : "what Save is fetching into the library"}</span></span>
|
||||
@@ -129,6 +197,23 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="sheet-sec">Streaming</div>
|
||||
<div class="sec-note">What happens to a track after you streamed it. Streamed songs stay in a private cache for instant replay — not in your library; Forever adds everything you stream to the library instead. Retention counts from the last play; saved and downloaded music is never touched.</div>
|
||||
<div class="group prefs" data-mode="streamcache">
|
||||
<div class="pref">
|
||||
<div class="pref-l"><span class="t">Keep streamed tracks</span><span class="m">how long the cache holds a track after its last play</span></div>
|
||||
<div class="chips">
|
||||
{#each RETENTION as [v, l] (v)}<button class="chip" class:on={retention === v} class:quiet={retention !== v} onclick={() => setRetention(v)}>{l}</button>{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pref">
|
||||
<div class="pref-l"><span class="t">Cache size limit</span><span class="m">the oldest streamed tracks go first past this size</span></div>
|
||||
<div class="chips">
|
||||
{#each CACHE as [v, l] (v)}<button class="chip" class:on={cacheMax === v} class:quiet={cacheMax !== v} onclick={() => setCacheMax(v)}>{l}</button>{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sheet-sec">Sources</div>
|
||||
<div class="sec-note">Independent for streaming and downloads. The ceiling and floor bound which peer files qualify; priority decides whether the resolver takes the best format or the fastest transfer among them.</div>
|
||||
{#each MODES as [key, label] (key)}
|
||||
@@ -155,15 +240,77 @@
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<div class="sheet-sec">Downloads</div>
|
||||
<div class="group">
|
||||
<button class="row" onclick={layoutPrompt}>
|
||||
<span class="lead"><Icon name="folder-open" size={20} /></span>
|
||||
<span class="b"><span class="t">Album folder format</span><span class="m mono">{cfg?.library_layout || "{artist}\\{album}"}</span></span>
|
||||
<span class="r"><Icon name="chevron-right" size={17} /></span>
|
||||
</button>
|
||||
<div class="sec-note" style="padding: 0 14px 12px">Use {"{artist}"}, {"{album}"}, {"{date}"} or {"{year}"}; "\" starts a subfolder. New downloads are organized into this format; existing files are not moved.</div>
|
||||
</div>
|
||||
|
||||
<div class="sheet-sec">Sharing</div>
|
||||
<div class="group">
|
||||
<div class="kv"><span class="k">Your library folder</span><span class="v dim">always shared · {(cfg?.share_files ?? 0).toLocaleString()} files</span></div>
|
||||
<div class="row slots">
|
||||
<span class="lead"><Icon name="users" size={20} /></span>
|
||||
<span class="b"><span class="t">Upload slots</span><span class="m">how many peers can download from you at once</span></span>
|
||||
<span class="stepper"><button class="iconbtn sm" onclick={() => setSlots((cfg?.upload_slots ?? 2) - 1)} aria-label="Fewer slots"><Icon name="minus" size={16} /></button><span class="n">{cfg?.upload_slots ?? 2}</span><button class="iconbtn sm" onclick={() => setSlots((cfg?.upload_slots ?? 2) + 1)} aria-label="More slots"><Icon name="plus" size={16} /></button></span>
|
||||
</div>
|
||||
{#each cfg?.shared_folders ?? [] as folder (folder)}
|
||||
<div class="row">
|
||||
<span class="lead"><Icon name="folder" size={20} /></span>
|
||||
<span class="b"><span class="t mono">{folder}</span><span class="m">also shared</span></span>
|
||||
<button class="iconbtn sm muted" onclick={() => removeShared(folder)} aria-label="Stop sharing"><Icon name="x" size={18} /></button>
|
||||
</div>
|
||||
{/each}
|
||||
<button class="row" onclick={addShared}>
|
||||
<span class="lead"><Icon name="plus" size={20} /></span>
|
||||
<span class="b"><span class="t">Add a shared folder</span><span class="m">on this phone's storage; the library folder needs no adding</span></span>
|
||||
<span class="r"><Icon name="chevron-right" size={17} /></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="sheet-sec">Lyrics</div>
|
||||
<div class="group">
|
||||
<button class="row" onclick={() => setFlag("wordTiming", !cfg?.word_timing)}>
|
||||
<span class="lead"><Icon name="lyrics" size={20} /></span>
|
||||
<span class="b"><span class="t">Word-by-word fill</span><span class="m">each word fills as it is sung; off keeps the line-by-line sync everywhere</span></span>
|
||||
<span class="sw" class:on={!!cfg?.word_timing}><span></span></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="sheet-sec">Album artwork</div>
|
||||
<div class="group prefs" data-mode="albumart">
|
||||
<div class="pref">
|
||||
<div class="pref-l"><span class="t">Art inside downloaded files</span><span class="m">the canonical cover, or whatever the uploader embedded</span></div>
|
||||
<div class="chips">
|
||||
{#each ART_MODE as [v, l] (v)}<button class="chip" class:on={artMode === v} class:quiet={artMode !== v} onclick={() => setFlag("replaceEmbeddedArt", v === "replace")}>{l}</button>{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pref">
|
||||
<div class="pref-l"><span class="t">Embedded cover size</span><span class="m">the longest edge of the copy written into each file; the original is kept in the folder</span></div>
|
||||
<div class="chips">
|
||||
{#each ART_EDGE as [v, l] (v)}<button class="chip" class:on={artEdge === v} class:quiet={artEdge !== v} onclick={() => setNumber("embedArtMaxEdge", Number(v))}>{l}</button>{/each}
|
||||
</div>
|
||||
</div>
|
||||
<button class="row" onclick={() => setFlag("writeFolderCover", !(cfg?.write_folder_cover ?? true))}>
|
||||
<span class="lead"><Icon name="image" size={20} /></span>
|
||||
<span class="b"><span class="t">Album folder cover</span><span class="m">cover.jpg and folder.jpg beside the files, so other players show the same art</span></span>
|
||||
<span class="sw" class:on={cfg?.write_folder_cover ?? true}><span></span></span>
|
||||
</button>
|
||||
<button class="row" onclick={refetchCovers} disabled={refetching}>
|
||||
<span class="lead"><Icon name="refresh" size={20} /></span>
|
||||
<span class="b"><span class="t">{refetching ? "Upgrading covers…" : "Re-fetch all album covers"}</span><span class="m">every record to the canonical high-resolution cover — about one a second</span></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="sheet-sec">This phone</div>
|
||||
<div class="group">
|
||||
<div class="kv"><span class="k">SoulStream</span><span class="v">{lib.version || "—"}</span></div>
|
||||
<div class="kv"><span class="k">Soulseek</span><span class="v dim">{lib.username || "not signed in"}</span></div>
|
||||
</div>
|
||||
|
||||
<div class="stub">
|
||||
<b>The network and the account come next.</b>
|
||||
They are the desktop's settings pages, drawn with the phone's components.
|
||||
</div>
|
||||
|
||||
<div class="endpad"></div>
|
||||
Reference in new issue
Block a user