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
+57
-2
@@ -352,6 +352,23 @@ function mockImport(cmd, args) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/* The walk's other device: a phone that has just installed the app. The
|
||||
mock is the whole backend on that leg, so it can answer as one — and
|
||||
the flag has to survive `location.reload()`, which is why it rides in
|
||||
the URL fragment as well as in localStorage: a file:// document's
|
||||
storage is not something the harness can lean on, and a fragment
|
||||
always comes back. */
|
||||
const fresh = (() => {
|
||||
try {
|
||||
if (location.hash.indexOf("fresh") >= 0) return true;
|
||||
} catch { /* no document */ }
|
||||
try {
|
||||
return localStorage.getItem("mockFresh") === "1";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
const enabled = (() => {
|
||||
try {
|
||||
return localStorage.getItem("mockLibrary") === "1";
|
||||
@@ -367,6 +384,14 @@ export function mockInvoke(cmd, args) {
|
||||
command arm exactly the same way (see the doors below) */
|
||||
const forecast = weather(cmd);
|
||||
if (forecast) return forecast;
|
||||
/* a device that has just been installed: an empty database. Not an
|
||||
error and not a refusal — `pipeline::init` scans the sandbox root
|
||||
at startup and honestly finds nothing in it, so every one of these
|
||||
answers with an empty list. Everything else below answers exactly
|
||||
as it always did: this is the same mock with nothing in it yet. */
|
||||
if (fresh && (cmd === "library_albums" || cmd === "library_album_stats" || cmd === "recent_albums"
|
||||
|| cmd === "followed_artists" || cmd === "list_playlists" || cmd === "library_track_index"
|
||||
|| cmd === "library_track_search" || cmd === "contacts_meta_list" || cmd === "buddy_list")) return Promise.resolve([]);
|
||||
if (cmd === "app_status") return Promise.resolve({ version: "mock", connected_as: null });
|
||||
if (cmd === "library_albums") return Promise.resolve(albums);
|
||||
if (cmd === "library_album_stats") return Promise.resolve(albums.map(statsFor));
|
||||
@@ -740,7 +765,7 @@ function mockCatalog(cmd, args) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (cmd === "plugin:storage|pick_folder") return Promise.resolve({ uri: "content://mock/tree/primary%3AMusic", path: "C:\\mock\\Music" });
|
||||
if (cmd === "plugin:storage|all_files_access") return Promise.resolve({ granted: true });
|
||||
if (cmd === "plugin:storage|all_files_access") return Promise.resolve({ granted: !fresh });
|
||||
if (cmd === "plugin:storage|request_all_files_access") return Promise.resolve();
|
||||
if (cmd === "plugin:storage|pick_image") return Promise.resolve({ path: "C:\\mock\\photo.jpg" });
|
||||
/* the downloads notice: the mock walk cannot see the shade, so the calls
|
||||
@@ -762,6 +787,7 @@ let downloadSeq = 0;
|
||||
const mockSettings = {
|
||||
download_dir: "C:\\mock\\Library",
|
||||
volume_normalization: true,
|
||||
setup_done: true,
|
||||
library_layout: "{artist}\\{album}",
|
||||
shared_folders: [],
|
||||
upload_slots: 2,
|
||||
@@ -777,6 +803,18 @@ const mockSettings = {
|
||||
stream_source_prefs: { max: "any", min: "any", priority: "quality" },
|
||||
download_source_prefs: { max: "any", min: "any", priority: "quality" },
|
||||
};
|
||||
/* the fresh device's own config: the library root Android actually hands
|
||||
a fresh install (the app's private sandbox — real, writable, and
|
||||
invisible to every other app), nothing shared, and the flag the tour
|
||||
stamps still unwritten. Mutated rather than replaced, so `set_settings`
|
||||
keeps working and a folder picked during the walk genuinely changes
|
||||
what the notes read. */
|
||||
if (fresh) Object.assign(mockSettings, {
|
||||
download_dir: "/data/user/0/com.soulstream.app/files/Library",
|
||||
setup_done: false,
|
||||
share_files: 0,
|
||||
shared_folders: [],
|
||||
});
|
||||
const cacheReady = new Set(); // paths a preload or a finished stream has fully fetched
|
||||
const cacheSizes = new Map(); // path → the file's size, for stream_cache_len
|
||||
const preloading = new Set();
|
||||
@@ -786,7 +824,7 @@ let posTimer = null; // the fake engine's position ticks
|
||||
them — a declaration moves both, the way the real server does */
|
||||
const MY_INTERESTS = { likes: ["Daft Punk", "french house", "Warp Records"], hates: ["loudness war"] };
|
||||
function mockNet(cmd, args) {
|
||||
if (cmd === "get_credentials") return Promise.resolve({ username: MOCK_USER, has_password: true });
|
||||
if (cmd === "get_credentials") return Promise.resolve(fresh ? { username: "", has_password: false } : { username: MOCK_USER, has_password: true });
|
||||
if (cmd === "connect_stored" || cmd === "connect_and_login") {
|
||||
later(400, () => mockEmit("soulseek:state", { kind: "connected", username: MOCK_USER }));
|
||||
return Promise.resolve();
|
||||
@@ -908,6 +946,23 @@ function mockNet(cmd, args) {
|
||||
return new Promise((r) => setTimeout(() => r(pool.slice(0, Math.min(pool.length, n + 1)).map((username, i) => ({ username, rating: Math.max(1, n - i) }))), 300));
|
||||
}
|
||||
if (cmd === "player_state") return Promise.resolve({ status: "idle", position: 0 });
|
||||
/* Two the phone can reach that the mock never knew, found while
|
||||
mapping every api.js wrapper against it for this round: unmocked
|
||||
they fall through to the real IPC, which on a file:// page rejects —
|
||||
a console warning AND a toast, in a step that has nothing to do with
|
||||
either. The transport is answered the way the real core answers it,
|
||||
with an event and nothing else; `resume` cannot restart the position
|
||||
ticks (they live in the closure the play_* command opened), so the
|
||||
state is right and the position simply stands still. */
|
||||
if (cmd === "player_control") {
|
||||
const act = String(args?.action || "");
|
||||
if (act === "pause" || act === "stop") { clearInterval(posTimer); posTimer = null; }
|
||||
if (act === "pause") later(60, () => mockEmit("soulseek:player", { kind: "paused" }));
|
||||
if (act === "stop") later(60, () => mockEmit("soulseek:player", { kind: "stopped" }));
|
||||
if (act === "resume") later(60, () => mockEmit("soulseek:player", { kind: "playing" }));
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (cmd === "open_external") return Promise.resolve();
|
||||
if (cmd === "preload_local") return Promise.resolve();
|
||||
if (cmd === "waveform") {
|
||||
const n = args?.buckets || 900;
|
||||
|
||||
Reference in new issue
Block a user