android: playlists, the import doors, the share sheet, You → Sources — and three bugs the walks found

The desktop's two playlist pages on the phone: the shelf (pinned first,
mosaic covers, badges, New playlist, Import) and the page (kind, Pinned
chip, description, the meta line's warnings, Play / Shuffle / more, a
filter, rows tagged unresolved / set audio / file missing). Sheets for a
card, the page's overflow, a row, pick-a-playlist, a prompt and a confirm;
Add to playlist on every track and record sheet; a removal undoable for
seven seconds from the neutral toast (notice.svelte.js); stream rows play
through the resolver as path-less entries, a set slice starts and ends at
its cues; Add to library / Download all tracks with the desktop's bulk
rules; nav.replace for a review that became the thing it made.

The import doors that need no file — a link (YouTube · Spotify · Apple
Music · TIDAL · Deezer), a pasted tracklist, a YouTube DJ set — with the
review screen and the desktop's catalog verification pass. A share plugin
crate (crates/tauri-plugin-share): Share → SoulStream from any app keeps
the text in a ShareActivity and the WebView hands it to the import when it
comes back to the front. You → Sources: the source-quality window and the
priority per mode.

Found by the walks and fixed: set_settings takes camelCase arguments, so
the phone's normalization and library-folder saves never landed;
Cover.svelte's non-eager wrapper has no layout box, so its intersection
observer never fired and row thumbnails never loaded (the Artists tab too);
a history push while a history.go was still in flight left the nav mirror
one entry ahead and the page blank after a confirmed delete — the mirror
now applies once per microtask and never pushes during a pending traversal.

The harness names unmocked commands, carries console warnings' frames (the
smoke bundle is built unminified), probes the history mirror on every step,
mocks app_status / play_local / waveform / lyrics_get / player_state and
applies settings patches, runs adb steps on the phone (the share intent),
and walks playlists, the imports, the share and the settings on both the
mock and the phone. ANDROID.md documents all of it.
This commit is contained in:
KeelrLeek committed 2026-09-13 17:56:52 -05:00
1 parent bb828ff2dc
commit 6e39dc43f3
38 files changed
+2383 -88

No files matched your search

+49 -4
View File
@@ -8,6 +8,7 @@
import { transfers, active } from "../transfers.svelte.js";
import { player } from "../player.svelte.js";
import { settings, setSetting } from "../prefs.svelte.js";
import { refreshSourcePrefs } from "../../lib/sources.js";
let cfg = $state(null);
let scanning = $state(false);
@@ -17,7 +18,7 @@
const normalization = $derived(cfg ? cfg.volume_normalization !== false : true);
async function toggleNormalization() {
const next = !normalization;
try { await api.setSettings({ volume_normalization: next }); cfg = await api.getSettings(); } catch (e) { player.error = String(e); }
try { await api.setSettings({ volumeNormalization: next }); cfg = await api.getSettings(); } catch (e) { player.error = String(e); }
}
/* the library folder: picked on the phone, set as the app's download
dir (the folder IS the library), rescanned. A folder in shared storage
@@ -39,7 +40,7 @@
try {
const r = await api.pickLibraryFolder();
if (!r?.path) { player.error = "That folder has no path the phone can scan — pick one on this phone's storage."; return; }
await api.setSettings({ download_dir: r.path });
await api.setSettings({ downloadDir: r.path });
cfg = await api.getSettings();
await rescan();
} catch (e) {
@@ -53,6 +54,24 @@
scanning = false;
}
/* the desktop's Source quality settings (Settings → Sources): a window
per mode and the priority inside it; the resolver re-reads them */
const MODES = [["stream", "Streaming"], ["download", "Downloads"]];
const MAX_Q = [["any", "No limit"], ["lossy_only", "Lossy only"]];
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" };
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;
const next = { ...prefsOf(mode), [field]: value };
try {
await api.setSettings(mode === "stream" ? { streamSourcePrefs: next } : { downloadSourcePrefs: next });
cfg = await api.getSettings();
await refreshSourcePrefs();
} catch (e) { player.error = String(e); }
}
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"],
@@ -110,6 +129,32 @@
</button>
</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)}
<div class="group prefs" data-mode={key}>
<div class="pref-head">{label}</div>
<div class="pref">
<div class="pref-l"><span class="t">Maximum</span><span class="m">lossy only avoids huge lossless transfers</span></div>
<div class="chips">
{#each MAX_Q as [v, l] (v)}<button class="chip" class:on={prefsOf(key).max === v} class:quiet={prefsOf(key).max !== v} onclick={() => setPref(key, "max", v)}>{l}</button>{/each}
</div>
</div>
<div class="pref">
<div class="pref-l"><span class="t">Minimum</span><span class="m">files below the floor are skipped; a lossy file must report its bitrate</span></div>
<div class="chips">
{#each MIN_Q as [v, l] (v)}<button class="chip" class:on={prefsOf(key).min === v} class:quiet={prefsOf(key).min !== v} onclick={() => setPref(key, "min", v)}>{l}</button>{/each}
</div>
</div>
<div class="pref">
<div class="pref-l"><span class="t">Priority</span><span class="m">among the files inside that window</span></div>
<div class="chips">
{#each PRI as [v, l] (v)}<button class="chip" class:on={prefsOf(key).priority === v} class:quiet={prefsOf(key).priority !== v} onclick={() => setPref(key, "priority", v)}>{l}</button>{/each}
</div>
</div>
</div>
{/each}
<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>
@@ -117,8 +162,8 @@
</div>
<div class="stub">
<b>Playback, storage, the network and the account come next.</b>
They are the desktop's settings pages, drawn with the phone's components; the library folder you pick and Import land with them.
<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>