Sync: decoding runs ahead of the speakers — by up to ~0.8 s after a GUI
stall, when buffers arrive in a burst — and the visualizer drew the newest
audio. It now keeps timestamped history and draws what the player says is
playing, plus a per-machine Sync delay (config.json) for latency Qt can't
see, like Bluetooth.
Every style has one Fade slider (0 = off); bars now leave fading steps.
Laser: afterimage time, line width down to 1 px, glow on/off, tip dot
on/off and size (a resting beam always shows its dot), and an oscilloscope
dot grid under the beam. Visualizer sliders run the full window width.
Highlight: click a swatch for a full color picker; the color picked
replaces that slot (right-click restores it).
- The now-playing panel and the visualizer are debossed by exactly as
much as the buttons are embossed: the same rims and outline, flipped
(theme.deboss_qss / theme.paint_deboss), so things you press stand out
and things that show you something sit in.
- The bpm button's text is black, enabled or not.
- The emboss hugs the edges: thin lit and shaded rims, a defined outline
that's a touch darker along the bottom, and a nearly flat face — shape
without depth.
- Hover darkens the whole button slightly instead of Qt's pale panel on
top of it.
- One face for every button (theme.button_face_qss): regular buttons,
Library and its ▾, the purple friend-mode buttons, and the transport
boxes. Shuffle and bpm are boxes that are one button, so the whole box
darkens (bpm's whole box is now its tap target); the play box's three
tiles each darken on their own.
Groundwork for Cassette (friend library sharing) that stands on its own:
- A brand-new library (fresh data dir, or a fresh iTunes import) starts
with a "New Tracks" smart playlist: added in the last 3 months, newest
first. Fixed persistent id, so two machines seeding one unsynced share
merge into a single playlist.
- Every non-flat button, the sidebar Library button and the transport
boxes get a very slight top-lit gradient computed from the live palette,
with the chosen color kept exactly at its midpoint.
- Tests pin that individual imports stamp date added = now even for an
old file, and that the iTunes migration keeps the XML's dates.
The export dialog gains an accent color — the hover background on links and
tracklist rows, and the player's progress fill — and the rest of the bar loses
its 2010 gold so that choice is the only color in it.
The accent travels as a `:root { --accent }` custom property declared in
index.html, which player.css reads as `var(--accent, #8c764a)`. That keeps the
stylesheet in the verbatim copyfile loop: index.html is still the only rendered
template. `normalize_accent` is the injection gate — the value lands raw inside
a <style> block and string.Template escapes nothing — and `contrast_text` flips
the hover text black or white, since the old page hard-coded white and a pale
accent made it unreadable.
The bar itself is now fixed light gray with black text. Its sprite glyphs are
pale lavender and yellow, drawn for the dark gold bar, so they're recolored
with `filter: brightness(0)` rather than by editing the GIF — which still ships
byte-identical, spinner and all.
Not persisted: the picker opens on #8c764a every time, so an export nobody
touches looks exactly like the mixes already online.
Claude-Session: https://claude.ai/code/session_01JBSM2bFC6UToiEg8BE4dqj
Three things that only ever hurt new users.
The startup font modal is gone. _ensure_now_playing_font ran before
MainWindow existed, so on a machine without Century Gothic the very first
thing LinTunes did was open a parentless dialog demanding a font decision.
theme.NOW_PLAYING_FALLBACKS now picks the closest geometric sans installed
(URW Gothic, the Avant Garde clone CG derives from, leads the chain), and
the choice moved to Preferences ▸ Now-playing font. An uninstalled saved
family falls back to automatic instead of the app default.
A non-iTunes user can finally set their music folder. library.music_folder
was written in exactly one place — the iTunes importer — and with it unset
_music_import_dir fell back to a *relative* Path("Music"), resolved against
a working directory GNOME's dash does not set predictably (see
packaging/install-desktop.sh). Music scattered somewhere unfindable. Now
the first launch asks one plain-language question, Preferences can change
it later, and an import with no folder set refuses rather than guessing.
Picking ~/Music files into ~/Music, not ~/Music/Music.
music_folder is portable at last. It was the only path in the library
stored raw absolute in the *synced* metadata, so machine 2 inherited
machine 1's paths. It is now also stored relative to the data dir, the
same trick Track.location has used all along. The absolute key stays
forever as the shared floor between versions: old code reads it and
behaves exactly as before, and old code that writes the file just drops
the new keys, so no version combination hard-fails.
Two traps worth naming. set_music_folder must call
mark_library_settings_dirty() or reload_from_disk reverts the change on
the next sync tick. And the dirty flag only guards until flush, so a
music_folder_set_at stamp decides adoption semantically — _merge_metadata
picks the whole file by mtime, which moves when someone resizes a column
(the Round 39 lesson, applied to metadata).
Also: correct CLAUDE.md's claim that the importer skips smart playlists —
it imports them; system playlists are what's skipped. README gains a
"never used a terminal?" on-ramp and loses the instruction to hand-write
library_metadata.json before first launch.
Verified against the live 21,490-track library: still resolves (via the
legacy key), metadata untouched, 646 tests pass.
Claude-Session: https://claude.ai/code/session_01G4Z46BMQYS57bcbxWbSS3C
- New prefs: visualizer_mode (on/dim/off, saved on every click) and
color_visualizer_gray (None = keep the derived alternateBase look).
- VisualizerWidget takes prefs (optional, so existing call sites/tests
keep working); restores its mode at startup and validates junk values.
- New "Visualizer (gray mode):" row in the Preferences color sliders;
theme.default_gray mirrors the widget's 0.88-lightness derivation so
an untouched slider shows the current appearance.
- TransportBar.refresh_theme() repaints the visualizer so slider drags
preview live.
Five grayscale (black->white) sliders in Preferences that update live:
outer-chrome background (QPalette.Window), the rounded transport button
boxes, the now-playing panel, the now-playing text, and the row stripes
(QPalette.AlternateBase). Defaults are None ("inherit current") so nothing
changes until the user drags. Decouples the button boxes from the stripe
color (they used to share palette alternate-base). Live preview via
Preferences.set_live (no disk write); persisted ~0.4s after the drag.
Snapshot of the existing codebase before working through the TASKS.md
backlog. Real library data (data/) and the iTunes import fixture
(itunes-test-library/) are gitignored.