Skip to content

release: promote dev to main for 0.2.0 - #10

Merged
kolebayev merged 27 commits into
mainfrom
dev
Aug 21, 2026
Merged

kolebayev merged 27 commits into
mainfrom
dev

Conversation

@kolebayev

Copy link
Copy Markdown
Owner

Summary

Promotes everything accumulated on dev since v0.1.0 and cuts 0.2.0. Eight
feature branches, the CI trigger change, and the version bump. This is the
first build to carry the in-app updater, so it is also the last one existing
users have to install by hand.

Changes

Test plan

CI does not run on pull requests, so the full suite was run locally on the
merged tree at 38ae3db:

  • npm ci && npm run typecheck && npm test && npm run build — 157 tests pass,
    production bundle builds.
  • cargo fmt --manifest-path tauri-src/Cargo.toml --check — clean. Every one
    of the eight branches failed this on arrival, since nothing had ever run it
    on them; fixed in 4dd933f.
  • cargo clippy --manifest-path tauri-src/Cargo.toml --all-targets -- -D warnings
    — clean.
  • cargo test --manifest-path tauri-src/Cargo.toml — 50 unit tests and the 13
    library_roundtrip tests that drive the real iTunesDB write path.
  • release.yml's verify-version gate simulated for tag v0.2.0 — tag,
    tauri.conf.json and package.json agree.

Merging this triggers CI on main, which builds a DMG. The release itself
comes from tagging v0.2.0 afterwards.

The title and Copy Snapshot lived inside the scrolling pane, so they
slid away as soon as the cover wall moved.
A lossy encoder is free to reconstruct a waveform louder than the one it
was handed, and every one of them does: a master peaking at -0.18 dBFS
comes back out of AAC at +1.45. The iPod's output stage is fixed point,
so those samples saturate and land as short bright clicks.

Nothing about the source predicts it — this master's true peak is
-0.05 dBTP — so measure the encoded file instead and re-encode it
attenuated when it decodes back above full scale. Lossless targets are
exact and skip the pass entirely.

convert_check grows --aac/--mp3 because the lossless default it had
could not exercise the new path at all.
gpod_remove_track frees the database record and nothing else, so every
removal left its file on the device for good. A 74 GB iPod had 2 GB of
audio no record pointed at.

Queue the device path at removal and sweep it once itdb_write returns.
The ordering is the whole point: deleting first and then failing to
write leaves the on-disk database pointing at audio that is gone, which
is worse than the leak. A path a surviving record still claims is
skipped — libgpod does not check either.
Picking a cover in the bulk panel wrote it to every selected track
immediately, out of step with the other fields and with no way back
from a mis-click. It is now a draft like any other field: the panel
shows the staged file name, offers Clear, and the single Apply stamps
fields and artwork together.

Claude-Session: https://claude.ai/code/session_01TdJdhEhGTJHScRWETLqEYA
A drop while Convert is visible used to fall through to the library
import path, which needs an iPod and starts writing immediately.
Convert now takes the paths and only lists them: format, bitrate and
destination are still unset at that moment, so nothing runs until
Convert Files is pressed.

Claude-Session: https://claude.ai/code/session_016BSACqVKky8go5gyYk41xk
Every heading is now a button: click sorts by that column, click again
reverses. This replaces the Sort By radio group in the View menu, which
offered four of the ten orders the list can be in and put them two menus
deep from the columns they describe.

The sort is stored per grouping rather than once for the list. Album
order is the only order that makes sense under an album header and a
poor one for a flat list, so sharing one value meant every switch
between grouped and flat silently re-sorted the view being left. Flat
view opens on artist A-Z, the grouped views on album order.

Around that:

- An Added column, so Recently Added has a heading that asks for it.
- Album absorbs the row's slack instead of Title. Title took the whole
  width of a wide window for a three-word string while the album beside
  it truncated.
- Flat rows carry their own cover art. The grouped views already show
  one per album header.
- The heading clips and mirrors the list's scrollLeft. A heading too
  wide for the window used to widen the document, and a sideways
  gesture slid the entire UI; html/body now hide overflow too.
- Library's inspector and Convert's settings column share one width
  constant, so the edge stops jumping when tabs are switched.
A cover the webview cannot decode is not a missing cover as far as the
<img> is concerned: WebKit swaps in its own broken-image icon, which on
macOS is the system question mark tile — foreign to this UI and readable
as neither "no cover" nor "bug".

The thumb now reports the failure to the artwork cache, so the key falls
back to the music-note placeholder for every later mount rather than only
the one that happened to paint it. The mark is an ordinary cache entry,
so invalidateArtwork drops it and a replaced cover is fetched again.
Half the cores left the UI responsive but not the audio: with eight
decode+resample chains running, CoreAudio's render thread loses the race
often enough that music playing on the machine stutters for as long as a
batch lasts.

Two workers still halve the wall clock against a plain queue, which is
where most of the win was.
The DMG is handed out by link, so without a check on launch a user stays
on whatever build they downloaded once. The HTTP request is made in Rust
rather than the webview: the CSP allows no origin but 'self', and
widening connect-src to reach GitHub would open that door for the whole
UI.

