Replace the four existing Flathub screenshots with captures from the
1.2.5 build (new logo, new sidebar, refreshed login chrome) and add
three new ones that showcase features worth highlighting on the
Flathub listing:
* enriched-artist-page.png — artist view with editorial data
* hifi-wizard.png — DAC setup wizard
* immersive-line-bed.png — immersive line-bed playback mode
Metainfo manifest is updated in the next commit because Flathub freezes
screenshot URLs by commit SHA, so we need this commit's SHA to land
first.
cc
The collision check in positionPopup was reading the popup's measured
rect right after mount, but the popup grows after that runs (the
genre tree fills in asynchronously). The first measurement reported a
height much smaller than the final ~500px / ~700px max-heights from
CSS, so the "would I overflow the bottom?" branch evaluated false even
when the final popup didn't fit. Result: bottom of the popup got clipped
by the window edge (visible in the Discover -> Filter by genre flow on
short windows).
Use the CSS max-heights (530/630 already used for width) as the
collision target instead. If the down-placement still overflows, try
flipping above the trigger; if neither fits (very small window), clamp
to the bottom padding and let the popup's own internal scroll handle
the overflow.
cc
* src-tauri/Cargo.toml and Cargo.lock: 1.2.4 -> 1.2.5
* src-tauri/tauri.conf.json: 1.2.4 -> 1.2.5
* package.json (+ package-lock.json regenerated): 1.2.4 -> 1.2.5
* snapcraft.yaml: 1.2.4 -> 1.2.5
* packaging/flatpak/com.blitzfc.qbz.metainfo.xml: new <release> entry
dated 2026-04-15 covering the visual refresh (new logo + login
branding), Plasma/Klassy auto-detect for window controls,
opt-in match-system-chrome rounded corners, Editor's Picks
ribbon cleanup, the two sidebar menu positioning fixes shipped
alongside this bump, and the DAC wizard contrast fix.
Tag not applied per request — release notes are staged, ready for
the screenshots pass and manual tag afterward.
cc
Two bugs surfaced during 1.2.5 QA:
* The per-playlist right-click context menu opened at the cursor and
could fall below the bottom of the window when invoked on items near
the last of the sidebar. It now measures its own rect and flips above
the cursor if there's no room below, or clamps to the bottom padding
as a last resort. Same pattern already used for the main playlists
dropdown menu.
* The Sort submenu inside the sidebar's general playlists menu was
being clipped by its parent .dropdown-menu because the submenu lived
inside that menu's DOM subtree. Teleport it via a local `use:portal`
action so absolute positioning escapes any ancestor `overflow: hidden`.
Also fix a duplicated "License" label in the About dialog's Build Info
block: the second row meant to show the platform was using the license
key. Swap to about.buildInfo.platform (the key already existed in both
locales).
cc
The inset dark frame + inset highlight + outer drop shadow combo
didn't produce a convincing edge on WebKitGTK — rounded corners are
the only piece of "match system chrome" that actually works today, so
drop the rest and leave the CSS minimal.
Real native shadow/border around a CSD Tauri window requires hints
like `_GTK_FRAME_EXTENTS` (X11) or `zxdg-toplevel-decoration-v1`
(Wayland) that wry doesn't expose yet. That path stays as a Phase 3
TODO — either upstream wry/tao or an FFI to gtk::Window via
raw_window_handle.
cc
Per user request, put explicit window edges above the ambient drop
shadow. Restore clip-path for clean corner anti-aliasing (it doesn't
clip inset shadows, only outer paints), drop the body padding
workaround, and paint two inset bands on .app:
inset 0 0 0 1px rgba(0,0,0,0.75) — crisp dark frame, 1px
inset 0 0 0 2px rgba(255,255,255,0.06) — subtle inner highlight
Keep a low-weight outer drop shadow (0 8px 20px) so the window still
has a hint of lift, but the edges — not the shadow — define the
window boundary now. Works the same on any compositor because it's
all painted by the webview, not by KWin/Mutter.
cc
Phase 2 produced clean rounded corners but no visible shadow because
.app filled 100vw/100vh, leaving no pixels for box-shadow to paint
into — and the clip-path added for anti-alias was clipping the shadow
as well.
Reserve a 12px padding on body while match-chrome-transparent is active
and shrink .app by the matching 24px in width/height, so the outer
drop shadow has somewhere to render. Drop clip-path (it was consuming
the shadow); keep transform: translateZ(0) for GPU AA on the corners.
Two-layer shadow: a broad soft ambient (18/40 rgba(0 0 0 / 0.65)) plus
a tight 4/12 crisp shadow for edge definition without painting a hard
1px stroke that jaggies at the rounded joint.
cc
Remove the hard 1px outline that the "match system window chrome"
style was painting inside the radius: on WebKitGTK the outline
amplified anti-aliasing jaggies at the rounded corners because the
stroke has no blur. Replace with a softer two-layer drop shadow,
add clip-path: inset(... round ...) on top of border-radius (clip-path
gives noticeably cleaner AA than border-radius + overflow:hidden alone
in WebKitGTK), and promote the container to its own compositing layer
with translateZ(0) + backface-visibility:hidden so the GPU handles the
edge.
cc
Rounded-corner window chrome now actually works. The toggle in
Settings > Appearance persists to the window_settings SQLite store and
gates the Tauri window transparency decision at startup, so on next
launch the main window is rebuilt with `transparent: true` and the
border-radius plus edge outline apply without painting white corners.
Flow:
* Toggle in Settings > Appearance -> windowChromeStore saves to
localStorage and calls v2_set_match_system_window_chrome which
writes match_system_window_chrome to window_settings.db.
* Toast reminds the user to restart.
* On next launch, should_use_main_window_transparency() reads the
setting and returns true, Tauri builds the window transparent.
* Setup stores the decision in MAIN_WINDOW_TRANSPARENT. A new command
v2_main_window_is_transparent reports it back to the frontend.
* +page.svelte only applies .app.match-chrome when the window is
actually transparent AND the custom titlebar is visible, so
stale-state toggling never produces white wedges.
* CSS sets html/body transparent via a global class on <html>, then
.app.match-chrome.floating paints the rounded corners plus a
12/28 rgba drop shadow and a 1px darkening outline.
* Maximized state clears the radius automatically.
Env vars QBZ_FORCE_TRANSPARENT_WINDOWS / QBZ_FORCE_OPAQUE_WINDOWS still
take precedence over the DB-persisted toggle.
cc
Adds an opt-in "Match system window chrome" toggle in Settings >
Appearance. When enabled (and the custom title bar is active, i.e.
neither "Use system title bar" nor "Hide title bar"), QBZ rounds its
own corners to match the detected desktop's decoration radius and
paints a thin edge outline so the window reads as its own surface
against the desktop instead of a bare rectangle.
Phase 1 scope:
* New windowChromeStore (localStorage) holding the toggle and the
detected corner radius.
* desktop_theme module gains a best-effort window_corner_radius_px:
10 for Klassy, 6 for Breeze Plasma, 12 for GNOME/Adwaita, 8 fallback.
* +page.svelte subscribes to the detector; .app.match-chrome.floating
applies the radius via a CSS variable plus a 1px darkening outline
and a deeper shadow. Maximized state removes the radius.
* SettingsView: new toggle, disabled when hideTitleBar or
useSystemTitleBar is on; i18n for en/es.
External shadow that matches each DE's compositor remains a phase 2
item — it requires `transparent: true` on the Tauri window, which has
compositor stability trade-offs and must be restart-gated.
Also cleans up redundant AlbumCard ribbons in the Editor's Picks
TAB (distinct from the home tab that was fixed earlier): removes
ribbon={album.ribbon} from the `editorPicks` and `qobuzissimes`
sections inside the editor-picks tab block. Press Accolades and
Popular Albums keep their ribbons (they vary per card).
cc
qbzd is feature-complete on disk but not ready for public release yet.
Keep all code paths in place so it can be unhidden with a one-line
flip, but remove it from the user-facing surface:
* build-qbzd.yml: comment out the `push.tags` trigger so neither a
regular `v*` version tag nor a `qbzd-v*` tag auto-builds artifacts.
workflow_dispatch is preserved for manual testing.
* CastPicker.svelte: guard the QBZ Daemon tab, the manual-connect
section, and the three `v2_qbzd_*` Tauri invocations behind a
`QBZD_ENABLED` flag (currently false). Default activeProtocol
switches to 'chromecast' so the picker doesn't start on a hidden
tab.
* SettingsView: DAC Setup Wizard icon filter swapped — button icon
drops the `invert(1)` so it reads on light accent colors (e.g.
Catppuccin Lavender), and the tooltip copy of the same icon now
has `invert(1)` so it reads on the dark surface bg. Unrelated to
qbzd but bundled since both are pre-release polish.
cc
Adds a runtime-detected "Klassy (auto-detect)" preset for the custom
window controls that reads the user's Plasma decoration theme and
applies matching colors and shape. When Klassy itself is not installed
but Plasma is, the option re-labels to "Plasma (auto-detect)" and
still applies the desktop color scheme. Hidden on non-KDE / non-Linux.
Backend:
* New desktop_theme module parses kwinrc, kdeglobals, and klassyrc
without external deps. Exposes detect_desktop_theme Tauri command
returning DesktopThemeInfo: desktop id, isKlassy flag, WM active/
inactive background + foreground, accent, DecorationHover, and the
user's Klassy ButtonIconStyle / ButtonShape when present.
Frontend:
* windowControlsStore gains applyKlassyPreset(), a cached detection
helper, and mapKlassyShapeToQbz() that maps Klassy's button shapes
(FullHeightRectangle, FullHeightRoundedRectangle, Tab, Circle,
Square, IntegratedRoundedRectangle, ...) onto QBZ's shape set.
* Adds the full-height-rounded shape to ButtonShape to cover the
most common Klassy style that QBZ was missing; CSS matches Klassy
FullHeightRoundedRectangle visual (full-height tab with ~7px
radius and margin).
* SettingsView fires detection on mount, derives the preset list
reactively so the auto-detect option only appears when relevant,
and adds the new shape to the Style picker.
* i18n keys added for the new shape and both preset labels in en/es.
cc
Apply the new matte-vinyl QBZ logo (Flathub-compliant: square canvas,
74% footprint, no baked shadows/gloss) across every icon asset: Tauri
standard sizes, Windows Store squares, Android icons and mipmaps,
static/, README, and the tray.
Ship two tray variants (black and white glyphs) and pick the right
one at runtime by reading GNOME color-scheme, GTK settings, and KDE
kdeglobals. Defaults to light-theme glyph when nothing matches.
Login screen: logo 80 -> 175 px with a subtle var(--text-muted)
drop-shadow using color-mix() for alpha, plus QBZ and "Qobuz(TM)
Player" under the icon. Update app.tagline i18n accordingly.
cc
The package's `postInstall` wraps the installed `qbz` binary with
`LD_LIBRARY_PATH` so it can `dlopen(libayatana-appindicator3.so.1)` at
runtime. Inside `nix develop`, users run `target/debug/qbz` directly
without that wrapper, so the tray init panics at startup.
Add a `shellHook` that mirrors `postInstall`'s library path.
Same root cause as the previous LIBCLANG_PATH fix: `inputsFrom` makes
dependencies available at build time but does not set runtime/env vars
the way the installed wrapper does.
Refs #312.
cc
rodio deprecated `DeviceTrait::name()` in favour of `description()`
(name) / `id()` (stable identifier). The legacy fallback in
`preferred_name()` was unreachable on our target platforms anyway —
the only path that matters is the `description()` one, since that is
what `get_audio_devices()` persists in settings.
Skip the device if `description()` fails instead of falling back to
the deprecated API. Silences the build warning.
cc
- src-tauri: rand 0.10.0 → 0.10.1 via cargo update (alert #36)
- qbz-player: rand 0.8 → 0.9, `gen_range` → `random_range` (alert #38)
The underlying RUSTSEC issue only triggers for code that calls
`rand::rng()` alongside a custom logger — we don't do that anywhere,
so actual impact is nil. Bumping closes the alerts.
cc
`inputsFrom` does not propagate the package's `env.*` attributes into
the dev shell, so running `npm run tauri dev` inside `nix develop`
failed at `mupdf-sys(build)` with "Unable to find libclang".
Re-export LIBCLANG_PATH on the shell itself.
Closes#312.
cc
Level A of issue #307 was firing 'device no longer available' spuriously
because settings.output_device stores the ALSA-normalized form
(front:CARD=X,DEV=0 per normalize_device_id_to_stable) while cpal's
output_devices() enumeration returns different strings entirely —
DeviceTrait::description() (friendly) and DeviceTrait::name() both
produce values that don't match what the UI persisted. Comparing them
marked every configured device as 'missing' on normal playback.
Removes the pre-flight emit_missing_if_needed() calls from
v2_play_track and v2_resume_playback so the false-positive toast
stops firing. The init_device fallback still routes audio to default
on a truly missing device — we just no longer surface that to the UI,
which is net neutral for the user vs. wrong notifications.
Keeps audio_device_watch module, DeviceMissingThrottle state and the
frontend listener in place so Level B (emit from inside the audio
thread when the fallback actually triggers) can reuse the plumbing
without another refactor. Hardens the presence check with
empty-enumeration = inconclusive and description()-based matching for
whenever Level B actually wires up a call site that has a comparable
device name at hand.
cc
Root cause traced from user diagnostic: only one playback:state event
fired (position=0), then nothing. Backend's current_position() returned
0 forever because thread_state.duration was 0 — the PlayStreaming
handler at player/mod.rs:1849 stores duration verbatim from the
command parameter and current_position() at player/mod.rs:789 ends
with .min(duration), clamping to 0.
Parameter names also didn't line up. cmdPlayTrack was invoking
v2_play_track with qualityStr and lowQualityFallback, neither of
which matches Tauri's camelCase mapping of the Rust signature
(quality, forceLowestQuality, durationSecs). Quality and force-fallback
were being silently ignored; durationSecs was never sent at all.
The normal queue playback path in playbackService uses the correct
camelCase names and passes track.duration, which is why the seekbar
worked everywhere except the session-restore first play — that path
goes through togglePlayback → cmdPlayTrack.
Fix:
- Correct the parameter names in cmdPlayTrack to match the backend
(quality, forceLowestQuality, durationSecs).
- Add durationSecs as a first-class arg and pass it from
togglePlayback when resuming a restored track.
- Drop the temporary [Diag:*] console logs added while investigating.
cc
evaluateQconnectSessionPersistence now unconditionally returns
shouldPersist: true. Previously it refused to save the local session
whenever Qobuz Connect was in remote mode, which meant closing QBZ
with QConnect active discarded the current track + position. On next
launch the app fell back to whatever QConnect held, which is
playlist/release-level only.
QConnect-vs-local priority on restore is a separate concern and
belongs at the restore path, not here.
Tests updated.
cc
Level A of issue #307. When the configured output device has
disappeared from the host device list at the time of play/resume
(KVM switched away, USB sink unplugged, PipeWire sink removed…)
the backend emits an 'audio:device-missing' event and the frontend
shows a warning toast:
'Output device "XYZ" is no longer available. Using system default.'
The existing init_device fallback already routes audio to the system
default, so this is purely informational — the user now understands
why audio came out of a different sink instead of staring at a silent
progress bar.
Does NOT touch the sacred audio init path (pipewire_backend.rs,
init_device, audio_settings.rs). New module audio_device_watch.rs
adds a pure presence check, a new-state throttle so we don't emit on
every resume once the user's been notified, and a v2 command
v2_check_audio_device_presence so the frontend can poll on demand.
Ready for Level B (background watchdog) — the same helpers plug into
a timer without further refactor; Level B only needs to add a tokio
task that calls emit_missing_if_needed() every N seconds while
playback is active.
cc
Referenced by AwardView hero / Other Awards mini-card / AlbumDetailView
sidebar award avatar in commit d719fb67 but the asset itself was never
tracked. Ships the SVG so the placeholder renders after a clean clone.
cc
appendPageReleases already skipped releases whose id already lived in
artist.albums/epsSingles/liveAlbums/others, but convertPageArtist
(the first /artist/page response) pushed everything blindly. When
Qobuz returned the same release in multiple groups (e.g. both the
main category and the 'download' distribution bucket) we ended up
with side-by-side duplicates on the discography grid — reported by
user on Holly Humberstone's 'Cruel World' entry.
Adds a seenIds Set scoped to the whole response so a release is
assigned to exactly one category on first encounter and silently
dropped on subsequent references. totalAlbums is now counted per
unique accepted item instead of raw group size, so the header count
matches the rendered grid.
cc
The compact QualityBadge in Popular Tracks rendered a gold-gradient
chip that clashed with the rest of the track row. Homologates to the
same look AlbumDetailView uses in its TrackRow: plain 12px grey text
('24-Bit/96 kHz' / 'CD Quality'), centered in an 80px cell, no
coloured background.
Uses formatQuality() with bit-depth > 16 as the hires signal so the
output string is identical to what AlbumDetailView shows for the
same track.
cc
Uses /static/laurels.svg as the default award artwork everywhere a
placeholder was rendering the lucide Award (trophy) icon:
- AwardView hero circle (140px, 68%)
- AwardView 'Other awards' mini-card (120px, 56%)
- AlbumDetailView sidebar award avatar (44px, 58%)
Laurel is rendered as an <img> filtered white (brightness(0) invert(1))
over the existing gold gradient. Awards that ship with their own
image/magazine cover keep theirs — the swap only affects the default
fallback.
cc
- AwardView stayed on the same activeView when switching between
awards via the 'Other awards' carousel, so onMount didn't re-fire
and the old award's data persisted. Replaces the onMount trigger
with a keyed on awardId: resets loading flags and calls
loadHero/loadAlbums/loadOtherAwards whenever the prop changes.
- Essential Discography albums all carry the Ideal Discography award
by definition — the ribbon is redundant on every card. Drops the
ribbon prop from the two AlbumCard call-sites for that rail.
Ribbons still appear on New Releases, Press Accolades, Most
Streamed, Qobuzissimes, Album of the Week, Editor's Picks and
Forgotten Favorites.
cc
1. Press ribbon restyled to a solid gold gradient (#f5c042 → #d49511)
with dark (#1f1407) uppercase text and a subtle white text-shadow.
Drops the red border-left. Qobuzissime / Album of the Week keep
their dark-background treatment with coloured left accent.
2. AwardView now loads /award/explore on mount (first 30 entries,
current award filtered out) and renders them as a horizontal
'Other awards' carousel below the album grid — same layout as
LabelView's 'More labels'. Each card shows the award laurel
(or magazine cover when present) + name + magazine name; clicking
routes into that award's AwardView.
i18n key award.otherAwards added for all five locales.
cc
Root cause of 'Discoteca Ideal Qobuz not clickable': /album/get
returns awards as LegacyAwardDto (per a20/c.java + LegacyAlbumDto
in the Android decompilation) whose id field is named awardId, not
id. Our AlbumAward struct only looked for 'id', so every award from
/album/get deserialized with id=None and the click fell back to the
catalog resolver — which was itself incomplete.
Adds serde aliases 'awardId' / 'award_id' for the id field and
'awardedAt' for awarded_at. Applies to both qbz-models (V2) and the
src-tauri legacy mirror.
Endpoint coverage:
- /discover/index → {id, name, awarded_at} (primary match)
- /album/get → {awardId, name, awardedAt, …} (alias match)
- /artist/page → {id, name, awarded_at} (primary match)
cc
Previous loop short-circuited when a page returned fewer than the
requested limit, but Qobuz may return a partial page with has_more=true.
Only stop when items is empty, has_more is explicitly false, or the
page repeats ids already seen (server ignoring offset).
Also bumps the sessionStorage key (v2) so prior truncated caches are
discarded, and logs every page (offset, returned count, has_more) plus
a summary (distinct ids, names, total items, pages) for debugging.
cc
- Console-dumps album and album.awards when AlbumView loads so we can
see exactly what Qobuz returns for a given album (user reported
Discoteca Ideal Qobuz still not resolving even after /award/explore
cached 15 entries).
- Adds rememberAward / rememberAwards / rememberAwardsFromAlbums to
the catalog store plus sessionStorage persistence. Wiring these
into the data paths comes next once we know what shape /album/get
actually returns for awards.
cc
User pushed back on the hardcoded list of Qobuz editorial award names
across locales. Removed the seed map entirely — the catalog is now
100% what Qobuz returns from /award/explore in the user's current
session locale, so id resolution always matches whatever labels the
API is currently emitting.
Kept:
- Cache-first lookup (hit the in-memory map before touching the API).
- Diacritic-insensitive + whitespace-agnostic normalization so accent
or case differences don't miss a real match.
- Single fetch per session, paginated.
Added a console.log summary after the initial fetch and a warn when a
name misses the catalog so future 'X award doesn't open' reports are
debuggable without guessing.
cc
'Discoteca Ideal Qobuz' was not resolving because /award/explore
either doesn't list editorial Qobuz awards or returns them under a
different locale's label. Adds a hardcoded seed of the three
stable-id editorial awards (88 Qobuzissime, 151 Album of the Week,
70 Ideal Discography) with their Spanish/English/German/French/
Portuguese aliases so they resolve instantly regardless of the user's
Qobuz locale.
Name normalization now strips diacritics (NFD + combining chars) and
collapses whitespace so 'Discoteca Ideal Qobuz',
'Qobuz Ideal Discography' and 'Discothèque idéale Qobuz' all map to
the same id.
Seed runs first on every lookup (sync). /award/explore only kicks in
for press awards the seed doesn't know about, once per session.
cc
AwardView now shows a single grid of the first 20 albums with a See
all link in the section header (visible only when there are more
albums behind it). Grid layout stays — no horizontal carousel.
Clicking See all navigates to a new 'award-albums' view
(AwardAlbumsView) with a full paginated list: header with kicker +
title, top-right search, virtualized grid with load-more. Same
structure as LabelReleasesView for visual consistency.
cc
/album/get's AwardDto has id: String? — Qobuz sometimes returns an
award entry with only a name and no id. Rather than disabling the
click or toasting, we look up the id in the Qobuz award catalog.
- Adds v2_get_award_explore (paginated /award/explore) all the way
through qbz-qobuz → core → bridge → command → invoke_handler.
- New frontend awardCatalogStore caches the full (id, name) list
lazily, one fetch per session, resolved on first click that needs
it. Further clicks are instant.
- handleAwardClick goes straight to navigation when id is present,
falls back to resolveAwardIdByName otherwise. Toast only fires if
the name isn't in the catalog either (genuinely unknown award).
cc
Previously disabled={!onAwardClick || !award.id} grayed out the award
card whenever /album/get returned an award entry without an id. User
reported the award was 'no longer clickable' on albums opened from the
artist page. Now the button always accepts clicks and the handler
shows a toast when no id is available instead of silently doing
nothing — keeps the affordance honest without sending 'null' to
/award/page.
cc
The log showed 'award=null' reaching /award/getAlbums and Qobuz
replying '400 Bad Request: Invalid argument: award_id (accepted type
are number)'. Root cause: AlbumAward.id was typed Option<u64> and
/album/get returns award ids as strings; serde dropped them to None,
frontend stringified null → 'null', we sent that as the query param.
Fix:
- AlbumAward.id becomes Option<String> with a custom deserializer
that accepts both string and integer JSON values.
- TS types + AlbumDetailView Award interface + pickAlbumRibbon all
use string id (compared as '88'/'151' now).
- Sidebar button is disabled when id is falsy; handleAwardClick
early-returns on empty id so we never navigate to award 'null'.
- Better body/status logging in get_award_albums stays so future
4xx responses are visible in the log.
Also switches /award/page and /award/getAlbums to signed_get_auth
(app_id + user token). Mobile does the same — catalog visibility is
region-scoped and the unsigned variant returns 400.
cc
Two behavior fixes for AwardView based on user feedback.
1. Grid content. The previous implementation rendered the releases
array from /award/page, which is user-scoped (only surfaces awards
on albums the user already favorited), so the grid came up empty
in almost every case. Switch to /award/getAlbums which is the
public catalog listing with pagination. /award/page is still
called, but only to hydrate the hero (name, image, magazine).
2. Follow-the-award. Awards themselves are followable on Qobuz mobile
(it's what feeds the 'Awards' tab of Release Watch). Add:
- favorite_awards table + store methods in favorites_cache.rs
- 4 V2 commands (get/sync/cache/uncache) mirroring the label ones
- awardFavoritesStore.ts — clone of labelFavoritesStore with string
ids (Qobuz mobile API passes award_ids as strings)
- Follow/Unfollow heart button in AwardView hero, matching the
ArtistDetailView / LabelView visual
- loadAwardFavorites() wired into the login bootstrap alongside
loadArtistFavorites / loadLabelFavorites
- i18n keys (award.follow / award.unfollow)
Also adds a Ver más button for paginated loading of the album grid.
cc
Matches Android's AwardDto where every field is nullable. Our struct
required id and name as String which made /award/page fail parsing
with 'missing field `id`' as soon as any award response deviated from
the happy path (e.g. a deprecated award, an id returned as integer,
or a reduced payload).
All top-level AwardPageData fields are now Option; id + awarded_at
accept string OR integer via a shared deserializer. AwardMagazine.id
gets the same treatment. Frontend TS type matches the looser shape.
cc
Adds the frontend for award detail pages. The hero header mirrors
LabelView's proportions 1:1 — 180px circular avatar left, inline
subtitle/name/magazine stack right, same 32px heading, same 12px
uppercase kicker, same 24px gap, same 40px bottom margin, same back
button treatment. Outer container uses the same paddings and scroll
behaviour as LabelView/AlbumDetailView.
Body pulls /award/page and builds one HorizontalScrollRow per
container (releases, new-releases, classics…). Container titles
localize via award.section.<id> with fallback to 'Acclaimed Releases'.
Wiring:
- 'award' added to ViewType and session restore map
- handleAwardClick in +page.svelte navigates to the view
- AlbumDetailView sidebar now passes onAwardClick so Phase 2 award
cards become interactive
- Frontend AlbumAward.id is optional to match tolerant backend struct;
sidebar disables the award button when id is absent
- i18n keys added in all five locales
cc
Previous attempt loosened PageArtistAward (id u64 -> Option<u64>,
awarded_at custom deserializer) to accept /album/get's inconsistent
award shape. That rippled into /artist/page consumers, which the user
rightly called out ('no eliminar lo que ya funcionaba, adaptar pues').
Restores PageArtistAward to its original shape (id: u64, name: String,
awarded_at: Option<String>) and introduces a parallel AlbumAward type
used only on DiscoverAlbum and Album. AlbumAward tolerates missing id,
accepts awarded_at as int or string, and never fails deserialization
of the parent payload. /artist/page keeps its strict contract.
cc
Backend plumbing for Phase 3 AwardView. New endpoints:
- v2_get_award_page(awardId) → hero + categorized releases
- v2_get_award_albums(awardId, …) → paginated album list
Mirrors Android's AwardDto shape (id, name, image, awarded_at,
magazine, releases, playlists). Follows the label/page pattern for
signing and error handling. Deserializer accepts the V2 generic
{has_more, items} envelope as well as the legacy search-style
{albums: {items, total}} in case Qobuz swaps shapes per account.
cc
/album/get returns award.awarded_at as a Unix timestamp integer while
/discover/index returns it as a YYYY-MM-DD string — our struct only
accepted String, so /album/get started failing with
'invalid type: integer `…`, expected a string' as soon as awards were
added to Album in Phase 2. Every 'Load album' call broke.
Adds a custom deserializer that accepts both shapes and normalizes to
String downstream. Applied in both qbz-models (V2) and src-tauri legacy
api::models for symmetry.
cc
Ports the Qobuz web 'label + awards' sidebar pattern to AlbumView.
Track list and sidebar sit side-by-side above 1100px and collapse
vertically below it. Label card navigates to the existing LabelView.
Award cards call onAwardClick when provided (disabled until Phase 3
lands AwardView).
Wiring:
- Adds awards (Vec<PageArtistAward>) to both qbz-models Album and
src-tauri api::models Album so /album/get payloads hydrate the
field instead of silently dropping it.
- Adds awards to QobuzAlbum and AlbumDetail frontend types plus the
convertQobuzAlbum adapter.
- New album.sidebar i18n keys in all five locales.
Card ribbon still shows only the LAST award (Phase 1 behavior);
sidebar renders every award as a tap target — mobile-style.
cc
Changes the AlbumCard ribbon prop shape from a union string to
{kind, label} so press accolades (Pitchfork BNM, Rolling Stone 5★,
Gramophone Editor's Choice, …) can render with their actual award
name rather than a pre-defined string. pickAlbumRibbon now picks the
LAST entry in album.awards[] (per product direction) and classifies
it as qobuzissime (id 88), albumOfTheWeek (id 151) or press (anything
else). Press variant gets a red border-left to differentiate from the
Qobuz-branded awards. Full award stack will render in AlbumView's
sidebar (Phase 2).
cc
Ports the Qobuz mobile 'Radar de Novedades' / Release Watch screen 1:1
(see rr0/i.java): a standalone view with three lazy-loaded tabs —
Artists, Labels, Awards — each hitting /favorite/getNewReleases with
the matching type parameter (limit 150 per tab, matching the mobile
cap).
Home's Release Watch rail (which still shows the Artists feed by
default, matching the mobile's default tab) now has a 'See all' link
that routes to the new view.
Adds view 'discover-release-watch' to the navigation type, wires the
route in +page.svelte, and adds tab/empty-state i18n keys in all five
locales.
cc
AlbumDto from Qobuz carries both legacy singular 'artist' and current
'artists[]' array. /favorite/getNewReleases omits the singular field
and only populates the array, so our Album struct (which has a default
serde Artist) deserialized an empty name — every Release Watch card on
Home read 'Unknown Artist'.
Pre-process the JSON response items before deserializing: when 'artist'
is absent or null, copy artists[0] into it. Keeps the rest of the
album-list plumbing untouched.
cc
User asked explicitly for the ribbon to sit below the hover overlay;
misread the original request and put it above. Relocates ribbon to
bottom:8px left:0 and drops z-index to 1 so .action-overlay (z-index 2)
covers it on card hover.
cc
The ribbon feature relied on album.awards arriving on the frontend, but
the DiscoverAlbum Rust struct in both qbz-models and src-tauri/api was
missing the 'awards' field, so serde silently dropped it during
deserialization of /discover/index. Result: frontend always saw an
empty awards array and pickAlbumRibbon returned undefined.
Also bumps the home sessionStorage cache to schemaVersion 2 so pre-fix
caches (which persisted AlbumCardData without ribbon) are discarded on
next launch instead of rehydrating with missing fields.
cc
Adds a small top-left ribbon on AlbumCard that surfaces Qobuz's two
locale-stable editorial awards: 'Qobuzissime' (award id 88, purple) and
'Álbum de la semana Qobuz' (award id 151, yellow). The ribbon is
z-index 4, above the hover action-overlay (z-index 2), so it stays
visible when the user hovers the card.
HomeView maps every /discover/index rail via discoverAlbumToCardData
through a new pickAlbumRibbon(awards) helper that matches by stable
award id rather than translated name. Essential Discography and
Forgotten Favorites (which render DiscoverAlbum raw) call
pickAlbumRibbon inline.
i18n keys added to all five locales under 'ribbon.*'.
cc
The card hover painted the same bg-tertiary that the follow button uses
on hover, so mousing over the button produced no visible state change
because the card background already matched it. Removes the card-level
background transition; the cursor:pointer is enough to signal the card
is clickable, and the button now shows its hover distinctly against the
plain background.
cc