android: the handoff — the pending rounds, the pipeline, and what is unverified
Everything written this session that has not been through the device: rounds 43 to 48 (each patch adversarially verified, 43's transient-notice expectation fixed), a partial round 49, the pipeline scripts, the Android Auto design and the icon comparison mock. Also the two harness changes that were never A/B'd — adaptive waits, which may only shorten a declared wait and never lengthen it, and a ship that runs the mock walk and the deploy at the same time. The A/B and its 255-step baseline are in handoff/tools; main stays at what the phone has actually walked.
This commit is contained in:
1 parent
867fd42972
commit
9f0601027c
400 files changed
+26401
-3
No files matched your search
@@ -0,0 +1,370 @@
|
||||
// Round thirty-six: one file, picked. The storage plugin had a folder
|
||||
// picker and an image picker and no way at all to name a FILE, and the
|
||||
// phone's import sheet has been four doors with a note under it apologising
|
||||
// for the fifth since round 24. So `pick_file`: the document sheet with the
|
||||
// caller's own filter, answered with a path Rust can open — the document's
|
||||
// OWN path where this phone has one (nothing copied, so a 300 MB pick costs
|
||||
// nothing), a bounded copy into cacheDir/picked under its own name where it
|
||||
// does not, and the display name handed over separately because that name
|
||||
// is what decides a playlist's format and its title. Then the fifth door,
|
||||
// on round 24's screen, wait card and review; `playlist_import_probe` took
|
||||
// a path and a name the way `playlist_import_folder` took a path.
|
||||
//
|
||||
// The brief also named a second consumer — `community_pick_file`, "sending
|
||||
// a file in a chat". It does not exist: `community_pick_file` is the YOU
|
||||
// section's three native dialogs (an avatar source, and the .ssrecovery
|
||||
// artifact), the protocol wrapper has `say_room` and `message_user` and
|
||||
// nothing that sends a file, and both frontends' thread composers are text
|
||||
// only. Nothing is wired for it, and nothing is invented.
|
||||
const fs = require('fs');
|
||||
const root = process.env.SS_ROOT || 'F:/Coding/SoulStream/';
|
||||
const scratch = 'C:/Users/Kyle/AppData/Local/Temp/claude/F--Coding-SoulStream/5488514a-11e8-482a-b0a0-20cb511ee110/scratchpad/';
|
||||
const ins = (name) => fs.readFileSync(scratch + name, 'utf8').replace(/\s+$/, '');
|
||||
let p, s;
|
||||
const load = (file) => { p = file; s = fs.readFileSync(root + p, 'utf8'); };
|
||||
const save = () => fs.writeFileSync(root + p, s);
|
||||
const once = (a) => {
|
||||
const i = s.indexOf(a);
|
||||
if (i < 0) throw new Error('missing in ' + p + ': ' + a.slice(0, 90));
|
||||
if (s.indexOf(a, i + 1) >= 0) throw new Error('ambiguous in ' + p + ': ' + a.slice(0, 90));
|
||||
return i;
|
||||
};
|
||||
const rep = (a, b) => { once(a); s = s.replace(a, () => b); };
|
||||
const NL = String.fromCharCode(10);
|
||||
/* Two tolerant anchors, both in ANDROID.md, both for the same reason:
|
||||
playlist EXPORT is named in the same breath as this round's picker twice
|
||||
over — the "Not on the phone yet" sentence and the "Not done yet" bullet
|
||||
— and export is a sibling round's line to close. Whichever of the two
|
||||
rounds ran first, what is left behind has to be true, so each takes
|
||||
either shape and throws only when neither is there. Every anchor outside
|
||||
ANDROID.md is hard, and so is every other anchor inside it. */
|
||||
const either = (a, b, c, d) => {
|
||||
if (s.includes(a)) rep(a, b);
|
||||
else if (s.includes(c)) rep(c, d);
|
||||
else throw new Error('missing (neither shape) in ' + p + ': ' + a.slice(0, 90));
|
||||
};
|
||||
|
||||
/* Several inserts below re-emit their own anchors, and one file is written
|
||||
whole, so a second run would double-write where `once` cannot see it
|
||||
coming and only throw further down, leaving half a tree. Refuse one here,
|
||||
before anything is touched: the plugin's COMMANDS array is what this
|
||||
round creates and nothing else in the repo makes it. */
|
||||
if (fs.readFileSync(root + 'crates/tauri-plugin-storage/build.rs', 'utf8').includes('pick_file')) {
|
||||
throw new Error('crates/tauri-plugin-storage already has pick_file — round 36 has already run against ' + root);
|
||||
}
|
||||
|
||||
// ── the plugin: Kotlin ────────────────────────────────────────────────
|
||||
load('crates/tauri-plugin-storage/android/src/main/java/com/soulstream/storage/StoragePlugin.kt');
|
||||
/* Round thirty-five lands before this one and needs the same two imports for
|
||||
its own document sheet, so take them only if they are not already there —
|
||||
a file cannot import a name twice for the sake of a second caller. */
|
||||
if (!s.includes(`import app.tauri.annotation.InvokeArg`)) {
|
||||
rep(`import app.tauri.annotation.Command`,
|
||||
`import app.tauri.annotation.Command
|
||||
import app.tauri.annotation.InvokeArg`);
|
||||
}
|
||||
if (!s.includes(`import android.provider.OpenableColumns`)) {
|
||||
rep(`import android.provider.DocumentsContract`,
|
||||
`import android.provider.DocumentsContract
|
||||
import android.provider.OpenableColumns`);
|
||||
}
|
||||
/* the args class sits at top level, where transfer-notice's NoticeArgs does
|
||||
— above the plugin's own KDoc, so that comment stays on the class it
|
||||
describes */
|
||||
rep(`/**
|
||||
* The phone's storage for the library: a folder picked through Android's`,
|
||||
ins('ins-r36-kt-top.txt') + NL + NL + `/**
|
||||
* The phone's storage for the library: a folder picked through Android's`);
|
||||
/* the class tail. folderPicked ends with the same two lines, but only
|
||||
imagePicked's are followed by the class's own closing brace. */
|
||||
rep(` Activity.RESULT_CANCELED -> invoke.reject("cancelled")
|
||||
else -> invoke.reject("the picker failed")
|
||||
}
|
||||
}
|
||||
}`,
|
||||
` Activity.RESULT_CANCELED -> invoke.reject("cancelled")
|
||||
else -> invoke.reject("the picker failed")
|
||||
}
|
||||
}
|
||||
|
||||
` + ins('ins-r36-kt-body.txt') + NL + `}`);
|
||||
save();
|
||||
|
||||
// ── the plugin: Rust ──────────────────────────────────────────────────
|
||||
load('crates/tauri-plugin-storage/src/lib.rs');
|
||||
rep(`//! Three commands: \`pick_folder\` → \`{ uri, path }\` (path null when the tree
|
||||
//! is not on a volume with a path), \`all_files_access\` → \`{ granted }\`, and
|
||||
//! \`request_all_files_access\`, which opens the system screen. On the desktop
|
||||
//! the plugin is registered so the permission set resolves, and every
|
||||
//! command answers "not here".`, ins('ins-r36-plugin-doc.txt'));
|
||||
rep(`#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PickedImage {`, ins('ins-r36-plugin-types.txt') + NL + NL + `#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PickedImage {`);
|
||||
rep(`/// A photo from the phone: the document picker limited to images, the`,
|
||||
ins('ins-r36-plugin-cmd.txt') + NL + NL + `/// A photo from the phone: the document picker limited to images, the`);
|
||||
rep(` .invoke_handler(tauri::generate_handler![pick_folder, all_files_access, request_all_files_access, pick_image])`,
|
||||
` .invoke_handler(tauri::generate_handler![pick_folder, all_files_access, request_all_files_access, pick_image, pick_file])`);
|
||||
save();
|
||||
|
||||
load('crates/tauri-plugin-storage/build.rs');
|
||||
rep(`// The three commands, as the Rust functions are named; the ACL slugs them
|
||||
// (\`allow-pick-folder\`, …) into permissions/autogenerated.
|
||||
const COMMANDS: &[&str] = &["pick_folder", "all_files_access", "request_all_files_access", "pick_image"];`,
|
||||
`// The commands, as the Rust functions are named; the ACL slugs them
|
||||
// (\`allow-pick-folder\`, …) into permissions/autogenerated.
|
||||
const COMMANDS: &[&str] = &["pick_folder", "all_files_access", "request_all_files_access", "pick_image", "pick_file"];`);
|
||||
save();
|
||||
|
||||
// ── the permission set. The three files under autogenerated/ are the
|
||||
// plugin build script's own output, emitted here verbatim rather than
|
||||
// hand-written, which is how they drift. ─────────────────────────────
|
||||
load('crates/tauri-plugin-storage/permissions/default.toml');
|
||||
rep(`The phone's storage: pick a library folder (a Storage Access Framework tree,
|
||||
resolved to a path when the volume allows), and Android's all-files access —
|
||||
whether it is granted, and the system screen that grants it.
|
||||
"""
|
||||
permissions = ["allow-pick-folder", "allow-all-files-access", "allow-request-all-files-access", "allow-pick-image"]`,
|
||||
`The phone's storage: pick a library folder (a Storage Access Framework tree,
|
||||
resolved to a path when the volume allows), pick a photo or a file (the
|
||||
document picker, answered with a path — the document's own where this phone
|
||||
has one, else a bounded copy in the app's cache), and Android's all-files
|
||||
access — whether it is granted, and the system screen that grants it.
|
||||
"""
|
||||
permissions = ["allow-pick-folder", "allow-all-files-access", "allow-request-all-files-access", "allow-pick-image", "allow-pick-file"]`);
|
||||
save();
|
||||
fs.writeFileSync(root + 'crates/tauri-plugin-storage/permissions/autogenerated/commands/pick_file.toml',
|
||||
ins('ins-r36-perm-command.txt') + NL);
|
||||
fs.writeFileSync(root + 'crates/tauri-plugin-storage/permissions/autogenerated/reference.md',
|
||||
ins('ins-r36-perm-reference.txt') + NL);
|
||||
/* the schema ends on its last brace, with no newline — the generator's own
|
||||
shape, and a byte the next real build would otherwise rewrite */
|
||||
fs.writeFileSync(root + 'crates/tauri-plugin-storage/permissions/schemas/schema.json',
|
||||
ins('ins-r36-perm-schema.txt'));
|
||||
|
||||
// ── the Rust the door lands on: the probe grows a path AND a name, the
|
||||
// way round 24's folder import grew a path ───────────────────────────
|
||||
load('src-tauri/src/playlists.rs');
|
||||
rep(`/// Open a playlist file and stage it for review. Parsing and resolution only —
|
||||
/// nothing is written until \`playlist_import_commit\`.
|
||||
#[tauri::command]
|
||||
pub async fn playlist_import_probe(
|
||||
app: tauri::AppHandle,
|
||||
lib: State<'_, LibraryState>,
|
||||
) -> Result<Option<ImportDraft>, String> {
|
||||
use tauri_plugin_dialog::DialogExt;
|
||||
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||
app.dialog()
|
||||
.file()
|
||||
.add_filter(
|
||||
"Playlist",
|
||||
&["m3u", "m3u8", "pls", "xspf", "csv", "json", "txt"],
|
||||
)
|
||||
.pick_file(move |path| {
|
||||
let _ = tx.send(path.map(|p| p.to_string()));
|
||||
});
|
||||
let Some(source) = rx.await.unwrap_or(None) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let source_path = PathBuf::from(&source);
|
||||
let name = source_path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| source.clone());`, ins('ins-r36-probe-head.txt'));
|
||||
save();
|
||||
|
||||
// ── the wrappers ──────────────────────────────────────────────────────
|
||||
load('src/lib/api.js');
|
||||
rep(`export const pickImage = () => invoke("plugin:storage|pick_image");`,
|
||||
`export const pickImage = () => invoke("plugin:storage|pick_image");
|
||||
// A FILE from the phone (the storage plugin): { path, name, size, copied }.
|
||||
// \`mimeTypes\` is the document sheet's filter, comma-separated; \`maxCopyBytes\`
|
||||
// is how big a document may be copied into the app's cache when it has no
|
||||
// path of its own on this phone — 0 refuses the copy, which is what an
|
||||
// audio-sized pick wants. The name comes back beside the path because a
|
||||
// copy is named after the document and only the document's name is it.
|
||||
export const pickFile = (mimeTypes, maxCopyBytes = 0) =>
|
||||
invoke("plugin:storage|pick_file", { mimeTypes, maxCopyBytes });`);
|
||||
rep(`// Import: probe opens a file dialog and stages a reviewed draft; text stages
|
||||
// a pasted tracklist; commit writes the accepted draft in one transaction.
|
||||
export const playlistImportProbe = () => invoke("playlist_import_probe");`,
|
||||
`// Import: probe stages a reviewed draft from a playlist file; text stages
|
||||
// a pasted tracklist; commit writes the accepted draft in one transaction.
|
||||
// The desktop passes neither argument and opens its own dialog; the phone
|
||||
// picks with the storage plugin and hands the path in — with the picked
|
||||
// document's own name, which is what chooses the parser and titles the
|
||||
// draft (a copied file is not always named what the document was).
|
||||
export const playlistImportProbe = (path = null, name = null) =>
|
||||
invoke("playlist_import_probe", { path, name });`);
|
||||
save();
|
||||
|
||||
// ── the fifth door ────────────────────────────────────────────────────
|
||||
load('src/mobile/importer.svelte.js');
|
||||
rep(` rules, on the doors that need no file picker: a link (YouTube · Spotify ·
|
||||
Apple Music · TIDAL · Deezer), a pasted tracklist (plain lines, a
|
||||
timestamped setlist, or track links — a paste holding one is read as
|
||||
links), a YouTube DJ set's posted tracklist, and a folder of audio files`,
|
||||
` rules, on every door it has: a playlist file off this phone (M3U · M3U8 ·
|
||||
PLS · XSPF · CSV · JSON), a link (YouTube · Spotify · Apple Music ·
|
||||
TIDAL · Deezer), a pasted tracklist (plain lines, a timestamped setlist,
|
||||
or track links — a paste holding one is read as links), a YouTube DJ
|
||||
set's posted tracklist, and a folder of audio files`);
|
||||
rep(` door: null, // "link" | "text" | "youtube" | "folder"`,
|
||||
` door: null, // "file" | "link" | "text" | "youtube" | "folder"`);
|
||||
rep(`/** [door, icon, title, hint] — the desktop's Import menu, minus the
|
||||
* playlist-file door (that one still wants a file picker). A YouTube
|
||||
* playlist link goes through "link": the backend dispatches it. */
|
||||
export const DOORS = [`,
|
||||
`/** [door, icon, title, hint] — the desktop's Import menu, all five doors.
|
||||
* A YouTube playlist link goes through "link": the backend dispatches it. */
|
||||
export const DOORS = [
|
||||
["file", "file-text", "Playlist file…", "M3U · M3U8 · PLS · XSPF · CSV · JSON"],`);
|
||||
rep(`/** The folder door picks FIRST — the phone's own document-tree picker — so`,
|
||||
ins('ins-r36-importer-pick.txt') + NL + `/** The folder door picks FIRST — the phone's own document-tree picker — so`);
|
||||
rep(`/** Stage a folder of audio files. The backend walks the folder and its disc`,
|
||||
ins('ins-r36-importer-start.txt') + NL + `/** Stage a folder of audio files. The backend walks the folder and its disc`);
|
||||
save();
|
||||
|
||||
load('src/mobile/HalfSheet.svelte');
|
||||
rep(` import { DOORS, start as startImport, pickImportFolder, startFolder } from "./importer.svelte.js";`,
|
||||
` import { DOORS, start as startImport, pickImportFolder, startFolder, pickImportFile, startFile } from "./importer.svelte.js";`);
|
||||
rep(` /* the folder door picks FIRST: the review only opens for a folder that`,
|
||||
ins('ins-r36-halfsheet-door.txt') + NL + ` /* the folder door picks FIRST: the review only opens for a folder that`);
|
||||
rep(` {@render row(icon, t, m, () => (door === "folder" ? folderDoor() : importPrompt(door)))}`,
|
||||
` {@render row(icon, t, m, () => (door === "folder" ? folderDoor() : door === "file" ? fileDoor() : importPrompt(door)))}`);
|
||||
/* the note that apologised for the missing door says the one true thing
|
||||
about the door instead: a file copied onto this phone has lost its own
|
||||
folder, and the rows in it match by filename after that */
|
||||
rep(` <div class="sec-note" style="padding-bottom: 14px">A playlist file — M3U, XSPF, PLS or CSV — imports on the desktop; the phone has no file picker for one yet.</div>`,
|
||||
` <div class="sec-note" style="padding-bottom: 14px">A playlist file's rows are matched against the library on this phone — by path where the file names one, and by filename otherwise, so a file copied here from another machine still finds what you have.</div>`);
|
||||
save();
|
||||
|
||||
load('src/mobile/screens/Import.svelte');
|
||||
rep(` : importer.door === "folder" ? "Reading the folder…"`,
|
||||
` : importer.door === "file" ? "Reading the playlist file…"
|
||||
: importer.door === "folder" ? "Reading the folder…"`);
|
||||
save();
|
||||
|
||||
load('src/mobile/Mobile.svelte');
|
||||
rep(` import { importer, bindImporter, startFolder } from "./importer.svelte.js";`,
|
||||
` import { importer, bindImporter, startFolder, startFile } from "./importer.svelte.js";`);
|
||||
rep(`imp: { startFolder },`, `imp: { startFolder, startFile },`);
|
||||
save();
|
||||
|
||||
// ── the mock: the picker, and the probe behind it ─────────────────────
|
||||
load('src/lib/mockLibrary.js');
|
||||
rep(` if (cmd === "plugin:storage|pick_image") return Promise.resolve({ path: "C:\\\\mock\\\\photo.jpg" });`,
|
||||
` if (cmd === "plugin:storage|pick_image") return Promise.resolve({ path: "C:\\\\mock\\\\photo.jpg" });
|
||||
/* a document that HAD to be copied, with the two names apart: the cache
|
||||
file is safeName()'s — every character outside the plugin's allowed
|
||||
set replaced, so the "#" is an underscore there — and the document's
|
||||
own is what the draft must be read and titled by. Same on purpose:
|
||||
if the name stopped travelling, the path's basename would still say
|
||||
m3u8 and only the title would quietly go wrong, which is the bug. */
|
||||
if (cmd === "plugin:storage|pick_file") return Promise.resolve({ path: "C:\\\\mock\\\\picked\\\\Warehouse set _3.m3u8", name: "Warehouse set #3.m3u8", size: 2048, copied: true });`);
|
||||
rep(` if (cmd === "playlist_import_folder") {`,
|
||||
ins('ins-r36-mock-probe.txt') + NL + ` if (cmd === "playlist_import_folder") {`);
|
||||
save();
|
||||
|
||||
// ── the walks. Both blocks sit INSIDE round 24's import run rather than
|
||||
// at the array ends, which every sibling round reaches for. ─────────
|
||||
load('tests/mobile/smoke.mjs');
|
||||
rep(` impBtn: (document.querySelector('.imp-foot .btn-fill') || {}).textContent || '',`,
|
||||
` impBtn: (document.querySelector('.imp-foot .btn-fill') || {}).textContent || '',
|
||||
impSrc: (document.querySelector('.imp-src') || {}).textContent || '',`);
|
||||
/* the share-cancel line is byte-identical in both walks, so the anchor
|
||||
takes the line after it too */
|
||||
rep(' { do: `__click(\'.imp-foot .btn-ghost\', null)`, name: "share-cancel", wait: 900 },' + NL
|
||||
+ ' { do: `document.querySelectorAll(\'.segtabs button\')[0].click()`, wait: 400 },',
|
||||
' { do: `__click(\'.imp-foot .btn-ghost\', null)`, name: "share-cancel", wait: 900 },' + NL
|
||||
+ ins('ins-r36-mock-steps.txt') + NL
|
||||
+ ' { do: `document.querySelectorAll(\'.segtabs button\')[0].click()`, wait: 400 },');
|
||||
/* "share-cancel" reads identically in both maps, so this anchor takes the
|
||||
line after it as well */
|
||||
rep(` "share-cancel": (i) => i.title.startsWith("Your library"),
|
||||
"cat-album-row":`,
|
||||
` "share-cancel": (i) => i.title.startsWith("Your library"),
|
||||
` + ins('ins-r36-mock-expect.txt') + NL + ` "cat-album-row":`);
|
||||
rep(' { do: `__click(\'.halfsheet .row\', \'Folder of audio files\')`, name: "import-folder-picker", wait: 2600 },' + NL
|
||||
+ ' { adb: ["shell", "input", "keyevent", "4"], name: "import-folder-cancelled", wait: 2000 },',
|
||||
' { do: `__click(\'.halfsheet .row\', \'Folder of audio files\')`, name: "import-folder-picker", wait: 2600 },' + NL
|
||||
+ ' { adb: ["shell", "input", "keyevent", "4"], name: "import-folder-cancelled", wait: 2000 },' + NL
|
||||
+ ins('ins-r36-phone-steps.txt'));
|
||||
rep(` "import-folder-cancelled": (i) => i.title.startsWith("Your library") && !i.half.includes("up") && !i.toast,`,
|
||||
` "import-folder-cancelled": (i) => i.title.startsWith("Your library") && !i.half.includes("up") && !i.toast,
|
||||
` + ins('ins-r36-phone-expect.txt'));
|
||||
save();
|
||||
|
||||
// ── the doc ───────────────────────────────────────────────────────────
|
||||
load('ANDROID.md');
|
||||
rep(`is outside the app's private data and access is missing). Nothing is read
|
||||
outside the folder the user picked.`,
|
||||
`is outside the app's private data and access is missing). Nothing is read
|
||||
outside the folder the user picked.
|
||||
|
||||
` + ins('ins-r36-doc-storage.txt'));
|
||||
rep(` 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`);
|
||||
rep(` \`playlist_import_folder\`, which grew a \`path\` argument for it: on the
|
||||
phone that command had no dialog to open and always answered nothing.`,
|
||||
` \`playlist_import_folder\`, which grew a \`path\` argument for it: on the
|
||||
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.`);
|
||||
/* export is named beside the file door here, so the sibling round that
|
||||
ships export rewrites this sentence before this one reaches it — and it
|
||||
re-wraps the lines when it does. Both shapes are answered, and BOTH
|
||||
answers end on the line
|
||||
"yt-dlp, a desktop tool), drag reorder, and Share on SoulStream."
|
||||
byte for byte, because that line is the drag-reorder round's own anchor
|
||||
and it is the third round to want this one sentence. Only the clause this
|
||||
round closes comes out; the rest is left where it was found. */
|
||||
either(`Not on the phone yet: the playlist-file door and export — both want 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: export — it wants a file picker the storage plugin
|
||||
has still to grow, or a writer for one — the listener door (it needs
|
||||
yt-dlp, a desktop tool), drag reorder, and Share on SoulStream.`,
|
||||
`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.`);
|
||||
rep(`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
|
||||
library)`,
|
||||
/* the line "library) → the artwork viewer → …" is left where it is, whole:
|
||||
the drag-reorder round anchors on it. So the wrap above it absorbs the
|
||||
new clause instead. */
|
||||
`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)`);
|
||||
/* and the bullet itself. Its other half is export again: while export is
|
||||
still waiting, only this round's sentence comes out and the bullet stands.
|
||||
Once the sibling round has shipped export the bullet is nothing BUT this
|
||||
sentence, and a "waiting on a picker" with nothing waiting — and a plugin
|
||||
that "cannot read one" when it can — is worse than no bullet, so the
|
||||
whole thing goes. */
|
||||
const BULLET_AFTER_EXPORT = `- **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.
|
||||
`;
|
||||
const LINE_THIS_ROUND = ` Playlist-FILE import wants a \`pick_file\` in the storage plugin,
|
||||
beside the \`pick_folder\` and \`pick_image\` it already has.`;
|
||||
if (s.includes(BULLET_AFTER_EXPORT)) rep(BULLET_AFTER_EXPORT, '');
|
||||
else if (s.includes(LINE_THIS_ROUND)) rep(LINE_THIS_ROUND, '');
|
||||
else console.log(' (round 36: the pick_file line had already gone from "Not done yet")');
|
||||
save();
|
||||
|
||||
console.log('round 36 wired');
|
||||
Reference in new issue
Block a user