qbzd (the dormant TUI crate) pinned ratatui -> unicode-width =0.2.0,
which blocked upgrading Slint past 1.14. Dropped from the workspace
members; the directory stays on disk, just no longer built.
cc
The Output Device dropdown, on the ALSA backend, listed devices flat,
unordered, with the BP badge on too many rows. Port the Tauri
DeviceDropdown.svelte ALSA behaviour:
- Regroup devices into Defaults / Bit-perfect (Hardware / Digital) /
Plugin Hardware / Other Outputs, in that order, with a section header
above the first row of each non-empty section.
- Align device_is_bit_perfect (the BP badge rule) with the Tauri ALSA
bit-perfect group: hw: / front:CARD= / iec958: only. hdmi: and
sysdefault: route through plugins and no longer get the badge.
- The regrouped ids stay index-aligned with SettingsMaps.devices, so the
settings-select("device", i) callback still resolves to the right id
and selected-device persistence is unaffected.
cc
QbzSelect gains an optional `groups` property parallel to `options`: a
non-empty entry renders a non-selectable, muted-caps section-header row
above that option in the open list. The header is plain text, no pill
background, matching the QBZ design rule.
The popup now self-sizes from the list layout's preferred height so the
extra header rows are accounted for. `groups` defaults to empty, so the
backend and sample-rate selects (which never set it) render unchanged.
cc
/proc/asound/cards lists every registered sound card, including
capture-only hardware (USB webcams, microphones). Those expose only
pcmXc capture nodes, so read_card_pcm_devices yields an empty playback
list for them. Without a guard such a card still got a bogus
sysdefault:CARD=<name> output entry, making a webcam appear as a
selectable output device.
cc
Slint 1.14 has no public register_font API, so the .ttf is imported
in app.slint (compile-time embed + registration) and selected via the
FONT_TEST_LINESEED toggle in main.rs. Flip the toggle to false to
restore the KDE system font.
cc
The nav-tab dropdowns now open when the pointer enters the tab, no
click required. NavTab gained a hovered callback fired from a
changed has-hover handler; click-to-open still works.
cc
Port the Tauri QueuePanel into QueueSidebar.slint: a Queue/History tab
switch, a highlighted NOW PLAYING card with a favorite heart toggle, a
paginated UP NEXT list with a Tauri-style counter (UP NEXT (N of M, K
left)) and prev/next page controls, four footer actions (clear, save as
playlist, infinite play, search) plus an inline queue search field.
Tabs and actions are plain text / plain icon buttons with no pill
backgrounds, per the QBZ design rule. All strings go through @tr. The
panel-open width animation is preserved; opening the panel fires
panel-opened so the controller pulls a fresh snapshot.
cc
QueueController owns the sidebar-local view state — active tab, search
query, paginator page — and the cached favorite-track IDs. It pulls a
full queue snapshot via QbzCore::get_queue_state_full, applies the search
filter, slices out the current 40-track page, and pushes the result onto
QueueState. Every queue mutation refreshes it.
The four footer actions: clear-queue calls core clear_queue (wiping the
now-playing slot only when nothing is audible, matching Tauri);
save-as-playlist resolves the queue track set (name-input modal pending);
infinite-play toggles the persisted AutoplayMode; search filters the
upcoming list client-side. play paths route through after_track_change,
extracted from the per-call duplication in playback.rs.
cc
Restructure the QueueState global so the sidebar can render two tabs and
a paginated upcoming list: separate now-playing / upcoming-page / history
properties, paginator counters, tab and search state, and callbacks for
every control. QueueItem gains duration and explicit fields. Adds a
favorite semantic color token and the heart-filled, infinity and list-x
icons the sidebar needs.
cc
QueueManager::get_state_full() returns the upcoming and history lists
without the 20/10 caps that get_state() applies, so clients can
paginate the upcoming list. QbzCore exposes get_queue_state_full()
plus favorite_track_ids() and set_track_favorite() for reflecting and
toggling per-track favorite state.
cc
play_track now checks the qbz-cache two-level cache (L1 memory ~400 MB
+ L2 disk ~800 MB) before fetching: a hit replays through play_data,
skipped only when the cached copy is a lower quality than requested
(cached_quality_below_requested, ported from Tauri). After a play the
track is stored — the CMAF path accumulates the assembled FLAC while
streaming, the legacy path stores the download. streaming_only
suppresses the write.
cc
femtovg's text rendering looks coarse next to native Qt/KDE. The
slint dependency now uses default-features = false with renderer-skia
(plus backend-winit, renderer-software fallback, accessibility,
compat-1-2). Pulls skia-safe — first build is slower and the binary
grows ~6 MB, both expected and accepted.
cc
The CMAF path passed duration_secs: 0 into play_streaming_dynamic, and
the streaming position timer clamps current_position to the duration
it was given — so elapsed, remaining, progress and auto-advance all
froze at zero. Duration is now derived from the CMAF segment table's
per-segment sample counts.
PlaybackEvent also gained buffer_progress (the streaming buffer fill
the audio thread already tracked but never surfaced); the poll loop
feeds it to the seek-bar cache overlay.
cc
Port the CMAF streaming playback path from the Tauri v2_play_track /
v2_cmaf_stream reference into the qbz-player core crate so any shell
gets CMAF transparently.
play_track now tries qbz_qobuz::cmaf::setup_streaming first: it fetches
only the init segment, derives sample rate / bit depth / total FLAC
size, estimates speed from init_fetch_ms, starts playback via the
existing play_streaming_dynamic, and spawns a background task that
fetches + decrypts + pushes audio segments. On setup failure it falls
back to the legacy get_stream_url_with_fallback + download_audio +
play_data path, which is kept unchanged.
The new private cmaf_stream_segments method ports the v2_cmaf_stream
fetch/decrypt/push loop: FLAC header first, per-segment reqwest get,
parse_segment_crypto, decrypt_frame for encrypted frames, push_chunk
to the streaming buffer, complete() at the end. The L1/L2 cache
parameters and cache calls from the reference are intentionally
omitted (separate later phase).
play_track's public signature and the bit-perfect output path are
unchanged.
cc
The app now resolves the KDE Plasma general font from kdeglobals and
sets it as the window default-font-family, so the UI matches the
desktop instead of the bundled Slint default. Falls back cleanly off
KDE. A full font picker lands later with Settings > Appearance.
cc
Tab labels rode high against their glyph and the header because a
bare Text box sits high on its descender space. They now use an
explicit row-height box with vertical-alignment, drop one point to
11px, and take the brightest text color.
cc
The BP badge marked every ALSA device; it now marks only the direct
hardware PCMs (hw:/front:/iec958:/hdmi:), matching Tauri — sysdefault
and plug devices are not bit-perfect. The device list also opens at
480px (right-anchored) so the full card + PCM id name is readable
instead of being elided in a 300px control.
cc
The output-device select now shows an accent BP marker on devices
able to deliver bit-perfect audio: every ALSA hw card, hardware nodes
on PipeWire, none under PulseAudio. QbzSelect gained an optional
parallel badges array so other selects are unaffected.
cc
The now-playing bar and queue rows rendered empty placeholders. They
now resolve cover art through the shared image cache: the cache is
published process-wide so the playback controller can decode covers
off the UI thread and apply each one to its row.
cc
The volume icon now toggles mute: it stashes the current level, drops
the player to zero, and swaps to the volume-x glyph. Unmute restores
the level, and any non-zero set-volume (slider drag or step) clears
the mute so the two controls stay consistent.
cc
Dropdown and context menus now sit on the content background
(surface-main) with a more defined border (border-muted, ~22% white)
instead of the elevated surface plus hairline border, so the popover
edge reads cleanly against the content instead of looking coarse.
cc
The header description wrapped at its natural width and left the
space to the right unused. It now spans the full width to the page
edge (bound to root.width to avoid a layout binding loop), and the
truncation grew from 200 to 360 chars so the wider band reads full.
cc
Shuffle toggles via core; repeat cycles off/all/one and swaps to the
repeat-1 glyph for one-track mode. Volume minus/plus step the level
by 5%. State flows back through NowPlayingState.
cc
Header nav tabs now use the same icons as Tauri (compass, music
library, hard drive, cassette) and the dropdown sub-items carry
leading glyphs everywhere except Discover, which stays text-only.
Tab label text is centered vertically against its glyph and the
header. Dropdown and context menus drop from Radius.md to Radius.sm
so their corners match buttons and album cards.
cc
NavButtons now renders the collapse toggle and history buttons as two
spaced pills instead of separating them with a bar. Queue and Lyrics
sidebars animate their own width open/closed (160ms ease-in-out) so
they slide in like the main sidebar instead of appearing abruptly.
cc
Playback (minimal, album-focused):
- playback.rs controller: play an album (set queue + start audio),
play a single track, enqueue, play a queue index, transport
(play/pause, next, previous, seek, volume)
- the audible step uses Player::play_track (fetch -> download -> play);
the protected audio backend is untouched
- a 450ms poll loop pushes position/progress/times/playing onto
NowPlayingState and auto-advances at end of track
- NowPlayingState / QueueState globals; NowPlayingBar bound to real
state (song card, seek bar, transport, volume); QueueSidebar lists
the queue and rows play on click
- a played track is recorded into the recently-played store
Detail views:
- album description and artist biography stay with the metadata,
filling the width to the right of the artwork (not below the header)
- header nav tabs gained a dropdown caret and 2px side padding
Settings > Playback defaults are now opt-out: show context icon,
restore session, resume position and stream-uncached default on;
streaming-only stays opt-in; initial buffer defaults to 2s.
Known gaps: queue/now-playing artwork not yet threaded (Image is not
Send); autoplay-mode preference not consulted by auto-advance; buffer
progress line stays at 0.
cc
Shell chrome (steps 1-3):
- Left sidebar is collapsible — a toggle in the nav cluster (left of the
[<] [>] pair) and a draggable right edge that resizes 200-420px and
collapses below 160px. ShellState global drives it.
- Queue and Lyrics right sidebars: 320px panels rendered as layout
siblings, so opening one pushes the content area narrower rather than
overlaying it. The header captions/queue buttons toggle them
(mutually exclusive) and light up when active.
- The four nav tabs (Discover/Library/Local Library/My QBZ) open
dropdown menus listing their sub-sections; Discover -> Home is wired.
Detail views (steps 4-5):
- Track rows: the even-row tint is now clearly fainter than the hover
tint so the two states no longer look identical.
- The favorite and offline-cache glyphs in album track rows are real
buttons (hover state, emit media-action).
- Album header artist link uses the metadata text color, bold, instead
of the accent blue.
- Album description and artist biography span the full content width
below the header block.
cc
NowPlayingBar (start of the playback-bar work):
- height raised to 110px
- SeekBar component: three-line track (total / cached / progress), a
hover thumb, elapsed/remaining times, and a hover time bubble with a
downward caret marking the seek position
- song card: background and border dropped, gains the extra height,
album/artist links and the context icon kept (context icon now
always shown)
- transport + secondary buttons copied from Tauri (shuffle, prev,
play, next, repeat, add, favorite; cast, picture-in-picture,
fullscreen, normalization; volume cluster). Queue/Lyrics stay in the
header — and the header now orders them lyrics-then-queue.
Settings constraints (1:1 with Tauri):
- Exclusive Mode enabled only on ALSA; DAC Passthrough only on
PipeWire; Lock Output Device disabled while DAC Passthrough is on;
Gapless disabled while Streaming Only is on
- backend change cascades: forces off DAC passthrough / force-bit-
perfect / exclusive / gapless as appropriate; DAC-passthrough and
streaming-only cascades; initial-buffer slider right-aligned
Discover fixes:
- navigation [<] [>] pair homologated to the same top/left margin
across every view
- album/artist description moved back into the metadata column (it now
fills the width there) with uniform secondary text color
- genre filter button hugs its label instead of stretching
- Recently Played Albums and Tracks modules always render, with a
placeholder until the play history has data
- wider spacing between Discover rows; the Discover gear is no longer
a Settings shortcut (it is the per-tab sort/show-hide control)
cc
Rebuild both Settings panels to match the Tauri app — every option
present, on the correct tab, and fully functional (persisted and
applied to the live player).
Audio: streaming quality, limit-quality-to-device, maximum sample rate,
backend, output device, ALSA plugin, hardware volume, exclusive mode,
reserve DAC, DAC passthrough, force bit-perfect, allow quality
fallback, sync-on-startup, lock output device, and a reset button.
Conditional rows (ALSA plugin, hardware volume, max sample rate, force
bit-perfect, lock output device) are gated on Rust-computed flags.
Playback: continue-after-track, show context icon, gapless, restore
session, resume position, stream uncached, initial buffer size (slider),
streaming only, quality-retry behavior. Note gapless and the streaming
options live on the Playback tab though the qbz-audio struct stores them.
- ui_prefs.rs: JSON store for the frontend-only streaming-quality pref
- QbzSlider primitive for the integer buffer-size control
- volume normalization row removed (Tauri hides it)
Caveat: toggling reserve-DAC persists and is read live, but re-arming
the device reservation guard mid-session needs a player-side API that
does not exist yet; it takes effect on next launch.
cc
Move the album description and the artist biography out of the metadata
column to span the full content width below the header block, so the
text is no longer cramped into the narrow column beside the artwork.
cc
Add a navigation history so pages can be walked back and forward.
- nav.rs: a back/forward stack of NavEntry (home, album, artist,
settings); record() on fresh navigation, go_back/go_forward to walk it
- NavButtons: a [<] [>] pair styled like the Discover tab group; inline
beside the Discover tabs and in place of the per-page Back button on
album, artist and settings views
- All navigation routes through Rust (open-album/-artist record entries;
the gear and header menu use NavState.request-settings) so the history
stays accurate; back/forward re-apply entries, re-loading entity pages
- AppShell listens for the mouse back/forward buttons
- can-back / can-forward drive the buttons' enabled state
Also in this round:
- Album header description: render a pre-truncated short string so it
wraps to a couple of lines instead of overflowing the header; the full
text stays in the Read-more modal
- Header queue button uses an ordered-list icon (the previous one read
as a playlist)
cc
Four album-view corrections from review:
- Explicit badge: the title row now uses `alignment: start`, so the E
badge sits right after the track name instead of being stretched out
to a fake column position.
- Label/awards sidebar: dropped `alignment: start` on the track/sidebar
row — it had defeated the track list's stretch and left the sidebar
floating mid-row with dead space. The list now stretches and the
254px sidebar is flush right; it rides the content region inward when
a main sidebar opens.
- Track search: now a real client-side filter — a TextInput drives the
AlbumActions.search global callback; album.rs keeps the unfiltered
list and filter_tracks() matches title/artist case-insensitively.
- Description modal: the Flickable is vertical-stretch bounded so long
text scrolls, and strip_html keeps paragraph breaks as newlines.
cc
The right-controls HorizontalLayout had no explicit width, so a layout
left to fill its parent reported the full header width — the right
anchor `x` then resolved to a negative offset and the queue/lyrics/
hamburger buttons piled up over the Discover tab. Give the row an
explicit width so it anchors correctly.
cc
Bring the album view closer to 1:1 with the Tauri original.
- Track list: favorite and offline-cache columns, and the context-menu
button now always shows in the muted column color (it brightens on
hover) instead of being hidden until hover
- Toolbar above the track list: a QualityBadgeFull port of the Tauri
QualityBadgeStatic (icon + tier + exact bit-depth/sample-rate), plus
a track search box
- Editorial album description, truncated, with a Read more link that
opens the full text in a modal
- Label/awards sidebar: drop the resting card background; awards use
the Tauri laurels SVG
- Control buttons on both album and artist views are ghost circles
(translucent fill, faint ring) instead of solid fills
cc
Discover Home now has both recently-played modules: a carousel of
album cards and the slim track grid (renamed "Recently Played
Tracks").
- recently.rs records track plays with their album metadata;
load_albums() derives the distinct recent albums from that history
- home.rs threads a recent_albums list into HomeData; HomeView renders
it as a carousel; the artwork pipeline gains ArtworkTarget::RecentAlbum
Also raises the header-gradient contrast: the artwork color is now
saturation-boosted off its average and normalized to a fixed peak
brightness instead of just darkened, so the tint is actually visible.
cc
Close the Recently Played Tracks tech debt: a JSON-backed play-history
store at the shared QBZ data path, surfaced as a slim-card section on
Discover Home (it hides while empty).
- recently.rs: load() reads the store newest-first; record() prepends a
track, dedupes by id and caps the list — the playback session calls
record() when a track starts
- home.rs threads a `recent` list into HomeData; HomeView renders a
"Recently Played" GridSection; artwork pipeline gains an
ArtworkTarget::Recent
The data path is complete end to end; only record() awaits the
playback session.
cc
Bring the album and artist pages closer to the Tauri originals.
Shared:
- Artwork-tinted gradient header — header_tint() derives an average,
darkened color from the decoded cover; the page header renders a
linear-gradient from it. Both pages scroll on a Flickable (no
scrollbar gutter).
Album view:
- Full header action row: play, shuffle, favorite, radio, credits,
booklet, mixtape, multi-select, more (visual where playback/feature
wiring is pending; play and favorite emit media-action)
- Right sidebar with the record label and editorial awards, each a
round-avatar card; AlbumData now carries id, label and awards
Artist view:
- Gradient header, fuller action row, and an "ARTIST" eyebrow
- Discography split into titled release sections (Albums, EPs &
Singles, Live Albums, Compilations, ...) instead of one flat list
- Artist track rows gained the more-button context menu
cc
The header user element is now just the hamburger icon. The menu it
opens leads with a user row showing the display name and the Qobuz
subscription tier, carried through from the login/restore session.
cc
The ellipsis faded on the row's hover state alone; moving onto the
button stole has-hover from the row and hid the button under the
cursor. It now stays visible while the row OR the button is hovered,
and is a proper 32px square hit area with a hover background.
cc
The tooltip was centered on the badge and overflowed past the card
edge, where the next card painted over it. Right-anchor it so it
extends leftward into the card and stays clear of the neighbor.
cc
The overlay buttons were outline-only with a transparent center, so the
pointer flickered between the ring and the hollow middle, and the
flicker made the more button's menu impossible to trigger. The buttons
now have a solid (semi-transparent) fill, the play glyph is filled, and
the pointer-gating feedback loop is gone. The overlay stays visible
while hovering a button (the states are OR-combined, since a child
button steals has-hover from the artwork detector).
cc
The ScrollView reserved a horizontal-scrollbar gutter at the bottom of
the Discover Home even with the scrollbar policy set to always-off. A
Flickable scrolls on the wheel with no scrollbars and no gutters; its
viewport-width is pinned to its own width so the clipped carousels can
never scroll horizontally.
cc
Add a first cut of context menus, opened from the "more" (ellipsis)
button on Discover album cards and on album track rows.
- ContextMenu / ContextMenuItem primitives (PopupWindow-hosted panel)
- AlbumCard: the overlay action buttons are now interactive (they gate
their pointer handling so non-hovered cards still open on click); the
more button opens a menu — Open album, Play, Add to queue, Add to
favorites
- TrackRow: a hover-revealed more button opens a menu — Play, Add to
queue, Add to favorites; AlbumTrackItem and TrackData carry the
track id
- Menu actions bubble up as a single media-action(kind, id, action)
callback through the shell; main.rs logs it for now — playback and
favorites wiring lands with the playback session
- user and list-plus icons
cc
Three corrections to the Discover album card:
- Overlay meta: the genre is one point larger and bold, and the year
now sits a single line below it (conditional `if` rows with explicit
heights, instead of `visible` toggles that left a phantom gap)
- Award ribbon: width hugs its label (capped at 200px, below the card
width) instead of spanning the full card — it reads as a ribbon again
- Quality badge: extracted to a QualityBadge primitive that reveals
the exact quality ("Hi-Res: 24-bit / 96 kHz") on hover, mirroring the
Tauri QualityBadge; home.rs derives the label from the discover
audio info
cc
The carousels' clipped, oversized inner track inflated the scroll
content's preferred width, so the vertical ScrollView reserved a
bottom gutter for a horizontal scrollbar that is never shown. Pin the
ScrollView viewport-width to its visible width so horizontal content
can never overflow.
cc
The three header columns shared free space by stretch but kept their
own preferred widths, so the wide tab row pushed the center column —
and the search field with it — off true window center. Anchor the
tabs left and the user element right, and position the search field
absolutely at the window center instead.
cc
Introduce the Settings page, reachable from the Discover gear and the
header menu. Two sub-sections backed by the existing UI-agnostic stores.
- Audio panel: backend, output device and sample-rate dropdowns;
toggles for exclusive mode, DAC passthrough, PipeWire force
bit-perfect, gapless, normalization and quality fallback
- Playback panel: autoplay mode, show context icon, remember session
and resume position
- settings.rs controller owns AudioSettingsStore + PlaybackPreferencesStore
at the shared global path; persists changes and applies audio ones to
the live Player (reload_settings, plus reinit_device for routing-
critical changes); a backend switch re-enumerates output devices
- QbzToggle (switch) and QbzSelect (PopupWindow dropdown) primitives,
SettingRow layout helper, chevron-down icon
- SettingsState global, ContentView::settings, settings-bool /
settings-select callbacks wired through the shell
cc