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:
1 parent
bb828ff2dc
commit
6e39dc43f3
38 files changed
+2383
-88
No files matched your search
+101
-5
@@ -172,6 +172,89 @@ namesake), the catalog's released records minus the shelf's by loose title,
|
||||
dimmed cards that open the catalog record page, the totals remembered in
|
||||
`library.gaps.v1` so the line says its number before the lookup.
|
||||
|
||||
## Playlists
|
||||
|
||||
The desktop's two playlist pages (`src/views/Playlists.svelte`,
|
||||
`Playlist.svelte`), at phone scale, on the same commands
|
||||
(`list_playlists`, `playlist_items`, `playlist_cover`, create / rename /
|
||||
describe / pin / duplicate / delete, `playlist_add_items`,
|
||||
`playlist_remove_items`).
|
||||
|
||||
- **The shelf** is Library → Playlists (`src/mobile/PlaylistShelf.svelte`):
|
||||
the stat line, Pinned then All, the custom cover or the composed mosaic on
|
||||
every card (fetched once per revision — `coverFor` in
|
||||
`playlists.svelte.js`), a Setlist / N unresolved badge, New playlist (a
|
||||
prompt sheet). A long press on a card is its sheet: Play, Shuffle, Pin,
|
||||
Rename, Duplicate, Delete (a confirm sheet).
|
||||
- **The page** (`screens/Playlist.svelte`) is the record page's shape: the
|
||||
cover or mosaic, the kind and a Pinned chip, the description, the meta
|
||||
line with its warnings (N unresolved, N missing), Play / Shuffle / More,
|
||||
a filter, and the rows with the desktop's tags — `unresolved`,
|
||||
`set audio`, `file missing`. Setlist cue times show only when every row
|
||||
carries one. The bar's More is the page's sheet: Pin, Rename, Edit
|
||||
description, Duplicate, Remove unresolved entries, Delete.
|
||||
- **Playing** follows the desktop's `queueableRows`: Play queues the rows
|
||||
that can play — present local files as themselves, stream rows as
|
||||
path-less entries with their catalog identity, which the player resolves
|
||||
on reach through `hooks.resolveEntry` (the same path a record's later
|
||||
tracks take, so the three-ahead preload covers them). A tap on a weak row
|
||||
with a name streams it one-off; a bare placeholder opens its sheet. The
|
||||
playing row is found by `plItemId` on the queue entry. A set-recording
|
||||
slice starts at its cue (`t.startSecs`) and the player moves on at the
|
||||
next cue (`t.endSecs`); the scrubber still shows the whole recording.
|
||||
- **A row's sheet** (`pltrack`): Play from here, Play next, Add to queue,
|
||||
Save to library for a stream row, Go to artist, Remove from playlist.
|
||||
A removal is undoable for seven seconds from the neutral toast
|
||||
(`notice.svelte.js` — the desktop's undo pill); the stash pins the
|
||||
playlist it came from.
|
||||
- **Add to library / Download all tracks** on the page's sheet — the
|
||||
desktop's bulk actions (`runBulk` in `playlists.svelte.js`, the rules
|
||||
verbatim): the first files every already-streamed track whose cache is
|
||||
byte-complete, the second finds a source for every stream row (the
|
||||
remembered one, else the ladder) and hands it to the download pipeline;
|
||||
the page shows the run's line with Stop, then its summary.
|
||||
- **Add to playlist** is on every track and record sheet: a pick-a-playlist
|
||||
sheet (New playlist at the top). A file on the phone becomes a local row;
|
||||
a streamed or catalog track keeps its identity (`track_id`) and cover URL
|
||||
and becomes a stream row.
|
||||
- Every mutation re-lists through `soulseek:playlists-changed`, which the
|
||||
desktop's commands already emit; the page re-reads its rows when the
|
||||
playlist's revision moves.
|
||||
- **Importing** (`importer.svelte.js`, `screens/Import.svelte`): the
|
||||
shelf's Import opens the doors that need no file — a link (YouTube ·
|
||||
Spotify · Apple Music · TIDAL · Deezer), a pasted tracklist (plain lines,
|
||||
a timestamped setlist, or track links), a YouTube DJ set's posted
|
||||
tracklist. The backend stages the draft over plain HTTP (the same
|
||||
commands as the desktop, no yt-dlp involved), the review screen shows
|
||||
every row's verdict, the stream rows are verified against the catalog
|
||||
while it is up (the desktop's pass, verbatim), and Create swaps the
|
||||
review for the playlist it made (`nav.replace`).
|
||||
- **The share sheet** (`crates/tauri-plugin-share`, `share.svelte.js`):
|
||||
Share → SoulStream from any app. The plugin's `ShareActivity` (an
|
||||
ACTION_SEND text/plain target, merged into the app's manifest from the
|
||||
plugin module) keeps the text and brings the app's own task to the front
|
||||
through its launcher intent; the WebView asks `pending_share` at boot and
|
||||
whenever it comes back to the front, and a link opens the link door (the
|
||||
first URL in the text — Spotify shares the link inside a sentence),
|
||||
anything else the pasted-tracklist door. The phone walk sends the intent
|
||||
itself (`adb shell am start -a android.intent.action.SEND …`).
|
||||
|
||||
Not on the phone yet: the file and folder import doors (the files are on
|
||||
the desktop), the listener door, the find-a-local-match repair panel, drag
|
||||
reorder, the cover picker, export, Share on SoulStream.
|
||||
|
||||
## Settings that reach the config
|
||||
|
||||
`set_settings` takes **camelCase** arguments (Tauri's rule for command
|
||||
parameters): `{ volumeNormalization, downloadDir, streamSourcePrefs,
|
||||
downloadSourcePrefs }`. The phone's first You screen sent snake_case keys
|
||||
and every save was silently ignored (found 2026-09-13 while adding the
|
||||
Sources group — the walk now flips the switch and a chip and reads each
|
||||
back from the config). You → Sources is the desktop's Source quality
|
||||
section: per mode (streaming, downloads) the ceiling (no limit / lossy
|
||||
only), the floor (any … lossless only) and the priority (best quality /
|
||||
fastest transfer); the resolver re-reads them (`refreshSourcePrefs`).
|
||||
|
||||
## Background playback and the lock-screen card
|
||||
|
||||
The service is started with `startForegroundService`, so it promotes itself
|
||||
@@ -342,12 +425,25 @@ waveform growing; backend events go over the mock's own bus, which
|
||||
press → Search → Library and its tabs → a record → its sheets → the player's
|
||||
three faces → the back gesture → an artist page → Community → a room → a
|
||||
catalog search → a song through the resolver → the lane rising in the sheet →
|
||||
You → the sleep timer, asserts what each step should show, screenshots every
|
||||
step into `tests/mobile/out/`, and fails on any exception or console error.
|
||||
Two toasts are expected there and filtered: without Tauri, `listen` and
|
||||
`invoke` do not exist.
|
||||
a catalog record, its Save and the Transfers burst → Gaps → the playlist
|
||||
shelf and a page with every row kind (a local row, the stream row through
|
||||
the resolver, the filter, a removal and its undo, unpin, rename, a new
|
||||
playlist, Add to playlist from a record's sheet, delete) → the import doors
|
||||
(a pasted tracklist, a link with its catalog verification, a share from
|
||||
another app) → You (the normalization switch and a source-quality chip,
|
||||
each read back from the config) → the sleep timer, asserts what each step
|
||||
should show, screenshots every step into `tests/mobile/out/`, and fails on
|
||||
any exception or console error. The mock answers every command the phone
|
||||
uses; one it does not know is named in the report (`[mock] no mock for X`)
|
||||
rather than surfacing as a TypeError toast, console warnings carry their top
|
||||
frames (the smoke bundle is built unminified for that), and every probe
|
||||
carries the history mirror — the entry's depth against the layers the app
|
||||
says the back gesture can peel (`hd`/`hwant`; a drift is how the blank
|
||||
page after a confirmed delete was found).
|
||||
|
||||
The phone walk is the same driver on the real thing: a debug build's WebView
|
||||
The phone walk is the same driver on the real thing (plus a step kind of
|
||||
its own: `{ adb: [...] }` runs an adb command — the share-sheet intent, a
|
||||
key): a debug build's WebView
|
||||
exposes DevTools on `localabstract:webview_devtools_remote_<pid>`, `adb
|
||||
forward` puts it on a local port, and the steps run against the real library
|
||||
— real playback, real covers, the colour following the record — with the
|
||||
|
||||
Reference in new issue
Block a user