1f9b30c51f1a0c22fffc701cbef61a4a2e35746b
23
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1f9b30c51f |
android: the page ends at the clock — the bars go, the hero keeps the top edge
The bar was anonymous until you scrolled, then faded in carrying the title the page already had in 27px type two centimetres below it. Kyle's rule for what replaces it: the page is cut off where it meets the Android status bar and dissolves there, and — the part that decides the whole shape of this — **backgrounds may pass under the clock, words may not**. A mask on the scroller cannot tell a photograph from a sentence. It takes both. So the artist hero is painted twice: the page's own copy inside the masked scroller, cut away in that band with everything else, and a second copy behind the scroller (heroart.svelte.js), unmasked, showing through exactly where the mask took the first one away. Same image, same box, same focal point, same scale, offset by the same scroll — one picture to the eye, and only the name dissolves. The second copy carries `overflow: hidden` for the same reason .hero does: without it the 1.04 scale renders 268px in a 258px box and five pixels of photograph appear under the hero's foot, below the black the scrim fades to. That also retires an attempt that did not survive contact. The mask first stepped aside for the hero on a scroll-driven `--fade-on`, which meant rebuilding a mask-image on the scroller every frame; measured at 60 Hz median and p90 either way, but it is work for nothing, and the flip that replaced it put the artist's name out of existence in one step. With the photograph protected by being OUTSIDE the mask, neither is needed: the mask is a constant and nothing recomputes it. No page has a bar now. A tab page's ⋯ — which is the app's own menu, the queue and the sleep timer and Settings — moved beside that page's own large title in `.phead`; Search and You grew the row they did not have, Community kept the one it had. A pushed page floats a back chevron and a ⋯ over the page instead, on flat discs with no backdrop blur, because the bar's own note records what three blurred bands did to a 60 Hz scroll on the Pixel 6a and these would have been a fourth. Three numbers now come from one place. `--chrome-d` and `--chrome-gap` are the disc and its margin; `.screen.has-chrome` reserves exactly that much at the top of a pushed page, and `--fade-top` reaches exactly the chrome's foot. They were independent for one build and it showed immediately: the page began underneath its own buttons, so "Transfers" sat behind the back chevron before you had scrolled at all, and the fade stopped 17px short of the discs so they stood on crisp text and a record's floating ⋯ landed on a track row's own ⋯. The walk followed. Nineteen steps reached a page's overflow through `.gbar .iconbtn`, which now matches nothing: the four on a tab page go to `.phead .iconbtn.muted`, the rest to `.fchrome.more`, and the album's back chevron to `.fchrome.back`. The `title` field read the bar's own text, which is a worse source than it looks — it only existed when chrome happened to be drawn. The screen carries its meta as data attributes now and the three probes read those, spelled exactly as `.gb-t`'s textContent was (the title, then the kicker when it differs) so every expectation written against it still holds. Checked on the device rather than argued: no `.gbar` and no `.gb-t` anywhere, `.phead .iconbtn.muted` present on all four tab pages and opening "SoulStream · Queue · Sleep timer · Settings", both `.fchrome` controls present on a record, and the title probe answering "Random Access MemoriesDaft Punk". One thing this leaves, and it is a real cost rather than an oversight: a tab page's ⋯ scrolls away with its header. The bar used to carry it the whole way down, so on Home the sleep timer now wants a scroll back to the top. |
||
|
|
cdb861fcbd |
android: no bar arrives over the page — the tab pages lose their top banner
The glass bar was anonymous until you scrolled, then faded in carrying the title the page already had in 27px type two centimetres below it. On Home that meant "Good morning" over "Good morning", half-transparent, while the content it covered kept moving underneath. Kyle's word: remove them. So a tab page has no bar at all. `barOn`/`titled` and the scroll trigger they read are gone; `showBar` is `!root`, and the element is not rendered on Home, Search, Library, Community or You. Nothing fades in over what you are reading. The bar was not only decoration, which is why this is more than a deletion: its ⋯ is the page overflow, and on a tab page that overflow is the app's own menu — the queue, the sleep timer, Settings. That moves to where the page's title already is, in `.phead`, beside Home's identicon and at the end of Library's existing row of actions. Search and You grow the row they did not have. Community already carried its own ⋯ and keeps it. A pushed page still has a bar, because it carries the back chevron and a title that is not repeated anywhere else on the screen. It is static now: rendered only when there is somewhere to go back to, on from the first frame and titled from the first frame. The opacity-0 default, the 180ms cross-fade, and the `.gb-t` rise the title used to arrive on all come out of the stylesheet — nothing is left that can catch the eye mid-scroll. The walk reached that overflow through `.gbar .iconbtn` on a tab page, which is now a selector that matches nothing there: `home-more` on both step lists and the two sleep-timer steps go to `.phead .iconbtn.muted` instead. The other `.gbar` steps are untouched on purpose — the playlist export sheet is a PUSHED page and still has its bar, and `library-ambience` is the album page's back chevron, not an overflow. Checked on the device rather than argued: on Home `.gbar` is absent, the ⋯ is present, and tapping it opens the half sheet on "SoulStream · Queue · Sleep timer off · Settings"; on a pushed record the bar is there with its two buttons, its title, and a computed opacity of exactly 1. One thing this leaves: the bar was also what backed the OS status bar once a page scrolled under it. The clock and the battery now sit on the content. That is fine on the ground and on dark artwork, and it will not be over a white sleeve — a short scrim with no title and no buttons would fix it without bringing a banner back, and is deliberately not in this commit. |
||
|
|
33f32cb4c9 |
android: the long session — what the app is still holding after an hour
Every walk this project has ever run is minutes long, and nothing in the port
had been looked at for a leak. Driving the real bundle lap after lap says two
useful things. The first is that the obvious answer is wrong: the naive
add-minus-remove listener counter climbs about twelve a lap, and every one of
those is a `.screen` element that `{#key nav.seq}` threw away — count only
what is still attached to window, document or a connected element and it is
flat, lap for lap, and so are the intervals. The second is that exactly two
things grow without a limit. `art.svelte.js` kept every cover it had ever
fetched for the WebView's lifetime — one browse of a 114-cover library takes
it from 5 keys to 114 and gives nothing back, at roughly 63 KB of base64
each — and `pls.covers` is keyed `id:revision`, so every playlist edit
stranded a 360 px data URL under a key nothing could ever read again.
Both are bounded at the desktop's own numbers rather than at invented ones:
`ART_THUMB_CACHE_MAX` 512 and `ART_THUMB_CACHE_MAX_CHARS` 16 MiB out of
`state.svelte.js`, oldest first. Two rules the desktop does not need are
added, because its callers ask through `getArt(path)` while the phone's
templates index the map: the entry that just landed is never dropped, and
neither is one a connected `<img>` is painting — `Cover`'s `visible` action
re-arms only on a path prop change, so an evicted cover that is still mounted
would stay a disc for good. Built with the cap forced to 8 and scrolled
through eighteen screens of a 140-card Library grid, the map sat at 61 —
spared, every one of them, for being painted — and not one cover on screen
lost its image; at the shipped 512 a whole library walk runs the trim zero
times. The playlist covers need no policy at all: the new revision drops the
old ones, which are unreachable by construction.
The verification is the round. `artTally()` on the handle reports the map and
its side tables, which are module-private; seventeen probe fields carry that,
the catalog's ten session maps, the queue, the transfers and the DOM on every
step of both walks; and seven mock steps install a live listener and timer
registry in the page — by a step, not by `addScriptToEvaluateOnNewDocument`,
which the first-run block's reloads would throw away — drive two warm laps, a
baseline and nine more, assert the census, and take the registry back out so
nothing appended after this block inherits a patched platform. Installed that
late it can only see what is registered from that moment on, so its absolute
counts are small and are not asserted as a census; what is asserted is the
delta across the nine laps, and that it fires was proved by planting a
listener and an interval stranded once a lap: `grewEars` 0 → 9, `grewTicks`
0 → 9, and the step fails. As it ships, over those nine laps, the listeners
and the intervals moved by 0, the art map grew 17 against a headroom of 80
and the catalog 5 against 24, and the DOM ended smaller than it started.
`tallyTimers` is a ceiling and not a delta, and says so: every track start
schedules a 47-second recents refresh, so a walk this fast keeps a handful
pending, and making it sharp would cost a fifty-second quiet period. The
catalog maps, `tracksCache` and that recents timeout are named in ANDROID.md
as left alone on purpose — evicting a catalog entry is not safe while
`Artist.svelte` looks one up inside `untrack`. Nothing new on the phone leg:
the registry is never installed there, those three fields read −1, and
`artTally()` is a pure read.
|
||
|
|
537bfdd5af |
android: the faces the walk had never seen — six failure states, read back in their own words
Round thirty-one gave the phone honest error states and shipped six of them unobserved, because neither `loadHome` nor `loadLyrics` was reachable from the `__ss` handle and both answer a second ask with nothing. The handle carries the two stores and their two loaders now, and a block of mock steps arms each failure with the mock's own weather, drives the face, and asserts the sentence a person reads as well as the absence of the Rust string behind it: Home's catalog line, the Gaps line and its second tap, a catalog record's stub, the artist page's two different answers, a playlist that would not read, the lyrics lookup that failed, a refused transfer, and the library toast's Try again, pressed for the first time. It sits before the first-run block, which reloads and empties the mock's follows; every failure is disarmed by the step that proves the face heals, and a last step asserts nothing was left armed. The phone walk gets a safe step of its own — a terminal transfer row written into the store and taken away, nothing fetched and nothing reaching the shade. ANDROID.md now names what the walk still cannot see: three screens that draw a raw backend string, six error faces with no expectation, and the history mirror printed on every step and asserted on none. |
||
|
|
f2cde4ec7c |
android: the whole discography, on a page of its own
The artist page already filed every release the desktop's way, but it only ever showed one category at a time and it showed all of it: no slice anywhere in that grid, so an artist with 140 releases drew 140 cards. There is a page now — All in front of the categories, a sort, a grouping by year, and the editions opened in place under their own record, which is the part a flat list loses — and the grid behind it stops at twelve with the way down under it. It is the same classifier twice over the same cached albums, which the desktop cannot do because its filing waits on a background description harvest; the three things its own page has and this one does not — a tracklist per record, an accent sampled per cover, the scroll-led banner — are 140 calls and 140 decodes over mobile data for rows that are one tap away, and ANDROID.md says so rather than leaving them looking forgotten. The list caps at sixty with the rest a button away, before the grouping, so the last year on screen can be a part of itself; under a name order By year is not offered, because the years would repeat out of sequence. The mock gains a 140-record artist so both caps are walked rather than argued about. |
||
|
|
defd8c5e6d |
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. |
||
|
|
95c2bfeeba |
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. |
||
|
|
01bab007f7 |
android: the boot chain goes out in one round, and a census that holds it there
Thirty rounds of onMount had never been read end to end, and read end to end it was a queue: two dozen commands with a critical path ten deep, because Tauri's `listen` is an invoke like any other. loadLibrary asked for five independent SQLite reads one behind the other, bindSlsk registered eight listeners one behind the other, and watchLibrary and the player's bindEvents did two each. None of them depended on anything before them. They go out in one round now — the same commands, critical path 10 to 3, and ten of them used to leave in the shell's own first turn where twenty-three do now — and in a rig that runs the real store modules with the bridge instrumented, lib.loaded lands at 53 ms instead of 85 at a 6 ms round trip and 62 instead of 148 at 20 ms. Those are a desktop rig's milliseconds with the round trip swept rather than measured; the phone's own numbers are unproven until the walk prints bootMs and bootLibMs. The behaviour is meant to be identical. loadLibrary's only movable part was lib.error, which has two writers and used to be decided by the order they were awaited in; the two messages are now carried out of the round and applied in that same order, so a failing library still shows the library's message. Every combination of which of the five reads fails, over six latency shapes, comes out identical to the old code. bindSlsk still does not return until every handler is up, so connectStored cannot let a fast connected state land with nobody listening, and bindEvents still hands back the transport's unlisten and still rejects only when the transport is the one that failed. src/lib/boot.js is the project's first launch instrument — one counter every invoke and every listen passes, sealed at the first frame the shell paints and never re-opened, so a probe landing a second and a half later still reads the launch. perf.mjs measures frames; nothing measured the launch. The mock walk's Home step now holds a budget against it and both walks print bootCalls, bootSync, bootMs and bootLibMs on every step. bootSync is the clause with no clock in it: a call that waits on the one before it can only issue its first in the shell's own turn, so a chain put back in a queue falls under the floor whatever the machine is doing. A bootCalls of -2 means __ss.boot is not there at all, which is the census itself being broken rather than the app. One import line goes with it: Sheet.svelte's `import * as api`, the only one of the ten dead imports this round turned up whose reason is the bundle at all. Rollup already drops an unreferenced NAMED import, so a dead name on an import line is hygiene rather than bytes; a namespace import is the one shape tree-shaking cannot see through, and this one was dead. It frees nothing on its own — twenty-nine other files in the phone's graph hold the same module the same way, so api.js is pinned into the chunk whatever Sheet.svelte does — but it is the only one of the ten that belongs to a round about weight. The nine that are drift go out with the sweep in the round after this one. Two things were looked at and left alone, both written down in ANDROID.md rather than half-done. Home's per-artist catalog walk is the deepest chain in the app, and that serial walk is the Deezer and MusicBrainz pacing, not an oversight. And the follows really are read twice at launch, but the obvious guard does not work: Home's effect calls loadHome with force the first time they land, so the guard would never fire. One small local read, left for a round that can do it properly. |
||
|
|
052e15302b |
android: what the phone says when things go wrong
Fifty-nine places put a raw Rust string into a field a person reads, and the red toast drew it verbatim — a phone in a lift showed "could not reach server: io: failed to lookup address information" under the password box. One translator, src/mobile/trouble.js, called where an error is DRAWN rather than where it is caught: the stores keep what Rust said and only the sentence on screen is ours, so every sink is fixed at once. Five screens that drew a failed read as an empty answer stop lying — Tracks no longer reports a broken database as an empty library, a playlist that could not be read is not an empty playlist, the lyrics face says a lookup failed instead of "no lyrics" and finally carries the ⋯ that reaches Paste lyrics, Gaps says why it could not reach the catalogue, and Search signed out says so where it used to render nothing at all. The room list, which is fire-and-forget over the wire, gets a twelve-second end and a Try again. rescan_library ran prune_missing() first, which deletes every track row whose file does not exist — so one tap on You → Rescan library with the folder unreachable emptied the whole index and returned Ok(()). It now makes the root if it is merely not there yet and refuses if it cannot be read, and You draws the path with what to do about it. The mock grew a weather system so the walk can drive all of this: fail, answer, offline and clear, checked before dispatch so a plugin command arms like any other. Nine mock steps and one on the phone — the first steps in thirty rounds that ever made a command reject or saw the client signed out — and the last of them puts the weather away and leaves the app as it found it. |
||
|
|
44eda8dff5 |
android: the room can measure the song, and a cold artist page stops waiting on one call
The tap-to-sync room draws the recording's own vocal band (lyrics_vocal_activity, the desktop's coarse envelope) as a strip under the head, and one button lays every line across the stretches it says are sung — the desktop's own generator, shared and unit-tested, which refuses rather than guesses when there is too little voice to read. The result is labelled an estimate everywhere it shows, the cursor goes back to the top so the taps correct it, and one more tap drops the whole thing back to an empty set, because a rough fill stamps every line at once and leaves Undo nothing to take back. The artist page now fires the hero seed and the overview beside the full page, the way the desktop stages them: a hero Kyle picked on this phone comes back off the file with no network at all, the discography paints from the overview's first page until the exhaustive pass lands with the classified one, and both lanes are dropped with the page when a hero changes. ANDROID.md's "Not done yet" is rewritten as the true map of the port — what is absent by design and why, what is waiting on Rust rather than on a file picker, and one bullet about the artwork viewer that had been done for rounds. |
||
|
|
5738c2d7bf |
android: the downloads say so in the shade
A Save still running with the app in the background now posts a progress notice — how many files, the one it is on, a bar, Stop all and Open — and takes it down when the queue empties. It sits on its own low-importance channel so it can be silenced without silencing the player's card, and rides its own dataSync foreground service rather than the media one: a mediaPlayback service stops itself when the bar empties, a download is not playback, and nothing else was keeping the process alive for a Save with nothing playing — which is how the first real Save died, four tracks in. Stop all goes through the same cancel(id) a row's tap takes, and Transfers grew the matching button beside Clear finished. The walk pushes two rows straight into the transfer store and reads the real notification back out of `dumpsys notification`, without fetching anything. |
||
|
|
4fc6e9c630 |
android: what a screen reader says — names, roles, and the sheets out of the tree when they are down
Every control on the phone has a name now, in the app's own words, and the desktop's word where it already has one; the nav is a tab set whose badge count is part of the tab's name — "You, 3 transfers" rather than a stray 3 read out before it. The two sheets are dialogs with somewhere sensible for the focus to land, and the half sheet's grab handle is finally a real Close — the first dismissal a screen reader could reach, because the scrim never was one. A sheet that is down is inert rather than sitting off-screen in the accessibility tree with the hidden mini bar and the nav under it, and one live region is mounted for the life of the app so that an undo offer is actually announced; switches say on or off, chips say pressed, and the waveform is a slider where it can seek. Nothing visible changed. The walk counts the controls with no name, with a junk name, in a hidden layer, or with unsaid state — all four held at zero on ten screens, and the nameless count on a room and on both prompts besides, which is where the six fields named here actually live. |
||
|
|
aff29aba5c |
android: a streamed file copied into a folder, the listening port, the dev build's diagnostics
The now-playing sheet's streamed track can be copied into a folder picked on the phone, as the file it is — the cache written out, nothing downloaded again, no library row (copy_stream_to_folder, which the desktop has had a command for and no UI). You → Sharing carries the listening port, what peers dial when they download from you: unset is the client's own 50300, the app asks the router to forward it over UPnP and falls back to the server relaying, and a change applies on the next connect. The dev flavour gets the desktop's debug overlay as a screen under This phone — CPU, memory, the network, the stream and its loudness, what the pre-buffer is fetching, the size of the play history — behind devBuild in util.js, which is Vite's own DEV flag, so the embedded build the phone keeps has no row to tap. |
||
|
|
c737809325 |
android: a folder of audio files, and a paste of track links
The paste door now reads itself the way the desktop's does: one track link in the paste and every line is looked up on its own public page (playlist_import_track_links), which is the way past Spotify's 100-track public ceiling — the wait screen counts them as they arrive. A fourth door imports a folder of audio files off this phone, picked with the storage plugin's own document-tree picker; playlist_import_folder grew a path argument for it, because on Android it had no dialog to open and always answered nothing. A YouTube playlist link needed no door at all — the link one already dispatches it. |
||
|
|
24c6857847 |
android: tap-to-sync, the per-track auto-sync switch, the vocal-reduced copy
The editor the desktop drives with a mouse is better under a thumb: the song plays, the line to time sits large in the middle of the screen with the one behind and the one ahead faint around it, and a tap anywhere stamps it. One layer the back gesture peels, and a set left mid-tap is offered back by the row that opened it; Save writes the track's own document, lines never tapped left out of it and the button saying how many are kept. The lyrics sheet also gains the per-track auto-sync switch and the desktop's reduced-vocals copy — the same title, artist and duration, so the words keep following it, and one row puts the original back. |
||
|
|
7eae0ee945 |
android: a name of your own for anyone on the network
The desktop's local contact metadata on the phone: a peer's sheet ends in Name and notes — a petname, a group, a private note and the Favorite and Trusted marks, kept in the app's own config and published nowhere. The petname stands in for the username on the buddy list, in the chat list, on a thread's bar, in the peer sheet, in a room's roster and on a room line, and the real username stays with it — on the line beneath, or beside the name in a room — so a stranger is never quietly turned into someone you know. Favourites sit at the top of the buddy list, and clearing a person's details drops the entry rather than storing an empty one — the backend's own rule. |
||
|
|
f792d7955d |
android: a peer in full — their card, their interests, and what they have that you don't
The peer sheet's new "About this user" row opens a screen of its own: the card their client serves (their own words, their picture, the upload slot and the queue), the likes and hates they declared, and the comparison this phone makes of their share against the library. The comparison is the point of opening a stranger — every record they have by an artist in your library that you don't, each row opening the search that finds it, and a way to forget the whole comparison again. Nothing is uploaded to work it out. Their picture opens in the artwork viewer, which now takes a data: key beside a library path and an https URL. |
||
|
|
694a29eb0d |
android: downloads on Wi-Fi only
One switch the desktop never needed, off by default: on mobile data every Save the phone can start — a record, a track, a folder, a file, a playlist's bulk, the now-playing Save — waits, with the reason on the bar; streaming is never held back. The walk flips the switch, makes the connection cellular and sees a download refused. |
||
|
|
59be9f6c3e |
android: the nav's badges — transfers in flight on You, unread chats on Community
The desktop's title-bar download ring and its unread indicator, on the phone's nav: a count on You while Save is fetching, a count on Community when a chat has unread lines. |
||
|
|
991d52d0cc |
android: the library's file stats on the cards, the artist's own plays, the thumbnail prewarm
The desktop Library's per-record file stats on the phone (library_album_stats, one call for the whole library): FLAC 24/96 or MP3 320k badges on the cards, the size on the list rows and the record page. The artist page's own listening (artist_play_stats): the time played as a chip, the most played track under "On this phone". The whole-library thumbnail warm a moment after boot, at the cards' edge, one decode at a time behind the tiles' gate. |
||
|
|
26928b706d |
android: the repair sheet, browse, every long-press sheet, You's last sections, the lyrics tools — and the walk's audio check made honest
The playlist repair (Find a local match: the library searched by the row's name, a pick relinks it through playlist_relink_item; an unknown row takes the track's identity). A peer's sheet on the raw results (play or save the folder, browse their files, message, buddy) and the Browse screen — the hub's Browse tab: a user's whole share with sizes and totals, a folder played or saved whole; a share can hold thousands of folders, so it filters and renders 80 at a time. "Look up a user" on Community → Buddies. The sheets the phone lacked: a queue row's (Play now, Move to next, Remove from queue), a catalog song's, a catalog card's and the record page's own, a raw file's (in the results and in Browse), a room name's, a buddy's, the now-playing Save (the desktop's Save menu for a streamed track or record). The artwork viewer with pinch zoom, one layer the back gesture peels. Search's recent searches and its spelling suggestion from the names this phone knows. You → Streaming, Downloads, Sharing, Lyrics, Album artwork, and Clear play history; a record's Re-fetch the cover. The lyrics tools on the sheet's face: the offset nudged and kept per track, another LRCLIB candidate chosen, the lookup forgotten and done again (lyrics.svelte.js, shared by the face and the lyrics sheet). The harness: every CDP call times out and the reload step re-attaches (a WebView that stopped answering hung a walk for ten minutes); the audio check counts SoulStream's own tracks' underruns from the mixer dump instead of the thread-wide counters that any app's notification moves — the afternoon's "dropouts" were other apps' sounds, and the browse of a 49,544-file share measured at zero; __settle resets every tab to its root through the nav; an adb step kind; the walks cover all of the above on the mock and on the phone. |
||
|
|
6e39dc43f3 |
android: playlists, the import doors, the share sheet, You → Sources — and three bugs the walks found
The desktop's two playlist pages on the phone: the shelf (pinned first, mosaic covers, badges, New playlist, Import) and the page (kind, Pinned chip, description, the meta line's warnings, Play / Shuffle / more, a filter, rows tagged unresolved / set audio / file missing). Sheets for a card, the page's overflow, a row, pick-a-playlist, a prompt and a confirm; Add to playlist on every track and record sheet; a removal undoable for seven seconds from the neutral toast (notice.svelte.js); stream rows play through the resolver as path-less entries, a set slice starts and ends at its cues; Add to library / Download all tracks with the desktop's bulk rules; nav.replace for a review that became the thing it made. The import doors that need no file — a link (YouTube · Spotify · Apple Music · TIDAL · Deezer), a pasted tracklist, a YouTube DJ set — with the review screen and the desktop's catalog verification pass. A share plugin crate (crates/tauri-plugin-share): Share → SoulStream from any app keeps the text in a ShareActivity and the WebView hands it to the import when it comes back to the front. You → Sources: the source-quality window and the priority per mode. Found by the walks and fixed: set_settings takes camelCase arguments, so the phone's normalization and library-folder saves never landed; Cover.svelte's non-eager wrapper has no layout box, so its intersection observer never fired and row thumbnails never loaded (the Artists tab too); a history push while a history.go was still in flight left the nav mirror one entry ahead and the page blank after a confirmed delete — the mirror now applies once per microtask and never pushes during a pending traversal. The harness names unmocked commands, carries console warnings' frames (the smoke bundle is built unminified), probes the history mirror on every step, mocks app_status / play_local / waveform / lyrics_get / player_state and applies settings patches, runs adb steps on the phone (the share intent), and walks playlists, the imports, the share and the settings on both the mock and the phone. ANDROID.md documents all of it. |
||
|
|
bb828ff2dc |
android: the phone, on the real core — the mockup shell, playback, the media card, the catalog, the resolver, Save, the dev flavour
The same Tauri 2 project builds for Android: mobile.html as a third Vite
entry (src/mobile/, the mockup on real data), tauri.android.conf.json, the
mobile stubs (updater, lyric alignment, vocal separation), rustls reqwest
on Android, the AAudio buffer, and a hand-driven detour build
(src-tauri/android/{env,build,deploy,dev}.sh) while Developer Mode is off.
ANDROID.md is the recipe.
Playback: the media-session plugin crate (lock-screen card, MediaStyle
notification, audio focus, headset buttons, a foreground service that
promotes itself first — Android 14 kills the app otherwise), session
restore across reload and restart, solid chrome after measuring the
backdrop-filter lag.
The catalog: Search is the desktop's top bar (the top-result rule, artist
cards), the artist page and the record page are the desktop's, Home is the
desktop's landing page, Gaps per followed shelf. Playing goes through the
desktop's own source rules, moved verbatim from state.svelte.js into
src/lib/sources.js so both apps share one resolver; the phone readies the
next three tracks and crosses boundaries gapless. Save fetches a record or
a track into the library by the download rules; Transfers shows the
burst. The raw Soulseek search stays, offered on request, a peer's copy
streaming with the lane rising as the bytes arrive. Community is the
Soulseek side (rooms, chat, buddies); the private layer waits.
The opener and storage plugins (social links, the picked library folder,
all-files access), the volume normalization switch, and the harness:
tests/mobile/smoke.mjs walks the mock (a fake network behind
src/lib/events.js) and the real phone over CDP, perf.mjs measures frames.
|