android: a playlist file, picked

The storage plugin grew `pick_file` beside `pick_folder` and `pick_image`:
the document sheet with the caller's own filter, answered with a path Rust
can open. A document already on this phone hands its path over as it lies —
the document sibling of the same volume:relative id the folder picker
resolves, plus the Downloads provider's raw: — so a 300 MB pick copies
nothing; one that has no path is copied into cacheDir/picked under its own
name, and only as far as the caller allows, because a playlist is kilobytes
and a DJ set is not. Those copies are swept at the start of every pick,
which pick_image's never were.

The import sheet has its fifth door, on round 24's screen, wait card and
review, and the note that apologised for its absence says the one true thing
about it instead. `playlist_import_probe` took a path and a name the way
`playlist_import_folder` took a path, and the name is the load-bearing half:
the parser picks its format off the extension and titles the draft from the
stem, so a cache copy under an invented name would have read every playlist
as plain text. The mock walk drives the door and both of the picker's
refusals through round 31's fail door; the phone walk opens the real sheet
and dismisses it with BACK, reading nothing and writing nothing.

The brief asked for a second consumer — sending a file in a chat — and there
is nothing behind it: `community_pick_file` is the persona layer's three
native dialogs, `soulseek-client` has `say_room` and `message_user` and
nothing that sends a file, and both frontends' thread composers are text
only. Nothing was invented for it. The plugin command is general enough — a
caller's own filter, a zero-copy path, a bounded copy — that the one reader
still waiting, the local set listener the phone puts off by design, needs no
further plugin change. Playlist export is not on that list and never was:
writing a document out is the share plugin's errand, not a picker's. The
Kotlin is unbuilt — nothing on this machine compiles it.
This commit is contained in:
KeelrLeek committed 2026-09-14 02:55:56 -05:00
1 parent 04c87e0db1
commit defd8c5e6d
16 files changed
+559 -57

No files matched your search

+38 -11
View File
@@ -207,6 +207,27 @@ the system screen that grants it (You shows the row only while the folder
is outside the app's private data and access is missing). Nothing is read
outside the folder the user picked.
**A file** from the phone is the same plugin's `pick_file`, beside
`pick_folder` and `pick_image`: Android's document picker with the caller's
own MIME filter, answered with `{ path, name, size, copied }`. It copies as
little as it can, because an image is small and an audio file is not. A
document already on this phone's storage hands its own path over as it lies
— the DOCUMENT sibling of the tree id the folder picker resolves, plus the
Downloads provider's `raw:` — so picking a 300 MB file costs nothing and
touches no cache. One that has no path (a cloud provider's, a document a
provider only streams) is copied into `cacheDir/picked` under its own
display name, and only as far as the caller's `maxCopyBytes` allows: zero
refuses the copy outright, which is what an audio-sized pick wants, and the
copy counts itself as it goes because a provider's stated size is often
absent and sometimes wrong. Those copies are swept at the start of every
pick — unlike `pick_image`'s, which live in the cache for ever — so a path
from one pick is gone by the next. The display name travels beside the path
because it is load-bearing: `playlist_formats::detect_format` reads the
extension off it and `parse` titles the draft from its stem, so a copy under
an invented name would read every playlist as plain text. The dialog plugin
cannot serve here: `tauri-plugin-dialog` does open the sheet from Rust, but
it answers with a `content://` URI, and `std::fs` cannot take one.
**Playback** (You → Playback) has the desktop's volume normalization switch
(−14 LUFS through the player crate's BS.1770 meter; on by default). There is
no graphic equalizer in the app — the "eq" in the code is the animated bars
@@ -445,7 +466,8 @@ describe / pin / duplicate / delete, `playlist_add_items`,
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 four doors — a link (YouTube · Spotify · Apple
shelf's Import opens five doors — a playlist file on this phone (M3U ·
M3U8 · PLS · XSPF · CSV · JSON), a link (YouTube · Spotify · Apple
Music · TIDAL · Deezer; a YouTube playlist link rides this one too, the
backend dispatches it by host), a pasted tracklist (plain lines, a
timestamped setlist, or track links), a YouTube DJ set's posted
@@ -456,7 +478,15 @@ describe / pin / duplicate / delete, `playlist_add_items`,
wait screen counts them as they come. The folder door picks with the
storage plugin's document-tree picker and hands the resolved path to
`playlist_import_folder`, which grew a `path` argument for it: on the
phone that command had no dialog to open and always answered nothing.
phone that command had no dialog to open and always answered nothing. The
file door does the same through the plugin's `pick_file`, and
`playlist_import_probe` grew `path` AND `name` for it — the picked
document's own display name is what `detect_format` reads the extension
off and what titles the draft, and a document this phone had no path for
arrives as a cache copy under a sanitized filename. A copied file's rows
still find the library: a relative locator that misses falls back to the
basename index, which is the one thing that reads differently from the
desktop, and the sheet says so.
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
@@ -491,9 +521,8 @@ describe / pin / duplicate / delete, `playlist_add_items`,
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 playlist-file door — it wants a file picker the
storage plugin has still to grow — the listener door (it needs yt-dlp, a
desktop tool), drag reorder, and Share on SoulStream.
Not on the phone yet: the listener door (it needs
yt-dlp, a desktop tool), drag reorder, and Share on SoulStream.
## The first run
@@ -1120,8 +1149,10 @@ 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, pasted track
links read a page each, a folder of audio files off the phone, a share
from another app) → the playlist repair (an unknown row relinked from the
links read a page each, a folder of audio files off the phone, a playlist
file through the phone's own document picker — with that picker cancelled
and a file it can hand no path for refused — and a share from another
app) → the playlist repair (an unknown row relinked from the
library) → the artwork viewer → a queue row's sheet → a catalog song's,
a catalog card's and the record page's sheets → the now-playing Save →
a peer's sheet, a raw file's sheet, a user's shares browsed and a folder
@@ -1228,10 +1259,6 @@ symptom.
- **Off by design — external runtimes.** yt-dlp and deno, the vocal
separator, the lyric alignment models, and the listener door that needs
them.
- **Waiting on a picker.** The storage plugin can WRITE a document now
(`create_document`, which the playlist export goes through) but it cannot
read one. Playlist-FILE import wants a `pick_file` in the storage plugin,
beside the `pick_folder` and `pick_image` it already has.
- **Still desktop-only, smaller.** Drag reorder (Move up / down / to the top
stand in), catalog track previews, the full discography page, and the
fused-artist portrait verdict — that last one needs the identity merge the