Move map_album (V2 nested-first with flat fallback), tier/tier_hires,
release-type classification, to_item and sort_album_items out of
label.rs into a shared album_map module. favorites.rs now maps albums
through it too, so favorite album cards gain the AlbumListRow extras
(release_type / quality_detail / track_count / plain_year) and the
hires-without-bit-depth tier fallback — one implementation to maintain
ahead of the Favorites album list/grid toolbar port.
cc
/label/getAlbums returns the V2 nested album shape (DiscographyAlbumDto:
artists[], audio_info, dates, track_count) — confirmed against the Android
decompiled sources — but it was deserialized into the flat Album model,
which dropped artist, quality and release date (Unknown Artist in
group-by-artist, no quality badge, no date, empty list-row columns).
qbz-models Album: capture the nested V2 fields (audio_info, dates,
track_count, release_type) as optional serde(default) — backward
compatible with the flat-shape endpoints.
label.rs map_album: read nested-first (quality from audio_info, date from
dates, count from track_count, artist from artists[] main-artist, explicit
release_type) with flat fallback. Fixes the releases grid + list columns,
the landing Releases carousel, and Critics' Picks.
Wire ("album","favorite") -> add_favorite + toast (works on every album
card via the shared component).
LabelReleases load-more now driven by has_more (the endpoint caps each
page well below the catalog, so total was an unreliable gate).
cc
CHECKPOINT — this batch is NOT yet acceptable; substantial feedback is pending. Committed so the work is preserved for rework next session, after a max-effort Tauri code review. Areas: TrackRow favorite toggle/state + clickable artist/album (TrackItem gains is-favorite/artist-id/album-id + fav_cache, populated across all builders); PlaylistView header buttons + cover-art hover overlay; LabelView full AlbumCard + grid/list toggle; vertical scrollbar moved to the right gutter across ~12 views.
cc
Marks every non-functional Appearance row (Theme, Language, Font, UI Scale, notifications, window-title, player views, system tray, immersive) with the SettingRow 'unsupported' red label. Only the wired 'Show playlist cover collages' and the 'Detected' status row stay normal. Red = not actually connected, so what still needs wiring is visible at a glance.
cc
Add a SettingRow 'unsupported' flag that renders the label red. Mark the Title Bar and Window Controls sections: that Tauri custom-window-decoration paradigm does not carry over to Slint's TitleBar/Plasma chrome.
cc
Playlist Manager: list view renders compact rows with total play time; folder grid cards no longer clip the playlist count (content-driven height); list view shows compact folder chips; the move-to-folder menu gains a folder search and a 4-row scroll cap. Search: the most-popular track hero is resized to the artist-card height with the quality shown as centered text instead of a clipped badge.
cc
Mirrors the play-cell click on the row body: double-clicking a track plays it, or toggles play/pause when it is already the current track. Applies to every TrackRow (album, artist, search, playlist, mix).
cc
Sidebar:
- Fix "Recursion detected" panic from focusing the playlist search input in init (defer via a single-shot Timer).
- Right-click context menus: playlist (Edit, Hide from sidebar, Move to folder with folder search + Move to root) and folder (Edit, Hide from sidebar), built on ContextMenu/ContextMenuItem; remove the unreachable hover "..." button.
- Filter hidden playlists/folders out of the list; add folders::set_folder_hidden.
- Folder list in the move-to-folder menu scrolls (cap 5 rows); close-on-click-outside so the folder-search input no longer dismisses the menu.
- Sort-menu Refresh now uses a dedicated refresh-cw icon.
Toast: in-app ToastKind/ToastState + Toast overlay mounted at the app root, crate::toast (show/hide, per-kind auto-hide via a reusable Timer, cross-thread *_weak helpers); surface play album/artist load failures.
SearchView: title inline with NavButtons + searchType filters floated right; tab strip on its own row; Most-popular hero for a track uses the new SearchTrackHero, sized to the artist card.
Playlist Manager: list view renders compact PmListRow rows instead of cards; grid/folder card fonts tightened to match Tauri; folder card centered with hover-reveal edit; search input narrowed to 70%.
cc
Port the Playlist Manager: header/toolbar (search/filter/sort/folder-flat
toggle/view-mode/new-folder/counter), FOLDERS card grid (accent icon, count,
edit), PLAYLISTS grid/list/tree, folder CRUD (FolderEditModal), move-to-
folder, per-card actions, and arrow-button reorder (custom sort). Reached
from the sidebar "Manage playlists" item; backend 100% reused via
QbzCore/library.db (no new commands). Pointer-event drag-and-drop and
gradient folder colors are deferred follow-ups.
This commit also carries the cross-cutting wiring the session's features all
share — state.slint globals/structs, NavEntry variants, main.rs dispatch +
action wiring, artwork targets, app.slint exports, AppShell mounts — so HEAD
is the verified-green tree (cargo check + release build clean).
cc
Add an Appearance sub-section to the Slint settings, mirroring Tauri's:
theme + auto-theme, font/UI-scale/language, startup page, sidebar playlist
collage (wired to the sidebar flag), notifications/toasts/tray, title bar +
window controls, immersive + miniplayer. Controls reuse QbzToggle/QbzSelect
and the AudioSettings/PlaybackSettings layout. Most controls are visual
stubs (AppearanceState + TODO callbacks) pending backend wiring.
cc
PLAYLISTS header: smaller title, recursive search (hides title), create
modal at 1:1 with Tauri (name/description/folder/public), sort menu, and a
collapse chevron. Folders: accent-tinted icon, open/close chevron as the
primary affordance (no more purposeless "..."), count shown on hover only.
Playlists render a 2x2 micro-collage of track covers (opt-out) instead of
the glyph, sourced from the existing playlist images (no extra fetch).
cc
Per-module discover See-All pages (new releases, press accolades, ideal
discography, qobuzissimes, albums of the week, most streamed) reachable
from each carousel's "View all" link: a paginated grid OR list view with
client-side search, genre filter, and a fixed header. Adds a reusable
AlbumListRow (columns #/ITEM/TYPE/SOURCE/QUALITY/TRACKS/YEAR, SOURCE
hideable, reuses QualityBadgeFull) with play-on-hover + even/odd zebra.
Recently Played albums now carry genre + localized release date + quality
(captured at play time, no extra fetch). Shared home::map_album /
card_to_item reused; grid-only AlbumCardItem literals get ..Default.
cc
Album-card hover overlay now shows the full release date as "MMM D, YYYY"
(e.g. "Nov 6, 2025") instead of the year alone. New dates::release_label
helper keeps the month/day/year order fixed but localizes the month via
chrono format_localized (the app ships 5 languages; current_locale() is the
single hook to thread the real UI language once Slint wires translations —
English for now). Applied to the Discover and Favorites album mappings.
Also fixes Favorites > Albums cards, which hardcoded an empty genre and a
year-only label: they now read album.genre and the full date, matching the
Discover cards 1:1. The data was already in the getUserFavorites response
(qbz-models Album parses genre + release_date_original) — no backend change.
cc
Each favorites tab is now its own history entry (NavEntry::Favorites{tab},
mirroring Discover) recorded from both the header nav and the in-view tab
bar, so back/forward moves between tabs instead of exiting Favorites. Add
the back/forward NavButtons at the same position as every other page
(11px top padding + 22px gap) — FavoritesView was the only view missing
it — and shrink the "Favorites" heading to Typography.section (the album
name size in AlbumView).
cc
The clickable-artist change bound the artist TouchArea width to the artist
text's preferred-width, which clamped the whole stretch:1 title column to
the (often short) artist name — truncating titles to a few letters and
shoving the quality badge inward instead of letting it float right. Drop
the explicit width; the inner Text fills the column (width: 100%) and elides.
cc
Port #470's UI behavior: add an "Auto" entry at the top of the backend
dropdown that, on select, resolves to the best available backend (PipeWire
if present, else System) and persists that concrete value. The rebuilt
snapshot then moves the dropdown onto the resolved backend, so backend_type
is never left null/Auto. Concrete backends keep mapping by their shifted
index; System wiring comes from available_backends() (merged backend half).
cc
Polish + consistency:
- Compact playlist header (cover 150px; nav->header spacing
homologated with AlbumView); description truncated to ~2 lines +
"Read more" modal; custom-artwork buttons only on cover hover.
- New Typography.section (18px) token for hero/section titles,
applied across Album / Artist / Mix / Playlist + Home carousels.
- Shorter track rows app-wide (50px row, 36px artwork, -1 fonts);
+3px column-header/divider gap; wider track-number column.
- Reusable ExpandableSearch (the JumpNavBar page-search pattern:
magnifier -> opens left -> X to close) in the playlist action row.
- Custom ListScrollbar (thicker, min thumb height, own gutter) since
Slint's ListView scrollbar can't be styled.
Sort:
- Default / Title / Artist / Album / Duration with asc/desc per
field, plus Custom (manual order persisted in library.db with
up/down reorder). Client-side search + sort combined.
WIP: track drag-and-drop onto sidebar playlists
- DragState/DragActions globals, drag source on the shared TrackRow,
follow-cursor DragGhost, sidebar playlist rows self-detect the drop
via absolute-position, multi-select group drag, add_tracks_to_playlist
on drop, Flickables freeze scroll during a drag.
- KNOWN ISSUE: the ghost is offset from the cursor (the row
absolute-position + pointer mapping is off, esp. inside Flickables).
Diagnostic logging is left in (TrackRow debug() + a start log) to
pin down the wrong term before the generic fix.
cc
Large playlists (2000+ tracks) were sluggish. Addressed the root
causes; this strategy is reusable for the library / mixtapes / tracks
views.
- Virtualization: the track list is now a ListView (only visible
rows are instantiated) instead of a Flickable with a for over all
rows. The header + column header are pinned above it.
- Smaller images: track rows use ImageSet::smallest() (new) instead
of best() — they are 40px thumbnails, so best() was downloading
mega/large covers.
- Per-target decode size: row thumbnails decode to 96px instead of
the 264px card size, so a 2000-row model holds ~75MB of pixels
rather than ~550MB.
- Track-number column widened to fit 4-digit positions.
- Back-to-top button: appears once scrolled past one viewport
(bottom-right), scrolls the list home. A shared rule for big lists.
- Custom-artwork buttons only show on cover hover.
cc
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
The playlist detail view gains an edit mode (owner-only), reusing
TrackRow's multi-select-mode:
- A "Select" toggle in the header turns on per-row checkboxes and a
bulk action bar (N selected, Select all, Remove).
- toggle-select is now context-aware (playlist vs artist Popular);
PlaylistState tracks selected-count for the bar.
- Remove calls remove_tracks_from_playlist with the selected ids,
then reloads the playlist and leaves edit mode.
Track reordering is the remaining playlist-editing piece (deferred —
it is the most involved).
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
First piece of the playlist module: a detail view at the AlbumView /
MixView level, reusing the shared TrackRow (show-artwork + show-album +
context menu, so Add to playlist / Go to album / etc. work in it).
- PlaylistState + crate::playlist controller (get_playlist -> header
metadata + TrackItem rows; cached track list backs play-all /
per-track play; artwork jobs resolve row covers + the header cover).
- PlaylistView.slint: cover + "Playlist" label + name + description +
"owner - N tracks - duration" info line + circular actions (Play
all, Shuffle, Favorite) over a column-headed track list.
- ContentView.playlist + NavEntry::Playlist (back/forward history) +
navigate_playlist; the Favorites > Playlists card now opens it.
- media-action: playlist play-all / shuffle / favorite (add/remove
favorite) + per-track play-from-here.
cc
Ports Tauri's behavior where clicking an album card's artist name
navigates to that artist (the card body still opens the album).
- AlbumCardItem gains artist-id; populated in every album-card
constructor (home/search/label/foryou/favorites). Artist-page
release cards leave it empty (their subtitle slot shows the year,
not the artist, so it stays non-clickable).
- AlbumCard wraps the artist name in a TouchArea that emits
media-action("artist", artist-id, "open") and brightens on hover
only when an id is known.
- main.rs handles ("artist","open") -> open the artist page;
FavoritesView (which only forwarded clicked) now forwards the
artist action.
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
Track row reuse: the mix views had a bespoke MixTrackRow with a dead
favorite/more controls (no context menu), while search / favorites /
artist already share SearchTrackRow (which has the TrackContextMenu).
Consolidated:
- SearchTrackRow gains optional show-album (album column) +
show-favorite (standalone heart) — off by default, so search /
favorites rows are unchanged. SearchTrackItem gains an `album`
field.
- MixView drops MixTrackRow/MixTrackItem and renders SearchTrackRow
with show-album + show-favorite. A row "play" starts the mix from
that track; the context-menu actions (play next / queue / favorite
/ view album / view artist / ...) now bubble through media-action
and work in the mix views too.
For You skeleton: the For You tab showed a bare spinner during its
lazy load; it now shows the same pulsing HomeSkeleton as Home (the
static Qobuz Mixes tiles stay visible above it). Editor's Picks is
served from the shared discover-index cache (Tauri parity) and shows
the skeleton during the initial load via the existing HomeView guard.
cc
The mix detail pages were a thin header + flat track list. Rebuilt to
mirror Tauri's playlist-detail layout at the AlbumView port quality.
- Header: a per-mix gradient artwork (224px, same gradients as the
QobuzMixesRow tiles) + "Qobuz Mixes" label + title + description +
"N tracks • duration" info line + circular action buttons (Play
all, Shuffle, Save-as-playlist, Refresh) — matching AlbumView's
header anatomy.
- Track list with a real column header (#, Title, Album, Duration,
Quality) over rich MixTrackRows: artwork thumbnail + number/play
cell (TrackPlayCell, artwork-on), title + explicit badge, artist,
album column, duration, quality, favorite + more controls.
- MixTrackItem gains an album column; MixState carries track-count +
total-duration for the info line.
- Play all / per-track play (from that track), Shuffle (plays a
random order without mutating the list), and Refresh (re-fetches)
wired through media-action.
Fixes a wiring bug: the tile click sends id=kind/action="open", but
the handler matched on the action and passed "open" as the mix kind —
now it reads the kind from id.
cc
The For You Qobuz Mixes tiles now open functional mix detail pages.
- New MixView: header (mix name + description + Play all) over the
mix's track list (reusing SearchTrackRow). Clicking a track plays
the whole mix from there; Play all starts at the top.
- crate::mix resolves each mix to a track list from available
backend: DailyQ/WeeklyQ -> /dynamic/suggest seeded from the local
play-history track ids; FavQ -> favorite tracks shuffled (xorshift,
no rng dep); TopQ -> tracks aggregated from the user's playlists.
The loaded list is cached so play-all / play-from-track build the
queue without re-fetching.
- Backend: ported /dynamic/suggest into qbz-qobuz
(QobuzClient::get_dynamic_suggest — POST, items parsed leniently
into Vec<Track>) + QbzCore::get_dynamic_suggest.
- ContentView.mix + NavEntry::Mix { kind } so each mix is a history
entry; the tile click records + navigates. New MixTrack artwork
target. playback::play_tracks generalizes the radio queue builder
with a start index (radio reuses it at 0).
Tauri's exact generation (listened-track analysis payloads, playlist
play-stats ranking) is approximated; same surfaces + playback, sourced
from the backend the MVP has.
cc
Ports the Tauri radio_engine into a standalone, frontend-agnostic
crate so any client (Slint, TUI, CLI) can build and play a smart
radio without Tauri.
- New crate qbz-radio (builder + db + engine + tests), copied
verbatim from src-tauri/radio_engine with the only change being
the dependency rewrite: crate::api::{QobuzClient, Track,
TracksContainer} -> qbz_qobuz::QobuzClient + qbz_models. Depends
only on qbz-qobuz + qbz-models + rusqlite + dirs. All four engine
tests pass.
- QbzCore::create_smart_artist_radio builds the session pool + pulls
up to 50 track ids + resolves them to full tracks. The radio DB's
rusqlite Connection is !Send, so the build + pull run on
spawn_blocking threads (the async builder driven via block_on),
mirroring the Tauri command.
- The artist "radio" action (Spotlight RADIO card + artist radio)
now uses the smart builder (playback::play_smart_artist_radio);
album radio (Radio Stations) keeps the Qobuz /radio/album
endpoint, so both radio systems are in use. play_artist_radio
(Qobuz /radio/artist) is retained as the simpler alternative.
cc
Completes the remaining For You sections by porting two Qobuz API
endpoints down into qbz-core and adding the local "forgotten
favorites" approximation.
Backend ports (qbz-models + qbz-qobuz + qbz-core):
- /album/suggest -> AlbumSuggestResponse, QobuzClient::get_album_suggest,
QbzCore::get_album_suggest.
- /radio/artist + /radio/album -> RadioResponse, get_radio_artist /
get_radio_album on the client + core. (Faithful ports of the legacy
src-tauri/api/client.rs methods — same paths, params, shapes.)
For You sections:
- Radio Stations — album-seeded radio tiles (recent + favorite
albums, deduped, capped 12), rendered with RadioCard; click starts
a real Qobuz album radio.
- More From Your Library — /album/suggest seeded from the most recent
played album (else first favorite).
- Rediscover Your Library — favorite albums not in the local
play-history (approximation of Tauri's reco-DB forgotten
favorites).
- Spotlight's RADIO card + the artist "radio" action now create a
real Qobuz artist radio (playback::play_artist_radio /
play_album_radio build a queue from the radio track list and play).
Section order now matches Tauri: Qobuz Mixes, Release Watch, Radio
Stations, Recently Played Tracks/Albums, Your Top Artists, More From
Your Library, Rediscover, Artists to Follow, Spotlight.
Still backend-blocked: the Qobuz mix detail views (DailyQ/WeeklyQ/etc.)
and the smart radio_engine (the Qobuz radio used here is the simpler
/radio/* endpoint, not the local pool builder).
cc
Ports Tauri's QobuzMixesRow as the first For You section — four
220px navigation tiles (DailyQ / WeeklyQ / FavQ / TopQ) with the
1:1 gradients, badge, name, and description. Pure navigation, no
per-tile data. The mix detail views they open don't exist in the
Slint MVP yet, so the click is logged for now.
cc
Ports Tauri's SpotlightLite — one favorite artist highlighted with a
hero and a content row. Backed by get_artist_page.
- Spotlight component: header (title + "Shine a light…" subtitle),
hero (round portrait + ARTIST label + name + play-top-tracks /
open-artist circle buttons), and a draggable Flickable row
mixing a TOP TRACKS card, a RADIO card, and the artist's albums.
- New RadioCard (port of RadioCardLite): a 180px tile with the seed
artwork centered on an elevated panel, a wordmark label over it,
and a title/subtitle below; hover reveals play. (The Tauri
palette-derived panel colour is approximated with a neutral
elevated panel.)
- foryou::load_spotlight rotates among the top-5 favorites by
wall-clock seconds, fetches get_artist_page, and extracts up to 6
albums (album > live > ep-single > compilation, deduped) plus
whether the artist has top tracks. ForYouState gains the
spotlight-* fields; two new artwork targets feed the portrait +
album covers.
- Wiring: ForYouView renders Spotlight after Artists to Follow.
play-top-tracks routes to the existing artist play-top action;
"start radio" has no artist-radio backend yet so it falls back to
playing top tracks (logged).
Remaining For You: Radio Stations section (old card design), Qobuz
Mixes row, and the reco-DB sections (More from library, Rediscover).
cc
Replaces the ad-hoc For You (a single album set crammed into
HomeState.sections) with a dedicated, ordered ForYouView/ForYouState
so the tab can mix album, track, and artist carousels — closer to
Tauri's For You. Lazy-loaded once on first open.
Sections (all reuse existing cards, no new card components):
- Release Watch (album Carousel) — get_release_watch.
- Recently Played Tracks (SlimCarousel) — local play-history.
- Recently Played Albums (album Carousel) — local play-history.
- Your Top Artists (ArtistCarousel) — the user's favorite artists
via get_favorites.
- Artists to Follow (ArtistCarousel) — similar artists seeded from
a few favorites via get_similar_artists, excluding ones already
followed (Follow chip shown).
Plumbing:
- crate::foryou controller (load/apply/artwork). Five ForYou
artwork targets feed the dedicated models.
- select_tab("forYou") clears HomeState.sections (the home-content
`for` renders nothing) and the For You content comes from
ForYouView; HomeView routes the For You tab to ForYouView and
gains an open-artist callback (wired through AppShell).
- ensure_for_you_loaded lazy-loads on tab open (pill, header
dropdown, and back/forward history).
- home.rs no longer builds a For You section set (removed the
redundant release-watch/favorites fetch on every home load).
Still to come (next increments): Qobuz Mixes row, Radio Stations
(old card design), Spotlight hero, and the reco-DB sections (More
from library, Rediscover) — those need backend the MVP lacks or the
mix detail views.
cc
Home / Editor's Picks / For You were plain tab swaps with no history
footprint. Each is now a NavEntry::Discover { tab } pushed on click
(both the in-view pill and the Discover header dropdown), so
back/forward navigates between the three Discover pages. apply_entry
replays a Discover entry by selecting that tab's cached section set
and re-firing artwork.
cc
The popup root Rectangle set width but no height, so as a direct
child of the home overlay it defaulted to the parent's full height —
the panel ballooned vertically. Bind height to the content layout's
preferred-height so it hugs its content.
cc
For You was a single Release Watch row. Adds the personalized album
sections the Slint MVP can source today, matching Tauri's forYou
defaults:
- Release Watch (followed-artist new releases) — kept.
- Favorite Albums — the user's hearted albums via get_favorites.
- Recently Played Albums — from the local play-history store
(carries the stored quality badge).
The remaining Tauri For You sections — Qobuz mixes, similar albums,
rediscover library, essentials by genre, artists to follow, artist
spotlight, continue listening — depend on the reco-DB / home-resolved
backend (v2_reco_*) that the Slint MVP does not have yet, plus an
artist-tile section type for the follow/spotlight rows. Those are the
next increment.
cc
Adds the advanced view to the Discover genre filter and recovers the
child-genre filtering the discovery-v2 home redesign dropped.
Child-genre recovery:
- The old (pre-redesign) home expanded a parent selection to its
descendant genre NAMES and filtered content client-side; the
discovery-v2 redesign passed only the explicitly-selected ids to
the discover index, so selecting a parent stopped covering its
children. Here filter_ids() expands the selection to all loaded
descendant ids and feeds that to get_discover_index; selecting a
genre eager-loads its descendant subtree so the expansion is
complete.
Advanced view:
- Search box + a scrollable 3-level genre tree (parent / child /
grandchild), flattened in Rust into visible GenreTreeRow rows
honoring per-node expansion. Each row: indent by level, expand
arrow (lazy-loads children on first expand), checkbox, name, and
loaded child count. Search returns a flat match list across all
loaded genres.
- Opening the advanced view eager-loads every parent's children so
counts show up front; grandchildren stay lazy until a child is
expanded.
State/wiring:
- GenreTreeRow struct; GenreFilterState gains tree + search-query;
GenreFilterActions gains toggle-expand + search.
- genre_filter STATE moves to LazyLock (HashMap/HashSet aren't
const) and gains children map, expanded set, and search query.
- main.rs wires toggle-expand (lazy child load), search, and the
advanced-open eager child load; toggle now eager-loads
descendants before the re-fetch.
cc
Ports Tauri's Filter-by-genre popup (simple grid view). The selection
is shared across the three Discover tabs — matching Tauri's single
"home" genre context — and re-fetches the discover index with the
chosen genre ids.
UI:
- New GenreFilterPopup: header (icon + title + close), Remember
selection + Advanced view toggles (QbzToggle), a 3-column grid of
parent-genre chips with an accent fill + check circle when
selected, and a Clear filter footer. Anchored top-right under the
button, with a click-away backdrop.
- GenreButton turns accent-filled and shows "N genres" / "1 genre"
when a filter is active (else "Filter by genre"), 1:1 with Tauri.
- Advanced view (search + 3-level tree + counts) is the next
increment; its toggle is present and shows a "coming soon" stub.
State / backend:
- GenreFilterState + GenreFilterActions globals; GenreChip struct.
- crate::genre_filter loads the parent genres (QbzCore::get_genres)
and owns the shared selection, persisted to
<data-dir>/qbz/genre_filter.json when Remember is on (dropped
when off). Stale ids are pruned against the loaded genres.
- load_home now takes genre_ids and threads them into
get_discover_index. The home-load path is extracted into a shared
`reload_home(active_tab)` used by the initial shell load and by
genre toggle/clear; it rebuilds all three section sets, restores
the active tab via select_tab, and re-fires artwork.
cc
Applies the same Flickable drag pattern to SlimCarousel (Recently
Played Tracks, Popular albums). The 4x3 page blocks now live inside a
horizontal Flickable — each block is one viewport width wide, cards
positioned absolutely within it — so the grid drags between pages
with kinetic momentum while slim-card taps still register. prev/next
nudge viewport-x by a page width; enabled state derives from
viewport-x. Two-page (24-item) cap unchanged.
cc
Introduces the horizontal drag concept (to be reused widely) on the
discover album Carousel. Replaces the manual page-slide (a clipped
Rectangle translated by -page*step) with a horizontal Flickable:
the row now drags by touch/pointer with kinetic momentum, and card
taps still register because Flickable distinguishes a flick from a
tap — unlike nested TouchAreas, where the card's own TouchArea would
swallow the drag.
- viewport-height is pinned to the row height so the Flickable
only scrolls horizontally and never fights the home page's
vertical Flickable.
- prev/next buttons nudge viewport-x by one page width (clamped to
[-max-scroll, 0]); enabled state + the edge fades derive from
viewport-x.
- No animate on viewport-x — that would rubber-band the per-frame
drag updates; button jumps are instant in exchange for smooth
dragging.
cc
The play-history store kept only id/title/artist/artwork, so the
Recently Played Albums cards rendered bare (no quality badge) and a
re-play had no stored context to fall back on.
RecentTrack now also persists quality_tier, quality_label and
artist_id; RecentAlbum carries the quality through to its card.
record_recent derives the quality from the live queue track's
bit_depth / sample_rate (same Hi-Res/CD badge format as the discover
cards) and copies its artist_id. home.rs maps the stored quality
onto the Recently Played Albums card so the badge now shows.
cc
Three of the four Discover > Home quick-wins.
Loading skeleton (#1):
- New HomeSkeleton replaces the spinner + "Loading your music…"
text shown while the first fetch is in flight. Grey placeholder
blocks shaped like two real carousels (title bar + a row of
square art with two text lines), gently pulsing via a shared
900ms Timer phase + animate opacity — no more artifact titles
during load.
Recently Played Tracks + Popular albums (#3, #4):
- New SlimCarousel: a paginated 4x3 slim-card grid (12 per page),
capped at two pages, sliding one viewport width per page. The
prev/next controls only appear when there is a second page
(>12 items); 12 or fewer renders as a single static page with
no arrows. Replaces the old non-paginated GridSection that grew
into a tall wall of rows.
- home.rs caps `recent` and `popular` to 24 items (two pages) so
the carousel never has hidden overflow; the Popular/Recent
artwork-job indices are unchanged.
GridSection is retained — the artist ReleaseGrid still uses it.
Note on #2 (Recently Played Albums "spurious AlbumCard"): there is
only one AlbumCard component (discover/AlbumCard.slint, used by 5
call sites incl. this carousel via Carousel) — no duplicate to
delete. The cards look bare because the local play-history store
keeps only id/title/artist/artwork (no quality/year/ribbon), not
because of a different component.
cc
Makes the three-tab Discover pill (Home / Editor's Picks / For You)
functional, plus the matching Discover header-dropdown rows. Mirrors
Tauri's DiscoveryView: all tabs draw from one fetch and swap which
curated section set is shown — no extra round-trips on tab switch.
home.rs:
- load_home now builds three section sets from a single
get_discover_index call (cloning the editorial containers):
* home — the existing balanced set.
* editorPicks — editorial only (New Releases, Qobuzissimes,
Press Accolades, Most Streamed, Ideal Discography, Albums of
the Week), ordered per Tauri's DEFAULT_PREFS.editorPicks.
* forYou — Release Watch (get_release_watch "artists"). The
deeper personalized sections (Qobuz mixes, similar albums,
spotlight) need reco-DB backend the Slint MVP lacks, so this
is the honest first cut.
- apply_home caches all three sets in a thread-local; select_tab
swaps HomeState.sections to the chosen set and returns the
Section-targeted artwork jobs to re-fire. Reuses the existing
Section artwork target — no new targets.
- map_full_album maps release-watch's catalog Album to a card.
Slint:
- HomeState gains active-tab; HomeActions.select-tab wired to the
pill (active state derived from active-tab) and to the Discover
dropdown rows (route "discover-<tab>") via header-menu-navigate.
- The Home-only sections (Recently Played albums/tracks, Popular)
are gated to active-tab == "home" so Editor's Picks / For You
render just their own set.
cc
Completes the five Favorites tabs. Both lazy-load like the others.
Playlists:
- Sourced from QbzCore::get_user_playlists (the getUserFavorites
envelope has no playlists branch; Tauri splits owned-vs-following
locally — the MVP shows the full user playlist list for now).
- FavoritePlaylistItem card: cover (images300 > images150 >
images, first entry), name, "N tracks". Cover streams via the
artwork pipeline (new ArtworkTarget::FavoritePlaylist). Click is
logged — no playlist detail view in the Slint MVP yet.
Labels:
- Parsed from get_favorites("labels"); the qbz-models Label is
just {id,name}, so a local FavLabel shape picks up the
albums_count. FavLabelRow (icon + name + "N releases") routes
the click into the existing LabelReleasesView via
FavoritesActions::open-label -> navigate_label.
FavTab gains Playlists + Labels; from_route/from_tab_id now resolve
them so the header dropdown and tab bar both lazy-load. Tab bar
shows live counts for all five tabs.
Still deferred (per "finish then polish one by one"): the richer
Tauri options (sort, group-by-artist, owned/following split,
per-row context menus, bulk select, download status).
cc
Wires the Library header dropdown to a real Favorites view. Mirrors
Tauri's FavoritesView.svelte core: a tab bar over per-tab content,
each tab lazy-loaded on first open.
Slint:
- ContentView gains `favorites`. NavMenuEntry gains a `route`
field; the Library dropdown rows carry "favorites-tracks" /
"-albums" / "-artists" / "-playlists" / "-labels". The
HeaderMenuOverlay click bubbles the route up through AppShell ->
AppWindow -> Rust (new header-menu-navigate callback).
- FavoritesState / FavoritesActions globals. Tracks reuse
SearchTrackItem (so the shared SearchTrackRow renders them with
per-row cover art), albums reuse AlbumCardItem + the Discover
AlbumCard, artists use a new FavoriteArtistItem round-portrait
grid.
- New ui/favorites/FavoritesView.slint: title, tab bar with live
counts + active underline, and the three populated tabs.
Playlists / Labels show "Coming soon" pending their backing
views. AppShell routes ContentView.favorites.
Rust:
- crate::favorites controller — load_favorites parses the raw
get_favorites JSON (`{ <type>: { items, total } }`) into typed
qbz-models Track / Album / Artist, maps to the Slint structs.
Lazy per-tab: select-tab and the header route both call
navigate_favorites, which fetches only the opened tab.
- artwork::ArtworkTarget gains FavoriteTrack / FavoriteAlbum /
FavoriteArtist; covers stream in via the shared pipeline.
- main.rs: navigate_favorites + on_header_menu_navigate +
FavoritesActions (select-tab / open-album / open-artist /
play-track / track-action). No new qbz-core method —
get_favorites already existed.
Deferred: Playlists + Labels tab content, full pagination (one 500
page is fetched per tab), favorites preferences (tab order).
cc
Wires the Origin section's location link to the scene-discovery
view. Completes the three artist-network click targets (Musician,
Label, Location).
Slint:
- ContentView gains `location`; NavEntry mirrors with the full
`Location { mbid, area_id, area_name, country, genres, tags }`
payload so back/forward replays the scene.
- LocationViewState / LocationViewActions globals carry the scene
header (label + genre summary) and a paginated artist grid
(LocationArtistItem: qobuz id, name, genres line, image).
- New `ui/location/ArtistsByLocationView.slint`: a scene header
(map-pin + location + genre summary) over a wrapping round-
portrait artist grid with load-more. open-artist routes the
card click to the artist page.
- AppShell routes ContentView.location; new map-pin.svg (Lucide).
Rust:
- `crate::location_view` controller — load_scene, apply_scene,
append_scene, reset_scene, artwork_jobs. Maps LocationCandidate
rows to the grid (qobuz_name fallback to mb_name).
- artwork::ArtworkTarget gains LocationArtist { index }.
- The Origin metadata load now caches LocationParams (area +
affinity seeds) in a cross-thread Mutex; reset_network_sidebar
clears it so a stale click can't fire for the wrong artist.
artist::location_params() exposes it to the click handler.
- main.rs: navigate_location + apply_entry route +
on_location_clicked (reads the cached params) +
LocationViewActions open-artist / load-more.
Backend (prior commit) supplied discover_artists_by_location;
this commit is the Slint + wiring layer.
cc
Wires the artist network sidebar's Labels section to a real
destination. Mirrors Tauri's LabelReleasesView.svelte core: a label
header (image + name + release count) over a wrapping album grid
with load-more pagination. (Sort / filter Hi-Res / group-by-artist
from the Tauri view are deferred — the core browse experience is in.)
Slint:
- ContentView gains `label`; NavEntry mirrors with
`Label { id, name }`.
- LabelState / LabelActions globals carry the header (id, name,
image-url + decoded image) and the AlbumCardItem grid + total.
- New `ui/label/LabelReleasesView.slint`: label header card,
then the same absolute-position album grid + load-more pattern
the artist/musician views use. Reuses the Discover AlbumCard
(artist-as-subtitle is correct here — a label's catalog is
various artists).
- AppShell routes ContentView.label and forwards open-album +
media-action.
Rust:
- `crate::label` controller — load_label (parallel-ish
get_label_page + get_label_albums), load_more_albums,
apply_label, append_albums, apply_image (label header), and
artwork_jobs. extract_label_image handles the flexible
/label/page image value (string or mega/extralarge/.../small
object), same precedence as the Svelte.
- artwork::ArtworkTarget gains LabelAlbum { index }.
- main.rs: navigate_label + apply_entry route + on_label_clicked
(sidebar) + LabelActions::on_load_more.
- No new qbz-core methods — get_label_page / get_label_albums
already existed.
cc