A declined version is remembered by version, not as a flag, so saying no
to 0.2.0 still leaves 0.3.0 audible.

Claude-Session: https://claude.ai/code/session_01NTrpqWnfR4k3BNVhAkeJe1
A successful job used to leave a log holding a header, a footer and
whatever ffmpeg happened to complain about — no record of the tracks
themselves. Each one now writes a start line and a result line, and the
result carries what the file turned out to be:

  ✓ FKJ - Blessed.flac: converted in 0.7s · ALAC 16-bit 44.1 kHz stereo
    · 28.5 MB · 856 kbps

Those facts are read back off the finished file rather than derived from
the request: a passthrough that skipped the encode, or a VBR pass that
landed nowhere near its nominal rate, would otherwise be reported as
whatever was asked for. The extra ffprobe is gated on the observer
wanting it, so the drag-and-drop import path does not pay for it.

Three things the per-track lines exposed:

- ffmpeg's own lines carried the destination name while ours carried the
  source, so one track appeared in the log under two names.
- swscale's "deprecated pixel format" fires once per track of an album
  with art, from cover normalization this app asks for itself.
- the failure line repeated ffmpeg's whole stderr tail, which had already
  been logged line by line just above it.

Colour follows who is speaking rather than one hue per level: the app
narrating its own progress stays muted, ffmpeg keeps amber, and the only
two saturated colours are the states a reader scans for — landed and
failed. Both are the queue's existing tones, so a track that went green
in the list is green in the log too.
The archive is packed by bundle-dylibs.sh, not by tauri's own
createUpdaterArtifacts: that one is produced inside `tauri build`,
before a single dylib has been copied, so it would carry an app that
still links ~/.local and /opt/homebrew — it installs cleanly and then
dies in dyld on the user's machine.

latest.json is written on `release: published` instead of alongside the
DMG because it has to carry the release notes, and those are typed into
the draft long after the tag build finished. A missing .sig fails the
release outright rather than drafting one the updater could never use.

Claude-Session: https://claude.ai/code/session_01NTrpqWnfR4k3BNVhAkeJe1
Replaces gray/dark/mono with orange/green/purple/black, all regenerated
from new 1024px sources, and adds the Icon Composer document the artwork
is authored in. The old three were value variations of one image and
read as the same icon dimmed; the colourways are told apart at Dock size.
The log export's three sentences of explanation were the bulk of the
panel, which read as Settings being mostly about logging. Five icon
tiles need 464px, so the dialog goes 420 -> 500.
xattr -dr on /Applications needs sudo.
An unfiltered pull_request trigger burned a macos-14 runner on every
push to every feature branch, while the dev -> main pull request
re-runs the same suite on the same commits before anything ships.

Claude-Session: https://claude.ai/code/session_019JSNhxAtjFP8is9uJ7nQWX
fix: clipped audio and leaked files on the device
feat(icons): ship four dock colourways
fix(ui): stage bulk cover art until apply
feat(ui): stage dropped files on the Convert tab
feat(ui): sort the track list by column heading

StatsView conflicted with the pinned header from #2: both branches
replaced the pane's `flex-1` root because the tab wrapper is a plain
block. The header split already roots at `flex h-full flex-col` and
moved the scroller inside, so that structure wins and only the
reasoning from this branch's comment is kept.
feat(convert): per-track log, and our own art placeholder
feat(updater): ship in-app updates from GitHub Releases

CLAUDE.md conflicted only because this branch and the branch policy
notes both opened a new section at the same anchor. Both are kept.
None of these branches was ever fmt-checked: ci.yml only runs for
pull requests based on main, and every one of them targeted dev.

Claude-Session: https://claude.ai/code/session_019JSNhxAtjFP8is9uJ7nQWX
Nothing here deploys — the artifact is a DMG someone downloads, so a
pull request has no environment a red tick protects, while every job
needs a macos-14 runner to compile against libgpod. main is now the
only branch with a verdict, and the promotion merge is the commit it
lands on.

The dmg job's `if: github.event_name == 'push'` and the concurrency
group's pull_request check are both dead with the trigger gone.

Claude-Session: https://claude.ai/code/session_019JSNhxAtjFP8is9uJ7nQWX
.gitignore already listed `.claude`, but the rule never applied:
gitignore is only consulted for untracked paths, and CLAUDE.md had been
in the index since before the entry existed. The stale comment above
`.claude/settings.local.json` claimed the opposite and is gone with it.

The file stays on disk; only the index entry is dropped.

Claude-Session: https://claude.ai/code/session_019JSNhxAtjFP8is9uJ7nQWX
release.yml refuses a tag whose name disagrees with tauri.conf.json or
package.json, so all of them move together. Cargo.toml and both lock
files follow to keep the crate and the DMG's name in step.

Claude-Session: https://claude.ai/code/session_019JSNhxAtjFP8is9uJ7nQWX
@kolebayev
kolebayev merged commit 4365996 into main Aug 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant