Commit Graph
7015 Commits
Author SHA1 Message Date
vicrodh a564ed936b fix(qt): the header search text stops short of the clear cross and the Enter glyph
The TextInput spanned the whole box, so a long query ran under the
cross and the Enter hint. It now ends at whichever of the two is
showing, and TextInput scrolls the text inside that width.

cc
2026-09-14 00:49:54 -06:00
vicrodh ed7fa588ea fix(qt): elevated controls lift on hover instead of darkening
Buttons that rest on surfaceElevated used surfaceHover for their hover
fill: a 6% white wash meant for rows that rest transparent. Over an
elevated button it lands DARKER than the rest fill — on a modal's dark
ground the Cancel/Save/footer buttons went near-black under the pointer.

QbzTheme gains surfaceElevatedHover (a lift of surfaceElevated, derived
so every palette has it), its ambient half-alpha twin, and the
elevatedFill / elevatedHoverFill pair an ambient-aware control paints.
Every site with the old pairing (65 across modals, toolbars, selects,
circle actions, chips and cards) now hovers to the lift.

cc
2026-09-14 00:49:54 -06:00
vicrodh 4bf5cb1a0e feat(qt): the playlist editor's folder select is searchable
Folder trees outgrow a plain list; QbzSelect's filter box (the
ui-control standard's searchable dropdown) now sits on top of it.

cc
2026-09-14 00:39:47 -06:00
vicrodh 2e984e14cd feat(qt): queue and Listen list row menus offer Go to album and Go to artist
Both queue surfaces (the sidebar panel and the Listen list view) gain the
two entries, routed by source the way the local rows already do: a
catalog row opens the album through its catalog id, with its title and
artist riding along as the unavailable-page hint, and the artist by id; a
local row (file, Plex, Jellyfin, Subsonic) opens the album through the
queue's navigation key — the key local_playback stamps so it opens the
same page the card does — and the artist by name through the Local
Library Artists tab. A row without the key or the name shows no entry.

cc
2026-09-14 00:39:47 -06:00
vicrodh fed0816e9b fix(qt): ambient mode reaches every chrome control
Under the ambient field some controls still painted the opaque theme
token while their neighbours used the half-alpha one: the album track
filter and its select toggle, the Library tab strip's host, the Local
Library icon selects, the header's active nav items and open playlists
button, the queue sidebar's now-playing card and artwork placeholders,
the Listen list's floating actions, and the shared SettingsButton,
IconTextButton, multi-select bar, text area, load-more pill, tree row
selection, sidebar count chips, Purchases tab strip, play-history filter
box and Orbit banner. Each now takes the ambient-aware fill the header
search and QbzTabBar already use (theme.ambientOn ? …A50 : …).

Tooltips, popups and modals stay opaque on purpose: they are read over
content, not chrome.

cc
2026-09-14 00:39:47 -06:00
vicrodh a75d7b0087 fix(qt): a submenu stays open while the pointer is over its rows
CardMenu's pair-hover check read the popup's HoverHandler, which lives on
the background and never fires over a row: the rows' MouseAreas take the
hover events, so the panel only counted as hovered on its 5px padding.
Crossing from that padding into the child's first row read as "left both
menus" and the 350 ms leave timer closed the pair — the Copy submenu
closed the whole context menu as soon as it was entered.

A menu is now hovered while its panel OR one of its rows is (one hot row
at a time, held by reference so a destroyed row cannot leave it stuck),
and the pair check reads that on both menus. scripts/test-card-menu-qml.sh
drives the pointer through the parent row, the child's rows, back to a
plain row and out, and joins the runtime gate and CI.

cc
2026-09-14 00:39:47 -06:00
vicrodh e278d931b7 feat(qt): "Where you left off" reopens the exact page, its arguments and its tab
The startup pref only knew the last ROOT view: `last_view` lives in the
ui_prefs.json the Slint build shares, whose vocabulary has no room for a
detail, so a session closed on an album came back on Library > Releases.

page_restore_qt keeps the page itself in a per-profile document
(last_page_qt.json: root, view, args, state), written as the user moves.
Openers that can rebuild their page record their arguments through the
new nav_qt::record_with (album, artist, playlist, label and its releases,
mixtape/collection detail, award and its albums, mix, artist releases,
discover browse); the Settings section, the search query and tab and the
purchased album are noted where they live; the mounted view's tab comes
from QbzShell.reportNavState. At startup the shell mounts that page's
view (no Home flash), the history is seeded with the page's root so Back
leads there, and the session entry re-runs the opener — one-shot, behind
the same gates as the local album restore (remember pref, crash chain,
launcher link, kiosk), skipping catalog pages while offline and Purchases
unless it is switched on. Pages only their opener could rebuild (scene,
musician, disco builder, metadata editor) keep the previous restorable
page on record; local albums stay with local_restore_qt.

ContentRouter now reports the active tab of any view that has one and no
richer state of its own (Discover, Library) and applies a Back/Forward
destination's saved tab, so tabs survive both a restart and the history.

cc
2026-09-13 23:56:10 -06:00
vicrodh dae03e3834 fix(qt): list rebuilds no longer steal the header search box's focus
Typing in the album track filter lost the keyboard on every keystroke. A
qt.quick.focus trace named the thief: a fresh track delegate took focus
"in scope pageFlick" right after each rebuild. In Qt 6.11 assigning a new
array to ListView.model after completion runs
QQuickItemViewPrivate::connectModel, which forces currentIndex to 0, and
updateCurrent() then focuses delegate 0 — inside the view's focus scope,
where the header's TextInput lives. `currentIndex: -1` does not survive
the assignment.

QbzArrayModel (a DelegateModel) now owns such arrays: a swap reaches the
view as remove+insert changes, which honour currentIndex -1 and keep the
rows. Because ListView re-seats inserted rows around the viewport, the
model re-expresses the viewport relative to the content origin and
restores it after layout; a page replacement calls scrollToTop(). The
album page, the kiosk album and the kiosk playlist swap their rows on it,
their ScrollMemory stores origin-relative offsets, and opening another
album resets the track filter (the box mirrors `trackQuery`).

scripts/test-array-model-qml.sh pins the behaviour (focus kept, viewport
kept, top on request) and joins the runtime gate and CI.

cc
2026-09-13 23:32:00 -06:00
vicrodh b868319233 feat(qt): search boxes clear on Escape, drop focus, and offer a clear cross
Every search field follows one contract: Escape empties the text AND hands
the keyboard back to the shell root, and a clickable cross clears the text
while keeping the caret in the field. The header search shows the cross
left of the Enter glyph while the cortinilla is open; QbzLineEdit's search
mode and the new QbzSearchField (the album track filter and the playlist
filter) float it at the right edge.

Each field also keeps its focus after an edit unless another text input
took it, and logs when it had to reclaim it.

cc
2026-09-13 23:32:00 -06:00
vicrodh e546e6614e fix(qt): catalog placeholder names never render as artists or composers
Qobuz fills missing credits with strings such as "Not Documented",
"Unknown Artist" or "N/A". `qbz_text_utils::names::is_placeholder_name`
recognises them (case-insensitive, hyphen/underscore folded) and the album
document, the track info modal and the album info modal drop those names
from credits, composers, featured lists and the header's artist fallback,
dropping a row entirely when nothing legible is left.

cc
2026-09-13 23:31:59 -06:00
vicrodh 7f609a55e5 feat(qt): Copy submenus open on hover and hold their parent open
CardMenu entries declare a submenu as data (`submenu: [ {label, icon,
action} ]`); the row builds a child CardMenu on first use, opens it when
the pointer rests on the entry (or clicks it), and closes it when the
pointer moves to another entry. While either menu is hovered the pair
stays open; leaving both for 350 ms, pressing Escape or clicking outside
closes both. Picking in the child closes the parent too.

The track row, the Popular Tracks row and the player bar's "+" menu
declare their Copy entries this way instead of re-opening a second
CardMenu at the row's saved click position.

cc
2026-09-13 23:31:59 -06:00
vicrodh 119fab64ee i18n: strings for View label, Copy submenu, Buy on Qobuz and the unavailable-release page
Twelve new msgids in all eight catalogs.

cc
2026-09-13 20:26:11 -06:00
vicrodh 1817de0743 feat(qt): release-aware album links, an unavailable-release page, Copy submenus and Buy on Qobuz
Unavailable tracks (8): playlist rows now say when the WHOLE release is
gone (`releaseUnavailable`, one probe per distinct album of the pulled rows
through the library's helper); TrackRow's album link and "Go to album" go
dark on it instead of opening an empty page. Every row that opens an album
passes the names it knows (`openAlbumFrom`), so when the catalog answers
404 the page names the release: AlbumView renders an "unavailable" arm —
heading, "This release is no longer available on Qobuz." — with the
best-ranked alternatives Qobuz still sells (the replacement flow's ranking,
exposed as `ranked_alternatives`) in a rail; a network failure gets the
same page with "Couldn't load this release." and no rail.

Copy (9): the player bar's "+" and the track row menus (shared TrackRow
and the artist's Popular Tracks rows) gain a Copy entry with a submenu —
"Track name" or "Track - Album - Artist" — through the shared clipboard
carrier; CardMenu marks `submenu: true` entries with a chevron and the
host reopens the second menu on the same anchor. Row menus keep both lazy.

Buy on Qobuz (10): the album header shows a store action when the catalog
says the release is purchasable and it is not already owned; the track
menus and the player bar carry the same entry for catalog rows and resolve
it at click time — the store page opens in the browser, or a toast says
the release is not sold. The link is `https://www.qobuz.com/album/-/{id}`,
which the store answers with a 301 to the localised page (verified).

cc
2026-09-13 20:26:11 -06:00
vicrodh 81c8d3908e feat(qt): the Releases list shows each release's label, and album menus open it
Library feed album items carry the catalog label id next to its name.
Library > Releases (list) gains a LABEL column between the item and its
quality — a link to the label page when the id is known, muted text
otherwise — hidden on the label page itself and on narrow kiosk rows.
AlbumListRow and AlbumCard menus gain "View label" (the card only where its
host passes the id: the library grid does). The library's release-wide
availability helper is shared with the playlist rows for the next change.

cc
2026-09-13 20:26:11 -06:00
vicrodh 22e064f340 feat(models): albums and tracks carry the catalog's purchasable flag
Qobuz answers `purchasable` on every album and track object (captured in
the API samples); the models never read it. `Some(false)` is the catalog
saying a release is not sold, absence stays unknown. The exhaustive test
fixtures in qbz-core and qbz-playlist-import spell the field out.

cc
2026-09-13 20:26:11 -06:00
vicrodh 10fb9eba84 fix(qt): the Popular Tracks play button matches its siblings, and its rows offer Track info
The header's play was a filled 44px accent disc beside two plain 32px
outlined actions and butted the JUMP TO rule; it is now the plain form with
8px of air above. The Popular Tracks row menu gains "Track info" for catalog
rows through ONE lazy TrackInfoModal per artist view, not one per row.

cc
2026-09-13 20:03:36 -06:00
vicrodh 3f3c4ad548 feat(qt): Track info and Album info are selectable and copy basic or full text
Every field of both modals is a QbzSelectableText — artist, label and
musician names stay links. A copy button beside the close cross opens a
flyout: "Copy basic data" puts "{Track} - {Album} - {Artist}" (or
"{Album} - {Artist}") on the clipboard; "Copy full" formats the whole sheet
as plain text — the track's metadata, credits and copyright, or the album's
header plus the ACTIVE tab: credits per track with performers, or the
review. Album credit row heads stay plain because their click expands the
row. The two new strings ship in all eight catalogs.

cc
2026-09-13 20:03:36 -06:00
vicrodh 9ea290d58b feat(qt): the shared text modal fills the window, copies and scrolls visibly
The artist biography (and the album, label and playlist descriptions that
share this modal) sat in a 560 x 460 card with dense body text. The card is
now at least 75% x 65% of the window, the body is selectable rich text one
size up with 150% line height and spaced paragraphs, the scrollbar stays
painted, and a copy button in the header puts the whole text on the
clipboard with a check as the receipt.

cc
2026-09-13 20:03:36 -06:00
vicrodh 27be9a8143 feat(qt): a selectable text control and one shared clipboard carrier
QbzSelectableText is a Text-shaped label the user can select and copy: a
read-only TextEdit with mouse and keyboard selection, the right-click Copy
menu, and a rich-text link for fields that navigate; its `raised` arm keeps
a plain shadowed Text for hosts on the ambient field. QbzClipboard is the
off-screen TextEdit copy carrier CommandBlock, SandboxSettings and the
lyrics flyout each grew on their own, as one primitive. QbzScrollBar gains
`alwaysShown` for surfaces that must read as scrollable.

cc
2026-09-13 20:03:36 -06:00
vicrodh 07e86d329d fix(integrations): Discord presence backs off for five minutes after a failed connect
With Discord closed, every now-playing edge (two per track: the immediate
publish and the poll's) tried the IPC socket and the crate logged
"find_pipe: could not find pipe" each time, so a listening session read
as an error stream. A failed connect now sets a five-minute deadline,
logs one line at info, and later edges push nothing until it passes;
opting the integration in again clears the deadline. The crate's own
warning is demoted to error in the default log filter. Pure backoff rule
covered by a unit test.

The Discover preferences store also logged "unavailable" when its lazy
init ran before the session restored; that startup order is normal and
the handle is rebound on activation, so it is a debug line unless a real
user directory fails to open.

cc
2026-09-13 18:35:09 -06:00
vicrodh cc345b38d8 fix(qt): measure the compact album title outside the Text to end a width binding loop
The compact heading sized its title with the Text's own implicitWidth
while eliding it; an elided Text recomputes its implicit size after laying
out at the bound width, so Qt reported a binding loop on `width` on every
album open (measured in a 30-minute session log). A TextMetrics with the
same font gives the natural width without feeding the loop.

cc
2026-09-13 18:35:09 -06:00
vicrodh e70204c8e1 fix(qt): stop shadowing Item.enabled and Item.palette in four components
SettingsButton, QbzToggle and QbzSelect declared their own `enabled` over
Item's, and ReactiveRingsPanel its own `palette` over Item's QPalette: Qt
flagged all four at start (propertyCache: "overrides a member of the base
object"). The shadowed `enabled` never disabled input by itself — each
control filtered clicks and keys by hand, and any child that did not would
have kept receiving events. The inherited property now drives the visuals,
the focus policy and the guards, and disables the subtree the way Item
does; the rings' colour list is `ringPalette`.

cc
2026-09-13 18:35:09 -06:00
vicrodh e834f8f52b fix(qt): the player bar keeps the same 8 px gutter on macOS
The wider macOS gutter added for the Liquid Glass corner curve read as
excessive next to the 8 px the content pane keeps; one value on every
platform now, and the macOS-only theme token is gone with its comment.

cc
2026-09-13 17:10:30 -06:00
vicrodh 41ca92961a feat(qt): header nav goes compact only when the text tabs no longer fit, and compact items honour the click rule
The compact icon form is now a fit decision, agnostic of the sidebar state:
text tabs while every label at the 11 px floor plus the tab chrome and
whatever trails the tabs (the playlists button a closed sidebar adds) fits
left of the search box, icons only otherwise — or always with "Compact
header navigation" on. A closed sidebar used to force the compact form by
itself; it now only adds one button to the measure, so a window with room
keeps its text tabs. The room is measured from the third sacred button,
not from the hidden row (a Row leaves a hidden child's x where it was).

The downloads ring and the separator + playlists flyout button move out of
the compact row into a trailing group shared by both forms, so a closed
sidebar with room keeps its playlists entry; the group hides outright when
it has nothing to show.

Compact section buttons now call NavFlyout.sectionClicked like the text
tabs and the sidebar rows do, so under the click-navigates opt-in a click
on an icon lands on the section's first entry instead of only opening the
flyout — Purchases was the one compact item that navigated.

Settings > Navigation describes the new rule in all eight catalogs (en
never carried the old entry). The Purchases navigation gate models
headerNavOn and the fit bit: 128 x 3 combinations, still exactly one
reachable Purchases entry.

cc
2026-09-13 16:27:53 -06:00
vicrodh f81b63b817 test(qt): the Purchases navigation gate follows the two-slot model and the mode-aware bar z
The Node gate still expected three PurchaseTab slots (the title-bar entry
went when Purchases started following the section navigation) and a fixed
`z: 1` on the shell's NowPlayingBar (mode-aware since the 2.1.2
stabilization: Small's seek thumb needs the lift, the full-height modes
yield to the Large cover dock). test-crates on pre-release was red on the
second since the stabilization merge and would have failed on the first
next; both assertions now state the invariants that actually hold. 64
combinations x 3 sidebar states still prove exactly one reachable
Purchases entry.

cc
2026-09-13 16:02:10 -06:00
vicrodh d05082eaee fix(qt): the player bar is one chrome band like the header, with its layout inset
The bottom band is now NowPlayingBar.qml's root: full width, flush on the
window's bottom edge, surface-card @ 0.5 over the ambient field like the
HeaderBar, opaque surface-card otherwise. The bar layout (PlayerBar /
NowPlayingBarSmall) paints no background of its own and mounts inset by the
gutter on its left, right and bottom, so the bar's ends still line up with
the content pane's while nothing around it shows the raw field. Both
rejected shapes are documented in place: an inset plate over the raw field
(three sides of raw background, the only zone of the shell without chrome)
and a frame slab behind an inset plate (a bar glued onto a slab). A future
layout inherits the band and the gutter from the seam instead of carrying
either.

cc
2026-09-13 15:38:25 -06:00
vicrodh cadbb768fc test(playlist-import): the qobuz track fixture carries favorited_at
The match_qobuz test helper builds a qbz_models::Track exhaustively, so the
new favourites timestamp field has to be spelled out there too. Test-only
code: the release binary was never affected, only the CI-mirror gate.

cc
2026-09-13 14:57:04 -06:00
vicrodh de5fe8c29c test(core): album and track fixtures carry the new favorited_at field
cc
2026-09-13 14:51:00 -06:00
vicrodh 8274df875d fix(qt): the player bar keeps its gutter with nothing painted behind it
Only the margin was asked for. The frame-tier backdrop behind the inset
bar read as a bar glued onto a slab; it is gone, and the bar floats over
the shell's own ground like the content pane's margins do.

cc
2026-09-13 14:51:00 -06:00
vicrodh 1ba578a1d8 feat(qt): move a playlist to a folder from its editor, delete one from any context menu
The editor gains a Folder row — the same folders the sidebar and the
manager file playlists into, "No folder" for the root — seeded from the
playlist's current folder and applied on Save through the manager's own
move verb, so an existing playlist no longer needs the sidebar's row menu
to be filed (#785). "Delete playlist" joins the sidebar row menu (own and
local playlists; sidebar entries now carry isOwner) and the playlist card
menu (own playlists), behind a shell-level "Delete playlist?" confirmation
that runs the same delete the editor's button runs and leaves the detail
page only when it was the deleted one (#776). Eight catalogs updated.

cc
2026-09-13 14:47:12 -06:00
vicrodh 83c12c62c5 fix(qt): Date added sorts by real dates in the library, and playlist sorts order instead of reversing
Library > Releases (and All) "Date added" was position-based: every source
got a rank of index / len and the merge interleaved sources by that
relative rank, so a purchase from yesterday could sit behind favourites
from months ago. The favourites API carries favorited_at (never modelled)
and purchases purchased_at; local favourites keep their own stamp. Rows now
carry addedAt, the merge and release-sort.js order by it newest first, and
only undated rows fall back to the rank, always after the dated ones.

The playlist view's Default and Date added never sorted: they reversed
whatever order the rows were in (after a Title sort, a reversed title
order), which is why flipping the direction took two clicks and switching
fields kept the old order (#782). Rows now carry their loaded position —
Qobuz's insertion order is its added order — Default and Date added sort
by it, and the direction toggle reverses a canonical order.

cc
2026-09-13 14:47:11 -06:00
vicrodh 0674b2adeb ci(aur): publish from a tag run when a deploy key exists, ship .SRCINFO in the dry run, one-command local publish
The workflow_run path only ever prepared metadata, and its artifact skipped
.SRCINFO (a dotfile needs include-hidden-files), so a green run after a
release left AUR untouched — 2.1.1 sat at 2.1.0. Tag runs now publish when
the repository holds AUR_SSH_PRIVATE_KEY (a passphrase-less deploy key on
the maintainer's AUR account) and print a notice pointing at the manual path
otherwise. scripts/packaging/aur_publish.sh is that manual path in one
command: stamp from packaging/aur, hash the published assets at their final
URLs, regenerate .SRCINFO in a clean Arch container, commit per repo, and
push only with --push (the personal key's passkey is asked interactively).

cc
2026-09-13 14:22:23 -06:00
vicrodh a106ef790c feat(qt): Qobuz Connect lists renderers on this network and pairs them
A LAN-only receiver — a BluOS player, another QBZ — was invisible until an
official app paired it into the session; that is the "it showed up once,
for a day" report. The Qt LAN runtime now browses _qobuz-connect._tcp
alongside its own receiver (same lifetime: Qobuz Connect enabled and
ready), probes every announcement that is not us, and the picker shows the
answers under "On this network" with a Pair action. Pairing mints the
delegated pair with /qws/delegateAuth for the renderer's app id and hands
it over with this controller's session uuid and become_active, exactly as
the official controllers do; the cloud then lists the device and its LAN
row drops out by uuid. scripts/qconnect-lan-fake-renderer.sh is a receiver
that never joins the cloud, for exercising this without a Node. Pending:
qbzd parity, the 5-minute retry backoff, become_active=false.

cc
2026-09-13 13:46:13 -06:00
vicrodh c60cdec064 feat(qconnect): the controller half of the official LAN wire
qconnect-lan gains what QBZ calls on another receiver: a
_qobuz-connect._tcp browser (own device_uuid filtered, Electron's SDK
floor), the get-display-info / get-connect-info probe (IPv4 first, 10 s,
Connection: close) and the connect-to-qconnect handoff body — the
controller's session uuid, the delegated API token and the delegated QWS
token under its LAN name jwt_qconnect, become_active. qbz-qobuz mints the
delegated pair with POST /qws/delegateAuth (q_app_id), the step every
official controller takes before the handoff. The receiver DTOs learn to
deserialize (unknown enum values collapse to Unknown). Pure helpers tested.

cc
2026-09-13 13:29:35 -06:00
vicrodh 9eedb278f6 test: gate the loudness cache, scan, amplify, analyser and start-gain suites in the CI mirror
cc
2026-09-13 11:37:11 -06:00
vicrodh 7c71c6bf52 feat(qt): the player bar keeps the content pane's gutter on every mode, wider on macOS
Every now-playing bar (New / Classic / Large / Small) is inset by the same
8 px the content pane keeps to its frame — left, right and bottom — behind
a frame-tier backdrop, so the bar's ends line up with the pane's, nothing
in it touches the window edge, and the gutter reads as one continuous
frame with the sidebar rather than a band of raw background. The Large
cover dock lifts with the bar. On macOS the gutter is 16 px: Tahoe's
Liquid Glass windows round their corners far more than before (larger
still around a toolbar) and clip whatever sits inside the curve — the
cover at the bar's left end lost its corner — and Qt has no LayoutRegion
to nest content into the curve, so the margin is the fix.

cc
2026-09-13 11:37:11 -06:00
vicrodh 228a6f12f3 chore(i18n): drop the retired Purchases placement strings from the seven translated catalogs
cc
2026-09-13 11:31:29 -06:00
vicrodh ae644dc163 feat(qt): header tab labels grow to the sidebar's 13 px when the room allows
The full text tabs kept an 11 px label at every width. The size now comes
from measuring the labels: 11 px stays the floor (the tabs beside the search
box at the 1140 px breakpoint), 12 or 13 px when every label plus its chrome
fits in the room left of the centred search box — 13 px being what the
sidebar rows and the flyout entries use, so with space the two hosts read
the same. Measured rather than a width breakpoint because the labels are
translated and their count changes (offline hides the Qobuz sections,
Purchases is opt-in).

cc
2026-09-13 11:31:28 -06:00
vicrodh db7ddc5edd feat(qt): Purchases follows the section navigation, sidebar or header
The separate "Purchases in title bar" placement toggle is gone: with Show
Purchases on, the entry sits wherever the other sections are — in the
sidebar while the navigation lives there, in the header beside the full
tabs or the compact buttons otherwise — so it is never the one entry left
behind. The nav_tb_purchases pref and its bridge key are retired; the
Show Purchases description says where the entry goes.

cc
2026-09-13 11:28:44 -06:00
vicrodh d22907b0e0 feat(qt): Settings > Audio > Loudness — normalization toggle, target loudness presets, clipping guard
The bars' audio flyout keeps its switch; Settings > Audio gains the same
key with its two companions: a Target loudness select (-11 / -14 default /
-16 / -18 / -23 LUFS, a value set elsewhere shows as a display-only
"Custom (N LUFS)" entry instead of being re-mapped) and the Prevent
clipping toggle. Eight catalogs updated.

cc
2026-09-13 11:25:22 -06:00
vicrodh ec7311ce54 feat(player): normalization gain known before the first sample
Start gain resolves from the loudness cache, then from ReplayGain tags in
the bytes; the catalog's own ReplayGain is fetched once per track alongside
the CMAF setup (and the cached replays, the disk replay and the gapless
download), and a prefetched track the catalog cannot describe is
pre-analysed in full (EBU R128 + true peak) off the audio thread, reading
the bytes back from the playback cache since the prefetch streams them
straight in. DynamicAmplify now starts AT that gain instead of ramping from
unity, the published normalization gain is the real one, and the four
seeding sites share one resolver (normalization::plan_start_gain).

cc
2026-09-13 11:09:16 -06:00
vicrodh 8c4b140ef8 fix(audio): the clipping guard only limits a boost, never attenuates
A track that needs no boost keeps unity (or its own attenuation) even when
its peaks already sit above the -1 dBTP headroom: that is the master's
doing, not ours. Without this, every hot modern master exactly at the
target lost ~0.5-1 dB the moment the guard saw its peak.

cc
2026-09-13 11:09:15 -06:00
vicrodh 8fef2466cc feat(settings): normalization clipping guard (audio.normalization_prevent_clipping)
A new audio setting, on by default: the normalization boost is capped so the
track's known peak stays 1 dB under full scale. Store column + migration,
portable in the settings bundle, shown in diagnostics, and settable from
qbzd (settings get/set audio.normalization_prevent_clipping).

cc
2026-09-13 11:05:15 -06:00
vicrodh 32a78f4a27 feat(audio): loudness cache stores absolute LUFS with peak and source; analyser applies the live gain once and glides
The cache kept a target-relative gain (a target change invalidated every
row) and was rewritten every 5 s, so a skip left a 5-second window behind.
Rows now hold integrated LUFS + true/sample peak + a ranked source that a
weaker measurement cannot overwrite; the last window of a track is stored
as the full-track figure, and nothing shorter than 30 s is stored at all.
The analyser no longer re-levels a track that started with a known gain,
moves the live gain at most once (a seek keeps it), measures true peak, and
DynamicAmplify glides over 400 ms instead of 50 while polling the shared
atomic every 1024 samples instead of every sample. loudness_scan is the
pure full-track measurement the player's pre-analysis will feed.

cc
2026-09-13 11:02:02 -06:00
vicrodh c817331ece merge: integrate player QoL round 3 into pre-release
cc
2026-09-13 10:55:31 -06:00
vicrodh 2cfbf9b66f merge: integrate static-review hardening into pre-release
cc
2026-09-13 10:55:31 -06:00
vicrodh e8351e57ba feat(qt): Settings sub-nav regrouped under an Advanced fold, with new glyphs
Audio, Playback, Appearance, Navigation, Local Library, Offline,
Integrations and Updates stay in the column; Blacklist, Storage,
Import / Export, Developer (and Orbit while enabled) move under an ADVANCED
disclosure row with a chevron. The fold is not persisted: it opens itself
whenever the current section lives inside it, and reads as active while
closed over the selected page. Appearance takes lucide's
paintbrush-vertical, Integrations unplug and Storage database — baked into
every tint and registered as masters. The kiosk selector follows the same
order, flattened. "Share logs" stays pinned at the bottom.

cc
2026-09-13 10:26:34 -06:00
vicrodh 198fbfc6fe feat(qt): the offline cache is purged from the manager only, with a tooltip and an explicit modal
Settings > Offline loses its "Clear cache" row: it ran the same
QbzOffline.clearAll() as the trash glyph in the Offline Cache Manager's
stats bar, so the whole cache had two entry points and one misleading text
("your downloaded albums are kept" — what is kept are the purchased
downloads in the music folder). The glyph now carries a hover tooltip (the
manager mounts the shared QbzTooltip overlay; the limit and folder glyphs
get one too) and its confirmation says exactly what goes — the live track
count and size — and what stays. The Tauri-era string leaves the eight
catalogs; the new ones enter them.

cc
2026-09-13 10:26:33 -06:00
vicrodh 9b93e58367 refactor(qt): Offline cache and Playback cache return to their own sections
Settings > Storage keeps only the caches that have no section of their own:
the artwork cache, the lyrics cache and the Plex mirror. The offline
downloads group goes back to Offline and the playback cache (memory profile,
usage, storage folder) back to Playback, since both configure the feature
they sit next to rather than disk in general. Rows and bridge verbs are
unchanged; the playback cache QML test instantiates PlaybackSettings again
(16/16) and the .po location comments follow the artwork rows to Storage.

cc
2026-09-13 10:09:59 -06:00
vicrodh 71eee01e4c feat(qt): featured artists on track rows are on by default
The preference stays (opt-out); an absent key reads as on so rows never
flash without the names before the first settings snapshot.

cc
2026-09-13 09:20:21 -06:00