android: the first run — a folder to keep music in, and an account to reach the network
Nobody had seen this app on a phone that has just installed it. On Android the library root is never missing — config.rs falls back to the app's own sandbox, a real folder nothing else can see — so "no folder yet" cannot be a null check, and setup_done has never been written by the phone, so every phone in existence reads as unstamped, Kyle's included. The tour therefore opens on three clauses, not one: no flag, no saved account, and no records, read only after the library and the credentials have both answered. Two cards, skippable, and no password anywhere in it — the Soulseek step goes to the card on Community and stops. Every screen that is empty because of those two things now says so with the row that fixes it: Home (whose copy predated the folder picker), Library → Albums, Library → Artists, which had no empty state at all, and Search, where a flat sentence about signing in became the row that does it. A row goes away because the config moved, so the two doors onto the same folder tell each other — You re-reads when the tour moves it, the tour's store is told when You's own picker does — and the all-files row re-reads the grant when the system screen hands the app back, the way You already did. You → This phone opens the tour again as a preview that reads the config and writes nothing, which is the door the phone walk drives on Kyle's own device. The mock can answer as a fresh install now — an empty database, no account, no permission, the sandbox root — so the whole thing is walked end to end, and two commands that had no mock at all (player_control, open_external) got one on the way past. It is the mock walk's first reload, which turned up two things a reload does not clear by itself: the player writes the session back on pagehide, so the fresh block clears it from a pagehide listener of its own, and the viewport emulation lives on the DevTools session, so the harness hands it back after the re-attach.
This commit is contained in:
1 parent
46cf6a98fe
commit
95c2bfeeba
13 files changed
+633
-14
No files matched your search
+131
-2
@@ -473,6 +473,119 @@ 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.
|
||||
|
||||
## The first run
|
||||
|
||||
Every walk and every deploy so far started from Kyle's own phone — signed
|
||||
in, library folder set, a config full of his settings. What a phone that
|
||||
has just installed the app actually has is different in three ways, and
|
||||
only one of them is what you would guess.
|
||||
|
||||
**The library root is never missing.** `config.rs`'s `download_dir` has an
|
||||
`#[cfg(target_os = "android")]` branch that falls back to
|
||||
`app_data_dir()/Library` and returns before the desktop's `audio_dir()`
|
||||
fallback is reached, so a fresh install has a real, writable, empty library
|
||||
root at `/data/user/0/com.soulstream.app/files/Library` — invisible to
|
||||
every other app on the phone, including the one that put the music there.
|
||||
`pipeline::init` scans it at startup and `library_albums` honestly answers
|
||||
`[]`. This is why "no folder yet" cannot be a null check anywhere: the test
|
||||
is the PATH, `isPrivateFolder` in `setup.svelte.js`, which You.svelte had
|
||||
first and now imports rather than keeping a second copy of.
|
||||
|
||||
**`setup_done` was already there and the phone never touched it.** It
|
||||
round-trips through `get_settings`/`set_settings` like any other field and
|
||||
`config::setup_done` unwraps a missing one to `false`, so **every phone in
|
||||
existence reads as unstamped**, Kyle's included. Gating the tour on that
|
||||
flag alone — which is what the desktop does — would have opened a setup
|
||||
sheet on a phone already full of his own music, and then written the flag
|
||||
into his config on dismissal.
|
||||
|
||||
**Nothing is signed in, and everything says so honestly and helpfully
|
||||
nowhere.** `connectStored()` returns at once with no account saved, the
|
||||
connection stays `offline`, and every stream and every Save ends as a red
|
||||
toast with a sentence in it and no way out.
|
||||
|
||||
So: `src/mobile/setup.svelte.js` is the one store, and there are two
|
||||
surfaces.
|
||||
|
||||
**The tour** is a half sheet (`kind === "setup"` in `HalfSheet.svelte`) —
|
||||
one scroll, two cards, a foot. Where your music lives: the current folder,
|
||||
a row that opens the storage plugin's folder picker, and the all-files
|
||||
permission row beside it once a real folder has been picked. Soulseek: what
|
||||
it unlocks, and one row that goes to the sign-in card on Community. **The
|
||||
tour never handles a password** — there is no field in it; Kyle signs in
|
||||
himself, on the card that already existed. Keeping the private folder and
|
||||
staying signed out are both legitimate answers, so the sheet is answerable
|
||||
by doing nothing, and the scrim and the back gesture close it like any
|
||||
other half sheet.
|
||||
|
||||
**The boot gate is a three-way AND** — `!setup_done && !slsk.savedUser &&
|
||||
!lib.albums.length` — and it is evaluated only after `loadLibrary()` and
|
||||
`connectStored()` have both settled (`Promise.all` in `Mobile.svelte`'s
|
||||
`onMount`). Read either of those early and they are `[]` and `""` on every
|
||||
phone there is. Kyle's phone fails the second and third clause, which is
|
||||
the only safe way to ship an auto-opening layer into a pipeline that
|
||||
installs onto his device.
|
||||
|
||||
**The note** (`SetupNote.svelte`) is the fallback for the screens that are
|
||||
empty because of the same two things, with the row that fixes each: Home's
|
||||
empty state (whose copy still said music "lives in the app's own Library
|
||||
folder for now", written before the picker landed in You), Library →
|
||||
Albums, Library → **Artists** — which had no empty state at all, just a
|
||||
`0 artists · 0 followed` line over nothing — and Search's empty page, where
|
||||
"Sign in on Community to play what the library does not hold" was a flat
|
||||
sentence and is now the row that does it. A row is drawn only for a thing
|
||||
actually missing, and the component renders nothing when both are answered,
|
||||
so a phone that is set up never sees it.
|
||||
|
||||
There are two doors onto the same folder — the tour's row and You's own
|
||||
picker — and they tell each other: You re-reads when `setup.folder` moves,
|
||||
and its picker calls `loadSetup()` on the way past, or the note on Home
|
||||
would go on offering a folder that had already been picked on You. The
|
||||
all-files row does the same trick You does with the grant: the system
|
||||
screen takes the app away and gives it back, so `requestAccess` re-reads
|
||||
the answer on the way back rather than leaving a row up that has been
|
||||
answered.
|
||||
|
||||
**The preview door** is one row in You → This phone: "Set up this phone
|
||||
again", `openSetup("preview")`. A preview reads the config and writes
|
||||
nothing — `setup.writes`, the number of times the store has stamped
|
||||
`setup_done`, stays where it found it. The phone walk drives exactly that
|
||||
door on Kyle's phone and asserts the counter at zero on the way in and on
|
||||
the way out; it types nothing, picks nothing and clears nothing.
|
||||
|
||||
The harness hosts the other device. `localStorage.mockFresh`, or `#fresh`
|
||||
in the URL (which is what the walk uses — a reload cannot lose a fragment),
|
||||
switches `src/lib/mockLibrary.js` to a fresh install: every library
|
||||
command answers `[]`, `get_credentials` answers with no account,
|
||||
`all_files_access` is not granted, and `mockSettings` is mutated in place
|
||||
so the library root is the sandbox path and `setup_done` is false —
|
||||
mutated rather than replaced so `set_settings` still works and picking a
|
||||
folder in the walk genuinely moves the state the notes read. The
|
||||
fresh-install block is the LAST thing in `MOCK_STEPS` because its reload
|
||||
wipes every module's state, and it turns the flag off and reloads again at
|
||||
the end so that anything appended after it is not silently running against
|
||||
an empty device.
|
||||
|
||||
Two things a reload does NOT clear on its own, both handled in that block
|
||||
because it is the first one on the mock leg. The webview's storage comes
|
||||
back with the page, and the player writes the session out on `pagehide`,
|
||||
which a reload fires — so removing the key up front puts it straight back;
|
||||
the block adds a `pagehide` listener of its own, last and therefore run
|
||||
last, and a phone with nothing in it comes up with no now-playing bar. And
|
||||
the viewport emulation lives on the DevTools session rather than on the
|
||||
page, so the re-attach after a reload drops it: the walk keeps what it set
|
||||
and hands it back, or every still from there on would be the Chrome
|
||||
window's own size at 1×.
|
||||
|
||||
Two commands the phone can reach had no mock at all until this round —
|
||||
`player_control` and `open_external` — found by mapping every `api.js`
|
||||
wrapper used under `src/mobile` against the mock (126 wrappers, 124
|
||||
mocked). Unmocked they fall through to the real IPC, which on a `file://`
|
||||
page rejects: a console warning and a toast, in whatever step happens to
|
||||
touch them first. The transport now answers the way the real core does,
|
||||
with an event; `resume` cannot restart the position ticks, so the state is
|
||||
right and the position stands still.
|
||||
|
||||
## Settings that reach the config
|
||||
|
||||
`set_settings` takes **camelCase** arguments (Tauri's rule for command
|
||||
@@ -994,12 +1107,16 @@ of theirs streamed → a buddy's sheet → You (every switch and chip saved
|
||||
and read back from the config) → recent searches and a spelling
|
||||
suggestion → the sleep timer → the phone turned sideways (the player's three faces, a
|
||||
record with a long sheet over it, an artist page, and the side insets faked,
|
||||
all at 915×411) and back, asserts what each step should show — the Home
|
||||
all at 915×411) and back → and last, on a reload of its own, a phone that
|
||||
has just been installed (the first-run sheet and its two cards, its
|
||||
Soulseek door, the notes on Home, on both Library tabs and on Search, the
|
||||
preview from You, a folder picked from the tour and the note losing the row
|
||||
it answered), then back to the provisioned device, asserts what each step should show — the Home
|
||||
step holds the launch to a budget as well, reading off `__ss.boot` how many
|
||||
commands the app issued before its first frame, how many of those left in the
|
||||
shell's own turn, how long it took, and requiring round 12's art prewarm to
|
||||
still be absent from them — screenshots every step into `tests/mobile/out/`, and fails on
|
||||
any exception or console error. The block at the end of the mock walk is the
|
||||
any exception or console error. Near the end of the mock walk is the
|
||||
weather: a library index that will not read and its Try again, the client taken
|
||||
offline, a refused password and a server that cannot be reached, Search with
|
||||
nowhere to search, a room list that never arrives, and a rescan that refuses
|
||||
@@ -1100,3 +1217,15 @@ symptom.
|
||||
- **Per library folder.** Follows and the play history live in the library
|
||||
folder, so the phone's own folder starts empty of both; they arrive with a
|
||||
shared library.
|
||||
- **A first run that restores rather than starts.** The tour covers the two
|
||||
things the app cannot work without (a folder, an account) and stops
|
||||
there. The desktop's own tour gates on an alpha key, which has nothing to
|
||||
unlock here — the APK is side-loaded and there is no updater channel on
|
||||
the phone — and its Community step has no counterpart, because Freenet is
|
||||
not on the phone. The honest first run for Kyle's own phone is neither:
|
||||
it is "point me at the desktop's library folder and read its follows, its
|
||||
playlists and its play history", which works only when that folder is
|
||||
reachable from the phone's storage. That is a sync door, not a setup
|
||||
step. Making an account from inside the app is possible — Soulseek makes
|
||||
one from the first sign-in with a new name — but it would mean handling a
|
||||
password, which the tour deliberately refuses.
|
||||
Reference in new issue
Block a user