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
View File
@@ -196,6 +196,7 @@ const PROBE = `(function () {
impWaitLine: (document.querySelector('.imp-wait b') || {}).textContent || '',
impWarn: [].map.call(document.querySelectorAll('.imp-warn div'), function (d) { return d.textContent.trim(); }).join('|'),
impBtn: (document.querySelector('.imp-foot .btn-fill') || {}).textContent || '',
impSrc: (document.querySelector('.imp-src') || {}).textContent || '',
keepOn: !!document.querySelector('.imp-form .sw.on'),
bulkline: (document.querySelector('.bulkline') || {}).textContent || '',
brwDirs: document.querySelectorAll('.brw-dir').length,
@@ -716,6 +717,24 @@ const MOCK_STEPS = [
app comes to the front, and it lands in the import */
{ do: `__ssMock.share = 'Check this out https://music.apple.com/us/playlist/late/pl.u-mock'; document.dispatchEvent(new Event('visibilitychange'))`, name: "share-import", wait: 3600 },
{ do: `__click('.imp-foot .btn-ghost', null)`, name: "share-cancel", wait: 900 },
/* the playlist-FILE door, the fifth: the phone's own document picker,
and the picked document's NAME deciding both the format and the
draft's title — then the two answers a picker can give that are not a
file, armed through the mock's fail door */
{ do: `__click('.newpl', 'Import')`, name: "import-file-sheet", wait: 700 },
{ do: `__click('.halfsheet .row', 'Playlist file')`, name: "import-file-review", wait: 2200 },
{ do: `__click('.imp-foot .btn-fill', 'Create playlist')`, name: "import-file-created", wait: 1500 },
{ do: `history.back()`, wait: 900 },
{ do: `__ssMock.fail('plugin:storage|pick_file', 'cancelled')`, wait: 200 },
{ do: `__click('.newpl', 'Import')`, wait: 700 },
{ do: `__click('.halfsheet .row', 'Playlist file')`, name: "import-file-cancelled", wait: 1400 },
{ do: `__ssMock.fail('plugin:storage|pick_file', 'that file is not on this phone')`, wait: 200 },
{ do: `__click('.newpl', 'Import')`, wait: 700 },
{ do: `__click('.halfsheet .row', 'Playlist file')`, name: "import-file-refused", wait: 1400 },
/* disarmed and the red dismissed, or every step after this one inherits
both — the toast stays until it is tapped */
{ do: `__ssMock.fail('plugin:storage|pick_file', '')`, wait: 200 },
{ do: `(function () { var t = document.querySelector('.toast:not(.notice)'); if (t) t.click(); return true; })()`, name: "import-file-cleared", wait: 600 },
{ do: `document.querySelectorAll('.segtabs button')[0].click()`, wait: 400 },
{ do: `__settle()`, wait: 800 },
{ do: `document.querySelectorAll('.nav button')[4].click()`, name: "you", wait: 700 },
@@ -1182,6 +1201,18 @@ const MOCK_EXPECT = {
"import-folder-created": (i) => i.title.startsWith("Music") && i.plrows === 4,
"share-import": (i) => i.title.startsWith("Import") && /Apple Music/.test(i.impFmt) && i.imp === 5,
"share-cancel": (i) => i.title.startsWith("Your library"),
"import-file-sheet": (i) => i.half.includes("up") && /Playlist file/.test(i.halfText),
/* impSrc is the round's own assertion, and the "#" is the whole of it:
the cache copy on disk is "Warehouse set _3.m3u8", so a draft read
off the PATH says that and a draft read off the picked document's
display name says this. Both this and the title below go wrong the
moment the name stops travelling beside the path. */
"import-file-review": (i) => i.title.startsWith("Import") && /M3U/.test(i.impFmt) && i.impSrc === "Warehouse set #3.m3u8"
&& i.imp === 3 && /1 in library/.test(i.impCounts) && /Create playlist \(3 tracks\)/.test(i.impBtn),
"import-file-created": (i) => i.title.startsWith("Warehouse set #3") && i.plrows === 3,
"import-file-cancelled": (i) => i.title.startsWith("Your library") && !i.half.includes("up") && !i.toast,
"import-file-refused": (i) => !i.half.includes("up") && /not on this phone/.test(i.toast),
"import-file-cleared": (i) => !i.toast,
"cat-album-row": (i) => i.playing.includes("Aerodynamic") && /streaming from/.test(i.live),
"cat-album-next": (i) => i.playing.includes("Digital Love") && /streaming from/.test(i.live),
"cat-album-disc": (i) => i.playing.includes("One More Time") && /streaming from/.test(i.live),
@@ -1341,6 +1372,11 @@ const PHONE_STEPS = [
{ do: `__click('.newpl', 'Import')`, wait: 700 },
{ do: `__click('.halfsheet .row', 'Folder of audio files')`, name: "import-folder-picker", wait: 2600 },
{ adb: ["shell", "input", "keyevent", "4"], name: "import-folder-cancelled", wait: 2000 },
/* the file door's picker, the same way: opened, dismissed with BACK.
Nothing is read, nothing is copied into the cache, nothing is made */
{ do: `__click('.newpl', 'Import')`, wait: 700 },
{ do: `__click('.halfsheet .row', 'Playlist file')`, name: "import-file-picker", wait: 2600 },
{ adb: ["shell", "input", "keyevent", "4"], name: "import-file-dismissed", wait: 2000 },
/* the share sheet: a Spotify playlist link sent by "another app" (an
intent on the plugin's activity) lands in the import — the review is
left without creating anything */
@@ -1492,6 +1528,8 @@ const PHONE_EXPECT = {
"import-folder-real": (i) => i.title.startsWith("Import") && /Folder/.test(i.impFmt) && i.imp > 0,
"import-folder-picker": (i) => !i.half.includes("up") && !i.toast,
"import-folder-cancelled": (i) => i.title.startsWith("Your library") && !i.half.includes("up") && !i.toast,
"import-file-picker": (i) => !i.half.includes("up") && !i.toast,
"import-file-dismissed": (i) => i.title.startsWith("Your library") && !i.half.includes("up") && !i.toast,
"share-import": (i) => i.title.startsWith("Import") && (i.imp > 0 || i.impErr),
"share-cancel": (i) => i.title.startsWith("Your library"),
artist: (i) => i.screen.includes("has-hero") && i.amb.includes("on:img") && i.tabs >= 1