spotify: a progress bar that works, and a tile that fits its size - #4
Conversation
Three things the widget got wrong. **The bar never moved outside an English locale.** `player position` is the one fractional value the state script asks for, and AppleScript writes numbers with the Mac's own decimal separator: on a French system it answers `69,724998`, which `Number()` reads as NaN. The width became `NaN%`, which a browser drops. AppleScript rounds the position to whole milliseconds now, and an integer has no separator to get wrong. **And it stepped once a second.** The position arrives on a one-second poll and the fill was drawn where the last answer put it. It is sent to where the track will be at the next answer and given that second to get there; each answer corrects the aim. A seek or a new track lands at once — running the fill backwards across a second reads as a fault. Scaled rather than resized, so the second belongs to the compositor. The bar is also thicker and stands clear of the buttons: at 4px under a 4px gap it read as an underline on the transport rather than a bar of its own. **The tile was a row whatever its shape.** A band about 150px high floated in the middle of anything taller while the cover stayed pinned to 40% of the width: on a 640×680 tile the content used 200 of 636 pixels. The cover, the song and the transport are three grid areas now, and only the areas move — row, header (from 440×340, the cover beside the song with the transport across the width below), or column when the tile is square or taller than wide. Two things there were worth measuring rather than guessing. The cover takes its height from the grid and its width from `aspect-ratio`, with the *same* cap on both sides — capping only the width left a portrait cover on every narrow tile, since a replaced element with an explicit height does not recompute it from a width that was clamped. And the type reads from whichever dimension the tile has to spare, held back by the other: sized on height alone a 950×280 tile carried a 21px title where it had room for thirty, and sized on width alone a 2560×120 banner would ask for 76px, which does not fit under it. `.info` is a real box in the row — which is what keeps the cover from pushing the song and the buttons apart when it is the taller of the two — and steps aside with `display:contents` in the other two, so its children become areas of their own. This is also what the `max-width:480px` rule in the file was written for, behind a width no tile could reach: the widget asked for sixteen cells, which is 640px. It asks for eight now, like every widget that is not a list of text, and the default size grows a row instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Thanks — read the whole thing, ran the three test files against the branch (41/41), and ran your new Should fix1. The two "snaps" tests do not test the snap. 2. The server is still one separator away from the same silent NaN — and a test now pins that. 3. 4. Nits
Checked and fineNo shell anywhere ( Once 1–4 are in I merge it, and |
What the review found. **The snap tests could not see a snap.** They asserted where the fill ended, which is the same whether it snapped or glided: deleting the whole `if (snap)` block left every test green. The style stub now records each transform with the class it was written under, so a track change reads as `snap` then a glide, a pause as a landing, and an ordinary answer as a single write. Without the snap block, four of them fail. **The parser no longer pins NaN.** The fix lived only in the AppleScript string, and a test asserted the NaN the parser would still make of a comma. Duration, position and volume now go through one helper that reads a decimal comma, exponent form included — from 10 000 up AppleScript writes `6,9724998E+4` — and rounds to a whole millisecond. What decides the comma is the number format setting, not the region. **`div` replaces `round`.** Inside the tell block `round` is a Standard Additions command sent to Spotify, and a Spotify that refused it would fail the whole script, turning a stuck bar into an error. `div` is an operator; a test keeps `round` out. **And the rest.** The glide stops under `prefers-reduced-motion`. A pause lands instead of running the fill back. A closed, stopped or failing Spotify empties the bar and forgets the last track — without that, a reopened Spotify left the fill stuck in `snap` and it never glided again. The three copies of "one second" (the poll period, `POLL_MS`, the CSS transition) name each other, and the catalogue test pins the new sizes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Merged — thanks. Checked the follow-up against Two things I did on top, in |
What this changes
Three things about the Spotify tile.
The progress bar never moved outside an English locale.
player positionis the one fractional value the state script asks for, and AppleScript writes numbers with the Mac's own decimal separator: on a French system it answers69,724998, whichNumber()reads as NaN. The width becameNaN%, which a browser drops, so the bar sat at zero — on the machine this is most likely to be running. AppleScript rounds the position to whole milliseconds now; an integer has no separator to get wrong.durationwas already in milliseconds andsound volumealready whole, which is why neither was affected.And it stepped once a second. The position arrives on a one-second poll and the fill was drawn where the last answer put it. It is sent to where the track will be at the next answer and given that second to get there; each answer corrects the aim, so nothing drifts. A seek or a new track lands at once — running the fill backwards across a second reads as a fault. Scaled rather than resized, so the second belongs to the compositor. The bar is also thicker and stands clear of the transport: at 4px under a 4px gap it read as an underline on the buttons rather than a bar of its own.
The tile was a row whatever its shape. A band about 150px high floated in the middle of anything taller, while the cover stayed pinned to 40% of the width: on a 640×680 tile the content used 200 of 636 pixels. The cover, the song and the transport are three grid areas now, and only the areas move:
minSizedrops to[8, 4], like every widget that is not a list of text, anddefaultSizegrows a row to[16, 5]. Themax-width:480pxrule already in the file was written for the narrow case but sat behind a width no tile could reach, since the widget asked for sixteen cells.Two things there were worth measuring rather than guessing, and both cost me a round:
aspect-ratio, with the same cap on both sides — capping only the width left a portrait cover on every narrow tile, since a replaced element with an explicit height does not recompute it from a width that was clamped;How to see it
pnpm dev, play something in Spotify, and watch the bar cross a second instead of jumping.For the arrangements, a tile at 16×5, 16×8, 16×12 and 8×12 on one page walks through all three. I swept widths from 8 to 64 cells against heights from 4 to 17 — about forty combinations — by driving the widget in iframes sized like tile bodies, with the same two messages the dashboard posts.
One size stays imperfect and I left it so: 8×8 (312×282 of body) keeps about 35px of margin top and bottom. It is too tall for a row to fill and too short for a column to be worth it — stacking would drop the cover from 143px to 77.
Checks
pnpm typecheckandpnpm testpass (1206 server, 334 ui)pnpm --filter ui buildrunBranched on
mainat2a16496. The widget's version goes to 1.1.0.🤖 Generated with Claude Code