Conversation
main only advances via a develop merge at release time; PRs/branches should target develop, never main directly.
Builds out the app described in docs/implementation-plan.md: the six-module split, the ZeroMQ transport, a Room-backed library cache, the Compose UI, the media session, launcher shortcuts and multi-box support. Targets Android 16 (API 36), which Play requires for new apps from 31 August 2026. Two protocol corrections against the upstream Python source, which both the plan and the web UI's command table got wrong: shuffle and repeat take an 'option' string rather than MPD flags, mute sets an absolute state, and get_folder_content's relpath is the path play_folder accepts. The four translations are unreviewed drafts and are marked as such in their files. Nothing has yet run against a real box. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Store listing text and release notes now live under fastlane/metadata/android/<locale>/, replacing the docs/whatsnew/ convention. Release notes are keyed by versionCode rather than versionName, which is how fastlane and the Play API identify them. Adds tools/check_store_metadata.sh so the per-locale character limits live in one place, enforced by both /release and google-play.yml. A missing or over-limit locale now fails the deploy instead of shipping a blank What's New — Play has no English fallback for store text. The script counts characters as bytes minus UTF-8 continuation bytes, because wc -m silently counts bytes when the shell locale is not a UTF-8 one, as in Git Bash on Windows. google-play.yml now checks out the tag being deployed rather than the default branch, so the versionCode it reads matches the AAB, and it aborts when build.gradle.kts disagrees with the tag. The four non-English locales are unreviewed drafts and still need a fluent-speaker pass before a release ships. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Running against real hardware found three wrong assumptions, all of them shared by the plan and docs/protocol-notes.md: - as_thread returns the Thread object rather than the call's result, so every library and cover request came back unusable. It is fire-and-forget only, and is no longer sent at all. - There is no core RPC package; core.* are published topics. Asking for core.version answered with an error, which made both the connection test and the watchdog ping fail against a healthy box. Both now ping player.ctrl.playerstatus. - Cover art takes two requests: the first only starts the extraction and answers CACHE_PENDING, which was mistaken for a file name. Fixing as_thread then exposed a design fault. Every screen combines the player state with coverUrl, and combine emits nothing until all of its inputs have emitted once, so a cover lookup that was slow or restarting froze the title, progress and controls with it. coverUrl is now a StateFlow filled in by a background resolver. Also: the volume slider sent a command per frame of a drag, and the album list was only ever fetched when the box happened to be idle at app start. The spike, the Python probe and the protocol notes carried the same wrong assumptions and would have re-taught them, so they are corrected too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The player's star gave no clue whether it saved the track, the folder or the album, and the only way to favourite something in the library was a long press nobody discovers. Favourite actions now name their target: a tap on the star saves the folder, a long press offers folder and track as separate entries, each showing whether it is already saved. Every library row and album cell gains an overflow menu with Play, save as favourite, and a details panel built strictly from cached data, so opening it never puts a request on the socket the box shares with its card reader. Single tracks become favouritable in their own right, which the plan deliberately ruled out. That costs a TRACK type and a trackUrl column (Room schema 2, migrated rather than dropped), a track variant of the coil://play deep link, and settings backup format 2. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
How deep the user is in the folder tree is view model state, not a navigation destination, so the system back gesture never saw it and popped straight out to the player from three levels down. Back now does what the breadcrumb's up arrow does, and leaves the library only from the top level. Picking the album tab is in-library navigation too, so back there returns to the folder tab. Both handlers register before the content they guard, which leaves an open details sheet taking back first and closing itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Multi-box was built but unreachable. "Add box" showed in settings only while no box existed, and the switcher holding the only other entry opens from the top bar only once there are two boxes, so one box was a dead end in both directions -- the feature reads as missing rather than hidden. Settings now always offers "Add another box", and lists the configured boxes as a picker once there are two or more, mirroring the switcher because settings is where boxes are configured. The collapsed top bar for a single box (7.5) is kept, and adding a box still does not make it active: nothing should tear down a live connection unasked. Switching box also left the library on the previous box's folder path and fetched it from the new one, where it need not exist. It returns to the root instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The coil://play deep link already worked from outside the app -- the activity answering it is exported, and home screen shortcuts are just this URI wrapped in a ShortcutInfoCompat -- but there was no way to obtain one: the box id it carries is a UUID shown nowhere. So the capability read as missing. Copy link and Share link now sit on a favourite's menu, hidden for a row that cannot be played so the menu never offers a dead link. The copied confirmation is suppressed on Android 13 and up, which confirms a clipboard write itself. AGENTS.md documents the URI format, that it is a public surface, why the box travels in the link, and that hand-written links would call for a host= form rather than a second id scheme. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Phoniebox protocol has no search command and list_all_dirs is off limits, so search runs against the Room cache: instant, free, and working with the box switched off. Matching goes through a folded column rather than LIKE on the raw text. SQLite's LIKE, lower() and COLLATE NOCASE are case-insensitive for ASCII only, so "bar" would never find "Bar" with an umlaut -- and the launch locales are full of them. SearchText folds both the stored text and the query the same way; schema 3 adds the columns and clears the cached library rows instead of backfilling, since accent folding cannot be expressed in SQL and that cache is the one disposable thing here. What is searchable is what has been fetched, so the empty state explains that rather than implying an empty library, and indexLibrary walks the whole tree to close the gap. It is opt-in, pauses between levels, will not start while the box is playing, and reports hitting its own cap -- unattended crawling of the shared RPC socket is what section 6 rules out. The three copies of details-sheet wiring collapse into one composable on the way past, which is what lets search reuse it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The box has four timers and three of them switch it off, which Coil promises never to do -- a delayed shutdown is still a shutdown, and on an unauthenticated LAN port it is one anything on the network could schedule. Only timer_stop_player is addressable: Commands.timer is hard-wired to that plugin, the SUB socket names that one topic rather than the timers. family, and a test asserts no command mentions shutdown, host. or reboot. Two details from the box's source that would otherwise have failed silently. start on a running timer is ignored, so setting 30 minutes over a running 60 would keep the 60 -- the repository cancels first. And state is published on change only, with no per-second tick, so the sheet asks get_state once when it opens and the countdown is interpolated locally, as the progress bar already does. Shuffle and repeat move into one playback options menu with the timer: three mode toggles flanking the play button is more than the screen carries. A running timer also shows the time left under the transport row, since a countdown to silence should not be hidden behind a tap. This reverses the "no timers" scope line, so AGENTS.md, the README and the protocol notes now say which timer is in and why the rest stay out. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nothing above the view models was covered, and the only way to see a change to the UI — or to the four unreviewed translations — was to put the app on a phone. Robolectric with native graphics rather than an emulator, because of the clock: this UI interpolates elapsed time, counts a sleep timer down and debounces the search field, and a paused looper is what makes the same picture come out twice. Roborazzi writes and compares the files. A plain `./gradlew test` neither records nor fails on a golden, so outside CI the suite costs only the guarantee that every screen still composes. Three levels of golden: the whole CoilApp scaffold on three device profiles, so a picture includes the top bar, the navigation and the mini player the way the app does; the player and library screens alone, for states that are tedious to reach through the app; and the chrome components, where connected and degraded differ by one coloured dot. Repositories are faked through Hilt, so a run opens no socket, no database and no DataStore file, and cover art resolves to a flat colour rather than the box's HTTP cache. Goldens are recorded on the Linux runner by screenshots.yml, since text rendering differs between hosts far enough that a file recorded on Windows can fail CI on antialiasing alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…he app [record-screenshots] The listing had no images at all and the landing page had nothing to show. Both now come out of the screenshot harness, so neither can drift away from what the app actually looks like. Not the goldens themselves, though, and the reasons are hard ones: Play takes 24-bit PNG with no alpha, and caps the long side at twice the short one. Roborazzi writes RGBA, and the goldens' phone frame is 2.23:1 — rejected on both counts. Beyond that, a golden's job is to change when the UI changes, and a store asset's is to be stable and curated; wiring the listing to a baseline would mean accepting a re-record silently rewrote the shop window. So StoreAssetTest renders its own frames (phone at 1.995:1, as tall as Play allows; 7-inch and 10-inch tablets), from StoreFixtures content chosen to read at thumbnail size rather than to stress the layout. The alpha channel is flattened afterwards by a Gradle task, since java.awt is not on an Android unit test's classpath, and the phone set is copied into the Pages site from there. check_store_metadata.sh now validates images beside the text — format, alpha, dimensions, aspect ratio, file size and count — reading the PNG header with od so it still needs nothing installed. google-play.yml already runs that script, so an image Play would refuse now fails the deploy instead of the upload. screenshots.yml also takes a commit-message trigger. workflow_dispatch only works once a workflow has reached the default branch, which for this repo means after a release; [record-screenshots] in a push to develop does the same job today, and keeps re-recording an explicit act. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
[record-screenshots] Many families run this on the family tablet, and there the player was the one screen that did not work: a full-width square cover is taller than a landscape window, so the transport controls — the entire job of that screen — sat below the fold. The tablet golden had been showing exactly this since it was recorded. On a window wider than it is tall, or wider than Material's expanded breakpoint, the cover now sits beside the title and controls. A 7-inch tablet held upright stays in one column, where two panes would only be two cramped ones. The same rule fixes a phone held sideways, which has width to spare and no height at all, and the single-column path now caps the cover against the window height rather than only the width, so a short window keeps its controls either way. Progress, transport, the timer line and volume are one composable used by both layouts. A control that appeared on a phone but not on a tablet would be a bug nobody notices for months. The other screens are unchanged: stretched single columns read as roomy rather than broken. Also pins Robolectric to SDK 35. Its SDK 36 image needs a newer JDK than the Temurin 17 every workflow pins, so the screenshot tests ran on a developer machine and threw on the runner — which is where the goldens are recorded, so it took the whole recording job down with it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
[record-screenshots] The script is checked in 0644, like every tool here, so invoking it directly failed the recording job with "Permission denied" after the goldens had already been rendered. google-play.yml and /release both call it through bash; this was the one caller that did not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The suite was built on the assumption that text rendering differs between hosts, and said so in three places. A recording run on the Linux runner against goldens recorded on Windows has now reported every file byte-identical, so the assumption was wrong for this project: Robolectric brings its own fonts and its own Skia. What does matter is the SDK level and the JDK major version, which is a separate note and already recorded next to the sdk=35 pin. screenshots.yml keeps its place as the way to accept a new look without a local toolchain; it is no longer described as the only trustworthy one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
[record-screenshots] Both tools carry a shebang and are meant to be run, so the mode should say so. Invoking them by path failed the recording job with exit 126 — a message that names permissions rather than the missing bit — and the workaround was to prefix `bash` at each call site, which every caller then had to remember. The mode is now 100755 and the four call sites drop the prefix: CI, google-play.yml, /release and the fastlane README. The script's header says to keep the mode, because `git add` on a new tool will not set it and the failure it produces does not point at the cause. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool. What Enabling Code Scanning Means:
For more information about GitHub Code Scanning, check out the documentation. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.