Local, Tauri-shared playlist folders (library.db) now organize the
sidebar:
- New folders.rs controller (create / rename / delete / move via
qbz-library); sidebar.rs reworked to a flattened entries model
(folder headers + grouped playlists + root playlists) with a
session expand cache, rebuilt without a refetch on expand/move.
- Sidebar renders folders (expandable, with playlist counts) and
playlists (indented inside folders); a hover "..." opens a context
popup: Move to folder (root + each folder) for playlists, Delete
folder for folders.
- Header gains a New-folder button + a CreateFolderModal; folders
reload after create/delete; moves are optimistic + persisted.
Adds folder-open / folder-plus icons.
cc
Wires qbz-library into the Slint app (new library_db module) so the
local, Tauri-shared organization data is read/written from the same
per-user library.db (<data_dir>/qbz/users/<id>/library.db). The DB is
opened per-operation on a blocking thread (rusqlite Connection is
!Send).
- Header collage: the playlist cover is now a 4-up collage of the
first tracks' covers (Tauri parity); falls back to the server
image, then the first track, then a placeholder.
- Custom artwork: owner-only buttons on the cover set/clear a custom
image. Set copies the picked file into <cache_dir>/qbz/artwork and
stores the path in playlist_settings.custom_artwork_path (shared
with Tauri); it's loaded directly from disk and overrides the
collage.
- Edit description: the edit modal gains a description field
(prefilled), saved through update_playlist alongside the name.
cc
Completes playlist CRUD. The detail header shows an edit button only
for the user's own playlists (is-owner = the open id is in the sidebar
list); it opens an EditPlaylistModal:
- Save -> update_playlist (rename); refreshes the detail header name
and the sidebar list.
- Delete -> delete_playlist; closes the modal, navigates back, and
refreshes the sidebar.
Adds the pen-line icon (Lucide) used by the edit button.
cc
- SidebarState/SidebarActions + crate::sidebar: the user's playlists
load into the left-nav list on shell entry; clicking a row opens
the playlist detail view, with the open playlist highlighted
(active-id). Empty state shown when there are none.
- The sidebar "+" opens a CreatePlaylistModal (name field + public
toggle); submit calls create_playlist, refreshes the sidebar, and
navigates to the new playlist.
cc
Connects the previously-inert track context-menu items now that the
backends exist:
- Add to library -> add_favorite("track").
- Go to album / Go to artist -> resolve the track (get_track) to its
album / performer and navigate there.
- Create radio -> Qobuz track radio (new get_radio_track core wrapper
over the existing qbz-qobuz endpoint + playback::play_track_radio).
- Share Qobuz link -> https://www.qobuz.com/track/{id} to clipboard
(new arboard dep + share module).
- Share Song.link -> Odesli (api.song.link) pageUrl resolution to
clipboard.
- Add to playlist -> a global picker modal (PlaylistPickerModal +
PlaylistPickerState/Actions + playlist_picker controller) listing
the user's playlists; picking one calls add_tracks_to_playlist.
Add to mixtape / Make available offline / Track info stay unwired
(no backend yet).
cc
Collapses the three near-identical track rows (AlbumView's inline
TrackRow, ArtistPage's inline TrackRow with multi-select, and the
shared SearchTrackRow) into a single primitives/TrackRow driven by
flags, backed by one TrackItem (superset). Foundation for the
upcoming playlist module, which renders the artwork variant heavily.
- New primitives/TrackRow.slint with: show-artwork (cover+number vs
number cell), show-album, show-favorite, show-download,
multi-select-mode (selection checkbox). One media-action wires
play / play-next / queue / favorite / download / toggle-select +
the TrackContextMenu.
- TrackItem is the unified model (superset of the old AlbumTrackItem
+ SearchTrackItem: number, album, selected, artwork all present;
unused fields stay empty per surface).
- Migrated AlbumView (show-artwork:false, show-download:true),
ArtistPage Popular (show-artwork:false, multi-select-mode),
Search, Favorites, Mix (show-artwork:true, +album/+favorite).
Deleted SearchTrackRow.slint and both inline TrackRows.
- Rust: AlbumTrackItem + SearchTrackItem -> TrackItem across
album/artist/search/favorites/mix/main; search's internal row
struct renamed TrackRow -> TrackRowData to free the TrackRow name
for the Slint component. 46 tests pass.
cc
Brings the JUMP TO bar from a visual placeholder to a working nav,
plus two interaction fixes the user flagged on the previous build.
JUMP TO bar — functional scroll-to:
- JumpNavTab gains an `anchor-y: length` field. JumpNavBar's
tab-clicked callback now emits (id, anchor-y); the parent
(ArtistPageView) scrolls page-flickable to land the section
just below the sticky bar.
- Tab list is built in Rust (`build_jump_tabs` in artist.rs)
from the artist's actual data and pushed to a new
`ArtistState.jump-tabs` model. Tabs are emitted only for
sections that exist (top tracks present, "Discography" /
"EPs & Singles" / "Live Albums" / "Compilations" / "Others"
only when the corresponding release_sections entry is there);
the Playlists tab — not implemented yet — never appears.
- anchor-y estimates use layout-derived constants per section
type (popular-tracks header + visible row count, release grid
headers + ceil(album_count / 5) rows). Not pixel-perfect for
long bios or grids that wrap differently, but lands the
viewport inside the right section.
- Removed the hardcoded ArtistPageView property — tabs and
visibility are now data-driven.
Sticky positioning — dynamic:
- jump-nav.natural-top now binds to
`body-row.absolute-position.y - page-flickable.absolute-y -
self.height`, so the bar always sits flush in the gap above
the body row regardless of artist-header height (long-bio
artists stop pushing the row out from under the bar).
- Sidebar natural-top follows the same body-row anchor, so it
too tracks where the body actually starts.
Search input UX:
- The outer search-icon slot is now removed from layout (via
`if !search-open`, not `visible:`) when the input opens, so
the expanded input runs all the way to the bar's right
padding — right under the user's cursor where the lupa just
was, no leftover gap on the right.
- `changed search-open` auto-focuses the inline TextInput, so
typing starts immediately after clicking the lupa with no
second click in the input.
cc
Three targeted fixes from the latest screenshot.
Body offset:
- The 24px head spacer before the body row only nudged content
past the artist header, leaving Popular Tracks' first row sitting
under the JUMP TO overlay (the play/select/more buttons in
particular were behind the bar). Spacer is now 52px so the body
column begins below the bar's natural y + 44 height + a small
gap.
- jump-nav natural-top tightens from 290 to 291 (purely cosmetic
alignment with the new 52px slot above it). Sidebar natural-top
follows to 335 (= 291 + 44) so it still sits flush below the
bar.
Search input width:
- max-open-width drops from 360px to 216px (60% of the previous
cap). The expanded input now reads as a compact box on the
right of the bar instead of a bar-wide slab.
X close affordance:
- Moves INSIDE the search input on the right side, alongside the
leading magnifier glyph. Clicking it both clears the query and
closes the input (calls back into search-clicked, which the
parent already wires to a reset).
- The standalone X button that used to live on the bar's right
edge is gone — the outer button now only ever shows the search
icon when closed, and hides entirely while the inline input is
open (no two competing search affordances).
cc
Two precision fixes from the latest screenshot pass.
Search input:
- The expanded input was claiming the full center-region width
(everything between the JUMP TO label and the close icon),
reading as a "gigante" slab. Caps the open width at 360px and
keeps the right-edge anchor so it expands inward from the
search icon, matching Tauri's `.search-input-container` shape.
Network sidebar top edge:
- natural-top moves from 320px to 334px (= jump-nav natural-top
290 + jump-nav height 44). The sidebar now starts flush with
the bottom edge of the JUMP TO bar instead of sliding behind
its last 14px ("se come un cachito").
cc
Moves the in-page search out of the body column into the JUMP TO
bar itself, with a width animation so the input slides over the
tabs from the right edge on open (and back to 0 on close), matching
Tauri's `.page-search.open` behaviour.
JumpNavBar:
- New `search-text` (in-out) and `search-text-edited` callback so
the parent owns the query string + filter wiring while the bar
owns the input UI.
- Center region is a single Rectangle whose `clip: true` hides the
expanding search-overlay until it has width. The overlay's
width binds to `search-open ? parent.width : 0px` with a 200ms
ease-in-out animation, so the tabs visually disappear behind it.
- Tabs' TouchAreas are disabled while the search is open, so the
visible click target is just the input.
- Search icon flips to an X glyph and stays accent-tinted while
open, signalling "click again to close".
ArtistPageView:
- The standalone in-page search Rectangle in the body column is
removed. The 24px head spacer stays so the loading state /
Popular Tracks header sit at the same y as before.
- JumpNavBar wired: search-text-edited routes through to
ArtistActions.search; search-clicked toggles search-visible and
also clears the query (and re-runs the empty search) on close
so the filter releases as the input disappears.
cc
Three follow-on refinements after the layout restructure.
JUMP TO bar (`ui/primitives/JumpNavBar.slint`):
- New reusable component — same shape Tauri ships above
section-rich pages (artist, library, future label / musician
views): "JUMP TO" label, horizontal list of tabs with an accent
underline on the active one, search icon on the right.
- Presentation-only — sticky positioning is the parent's
responsibility. Tabs and search icon emit callbacks; reuse by
passing a fresh tab list and routing the callbacks.
Artist page wiring:
- JumpNavBar mounted as an overlay sibling of the page Flickable
with the same sticky math as the sidebar: y =
`max(290px + page-flickable.viewport-y, 0px)`. At rest the bar
sits below the artist header; once the user scrolls past the
header, it pins to the top of the page. (This is the reusable
sticky recipe the user asked for — it's wired here for artist
and lifts straight into the next section that needs it.)
- jump-tabs hardcoded with About / Popular Tracks / Discography /
EPs & Singles / Compilations / Playlists — same labels as
Tauri. tab-clicked updates the active-tab visual state; actual
scroll-to-section wiring is a follow-up (needs anchor positions
that depend on per-artist data).
- search-visible flag flips on the bar's search-clicked. The
existing in-page search input is now gated by it, so the search
is closed by default (matches Tauri's `.page-search.open`
behaviour) and opens on click of the JUMP TO bar's search icon.
Popular Tracks "View less":
- The Load more affordance now flips its label to "View less" once
expanded so users can collapse back to the 5-track preview
without scrolling. A small improvement over the Tauri flow
(which only shows Load more).
cc
- ArtistCard rebuilt 1:1 with Tauri SearchView.artist-card: 160x220
inside a surface-card container, 12px corners, 120px circular avatar
with a gradient placeholder, hover swaps to surface-elevated. The
Follow chip (rectangular per ADR-008) sits below the name when the
user does not follow the artist.
- Search loading state uses a rotating spinner (Lucide LoaderCircle on
Slint transform-rotation) matching the Tauri spinner — 1s linear
infinite. The search header (title + tabs + filter radios) collapses
while the initial load runs so the chrome does not show through.
- Search history: live debounce + submit now use
nav::push_or_replace_search, so the latest query is the single Search
entry in history. Back from an opened album lands on that search.
- Track row backgrounds use the OR-d row-hovered (cell / fav / dl /
more), so hovering the more button no longer drops the row tint.
- replace_category artists arm re-applies the top-result dedupe, so
filtering does not bring the duplicate back.
cc
New LoadingDots primitive (three pulsing dots over an optional label)
shown while a search is running. The result sections fade in once the
results arrive instead of showing empty placeholder colors. The
LoadingDots component is reusable for other heavy loads (large grids,
playlist manager) when they get the same treatment.
The header nav-tab menu now closes after 1s of pointer idle (was 1.5s)
per the project rule that no menu stays open more than 1s idle.
PopupWindow context menus already close on click-outside, which is the
idle behaviour for click-triggered popups.
cc
New shared TrackContextMenu primitive with every option the Tauri
TrackMenu shows (flat — no submenus): Play now / Play next / Add to
queue / Create radio / Add to library / Add to mixtape / Add to
playlist / Share Qobuz link / Share Song.link / Make available offline
/ Go to album / Go to artist / Track info. Items already wired in the
MVP (play / play-next / queue / favorite) fire as before; the rest emit
their media-action and stay no-op until the backend lands. Used by
SearchTrackRow, AlbumPageView TrackRow, and ArtistPageView TrackRow.
cc
- Follow button on the artist card is no longer a pill — Radius.sm
rounded rectangle, matching the rest of the app (new ADR-008).
- PlaylistCard sized identically to AlbumCard (220x266) with the same
220px artwork area; collage and the grid/carousel updated to match.
- Track row play overlay now appears anywhere over the row, not only
when the pointer is directly over the cell. Slint does not propagate
has-hover to ancestor TouchAreas, so each row OR-s its own ta with
every nested TouchArea (play-cell, fav/dl/more) into row-hovered.
- EqualizerBars sit on a fixed 16px baseline so the animation stays
perfectly centered while the bar heights oscillate.
- TrackPlayCell exposes out hovered so rows can OR it cleanly.
- Search dedupes a top-result artist that is also the first carousel
entry, removing the redundant duplicate.
cc
New shared TrackPlayCell for the track-row leading slot, ported 1:1 from
the Tauri track rows: at rest it shows the cover thumbnail or track
number; on row hover a play triangle; for the current track an animated
three-bar equalizer, swapping to a pause glyph on hover. Clicking a
non-current cell plays the track, clicking the current one toggles
play/pause. Wired into the search, album-page and artist-page track
rows.
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
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
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
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
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
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
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
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
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
- Replace every Unicode glyph placeholder with Lucide icons via QbzIcon
(player transport, carousel nav, album-card overlay, album-page
actions, gear, search, checkbox, sidebar).
- Header: the search field is centered to the window width and gets a
search icon; the header and player bar now use the elevated surface,
so all chrome (header, sidebar, player bar) shares one color over the
darker content area.
- The sidebar user card moves to the header as a user element (name +
QBZ logo) that opens a dropdown — Settings, Keyboard Shortcuts,
Documentation, Report an Issue, About, plus Log Out and Close. Log Out
clears the session and returns to login; Close quits the app.
- Content ScrollViews disable the horizontal scrollbar — no view scrolls
horizontally, so the phantom reserved lane is gone.
cc
Adds 28 Lucide icons (the same set the QBZ Svelte app uses) as SVGs
under ui/assets/icons/, plus a QbzIcon primitive — an Image wrapper that
applies `colorize` so a monochrome SVG renders in any theme color.
cc
M1 progress. Adds the foundation token layer (palette, semantic
colors, spacing, radius, typography) bound to the QBZ dark theme,
three primitives (QbzCheckbox, QbzPrimaryButton, QbzTextLink), and
the LoginScreen feature component.
The login screen mirrors the Svelte LoginView: centered card, logo,
QBZ wordmark, Qobuz ToS checkbox, "Sign in via Browser" button,
system-browser and offline links, and the legal disclaimer. Colors
are lifted verbatim from src/app.css; strings use @tr() markers.
Rust side: typed AppCommand enum; UI callbacks dispatch through it.
M2 wires the command handler to AppRuntime.
cc