Shell & navigation:
- Three-state sidebar (open/mini/closed); collapse cycles via header button.
- Section nav as right-flyouts (sidebar) + compact header nav (closed) +
full header tabs (nav-in-sidebar off), via a shared HeaderMenuOverlay.
- Collapsed-folder right-flyout popup (max 4 + scrollbar), Rust-populated.
- Thin auto-hiding sidebar scrollbar pinned to the edge (Flickable, drag-safe).
- Slack-style bezel: rounded inset content panel over surface-card chrome.
- Header restructure: nav buttons, window-centered search, app menu.
Themes & components:
- Live theme switching: Dark / OLED Black / Tokyo Night / System (std-widgets).
- SegmentedTabBar (Discovery-style) with count badges for Library + Local Library.
- MP3 quality-badge tier (format-first classification) across badges.
- QbzSelect (Group/Sort) trimmed 20px.
Behavior fixes:
- Artist + Local Library Folder cover art now reach the rendered (visible/
grouped) models, not just the source set.
- Favorites Playlists/Labels random buttons.
- Playlist track click plays from the clicked track in visible order.
- Dropdown idle-close pauses while the cursor rests on the menu.
cc
Albums tab — full 1:1 with Tauri in one pass:
- Switch from the chunked-paginated grid to a full-load + in-memory derive so
grouping/filtering span the whole set. derive_albums = search + sort + group
(off / by letter with A-Z jump / by artist, both with section headers) + the
quality/format/source filter (1:1 with matchesQualityFilters: OR within a
group, AND between groups; empty group passes).
- Grid/list view toggle; the list view shows a SOURCE column (Local/Offline/
Plex), placed after TYPE. Filter popup now FLOATS (sibling of the content
layout, not a layout slot) with an active-count badge. Fixed the year sort
(sort_album_items maps year-asc/desc — was also silently broken in Folders).
- Album covers dual-set by id across the full set + visible + grouped sections.
Dedicated local album view — SEPARATE from the Qobuz AlbumView:
- New ContentView::local-album + NavEntry::LocalAlbum + ui/album/LocalAlbumView.slint,
reusing TrackRow/NavButtons/CircleAction/QualityBadge internals but ONLY local
actions (play all / shuffle / edit tags / add to playlist) — no Qobuz chrome.
- Versions: an album's tracks are split by source directory into versions, so
multiple copies of the same album no longer merge into a duplicated track
list. Best-quality version is the default; a source-icon version picker
(hard-drive / Qobuz / Plex glyph in both the selected display and the options)
switches the shown copy in place. The album cover is the best-quality version
that has one, falling through versions, else a placeholder (covers backfilled
from disk via fill_missing_covers). Header padding-top 11px matches the app.
Fix: Artists tab deadlock — fetch_missing_artist_images snapshotted names with a
blocking rx.recv() called from inside an event-loop closure, freezing the UI
(static spinner, no error). Names are now snapshotted on the UI thread and
passed in; the worker never blocks the event loop.
cc
The local-album tag editor (no remote lookup yet — that lands with the
remote-metadata crate move). Opens from a pencil on the local album detail.
- TagEditorModal.slint: album title/artist + year/genre/catalog + per-track
title/track#/disc# table (indexed model write-back) + persistence QbzSelect
(sidecar default / direct opt-in) + save-with-progress. Mounted last in
AppShell (ADR-009 by mount order). ADR-008: dropdown, no pills.
- tag_editor.rs controller: open_tag_editor (off-thread pre-fetch via
fetch_album_tracks_blocking + D1 year seed + can-direct-write CUE guard),
save_tags (validate, is_dir gate, CUE guard, one-time direct-write confirm
via rfd + library_kv ack, sidecar/direct branch on spawn_blocking with
per-file progress, update_album_group_metadata via new with_db_mut, post-save
refresh of the album + browse models), close.
- AlbumPageView: edit pencil CircleAction, visible is-local && source != plex;
AlbumState gains `source`, set in apply_local_album.
- library_db: with_db_mut for the transactional metadata update.
- main.rs: on_media_action("album","edit") + TagEditorActions wiring (close/
save/set-persistence with the CUE gate).
cc
Port the Tauri folder-management surface to a new Settings > Local Library
sub-section (index 4); the browse-view gear routes here and pre-selects it.
UI:
- LocalLibrarySettings.slint: folder list (icon, display name, disabled/
unavailable badges, multi-select checkboxes) + toolbar (scan-all/add/edit/
remove icon buttons) + filter + Maintenance (cleanup missing) + two-step
Danger Zone clear. ADR-008: plain bordered icon buttons + square badges.
- LibFolderEditModal.slint: alias, enabled, network override + fs-type,
change-path, accessibility status, last-scanned, scan-this-folder. Mounted
at the AppShell root (overlays the window, ADR-009 by mount order).
- SettingsView gains the section-4 sub-nav + panel; app.slint re-exports the
new globals so the Rust layer can populate them.
Controller (local_library_settings.rs): load/add/remove/edit/save/change-path,
cleanup-missing (stats outside the DB lock), two-step clear, filter, select.
All DB access via qbz-library through library_db::with_db on spawn_blocking;
native dir picker + confirms via rfd. Selection preserved across reloads;
network folders get an async 6s-timeout accessibility check (exists() fallback
on timeout, mirroring Tauri).
Core (qbz-library): network_fs_label() for fs-type parity; separator-safe
delete_tracks_in_folder_prefixed() + remove_folder_with_tracks() (fixes the
prefix-collision bug without changing the legacy fn). Scan engine lands in
Slice B.
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
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
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
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
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
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
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
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
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
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
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
Wires the sidebar's musician-clicked callback into a real
destination. Mirrors Tauri's MusicianPageView.svelte plus the
"jump to artist page when Confirmed" shortcut.
Slint:
- ContentView gains a `musician` variant; NavEntry mirrors with
`Musician { name, role }` so the back/forward history can
re-fire the page with the original payload.
- MusicianState / MusicianActions globals carry the resolved
musician metadata (name, role, confidence) + the paginated
Appears On grid (MusicianAppearanceItem mirrors the Tauri
interface — album_id, title, artist_name, year,
role_on_album, artwork URL/handle).
- New `ui/musician/MusicianPageView.slint`: header (avatar +
name + role), Bands & Projects chips when populated, and an
Appears On grid built from the same absolute-position pattern
the artist release grid uses. Load more button drives
MusicianActions::load-more.
- AppShell routes ContentView.musician to MusicianPageView and
forwards open-album.
Rust:
- `crate::musician` controller — load_musician, apply_musician,
load_more_appearances, append_appearances, reset_musician,
artwork_jobs. Pure data + UI plumbing; the orchestration lives
in qbz-core.
- artwork::ArtworkTarget gains MusicianAppearance { index } so
the existing artwork pipeline can target the new grid.
- main.rs gains `navigate_musician` (the navigation entry point
apply_entry now also routes to) and `on_musician_clicked`
(does the resolve, jumps to navigate_artist on Confirmed,
otherwise records + navigates to the Musician view).
- MusicianActions::on_load_more wired to load_more_appearances
+ append.
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
The collapse toggle is a bare icon; the window-edge margin (Spacing.md)
was wider than its ~10px gap to the Discover tab. Drop the tab cluster
x to Spacing.xs so the toggle glyph sits symmetrically.
cc
SearchResultsView renders the four SearchState categories; mounted in
AppShell on ContentView.search. Phase 1 is now functional end to end:
header input -> debounced search -> QbzCore::search_all -> results.
Phase 2 builds this into the tabbed dashboard.
cc
The header TextInput now submits on Enter (records history, opens the
results page) and live-searches on edit. Live search debounces 300 ms
via a Slint Timer and requires 2+ characters. A monotonic version
counter guards navigate_search so a stale load cannot overwrite newer
results.
cc
Icon buttons (header lyrics/queue/hamburger, queue footer) centered
their glyph at a fractional offset (e.g. (36-19)/2 = 8.5px), which
renders blurry — the offset is now rounded to whole pixels. The
header tab spacing dropped from 4px to 2px. The Discover toolbar's
back/forward pair drifted 3px from where the album/artist/settings
views place it; both toolbar clusters now anchor their vertical
center to y=25 so the [<] [>] pair holds one position across views.
cc
The click-outside scrim was a full-window TouchArea — it covered the
tabs and intercepted their hover events, so hover-switching between
menus still did not work (same failure mode as the old PopupWindow
grab). The scrim now starts below the header, leaving the tabs
hoverable. The panel also dropped from header-height + 6px to 45px,
removing the unjustified gap between the tab and its dropdown. Header
tab labels lost another 2px of right padding.
cc
Add HeaderMenuOverlay, a plain in-window Rectangle rendered as the last
child of the AppShell root so it draws on top of the header and content
region. Unlike a PopupWindow it does not grab the pointer, so the header
tabs stay hoverable while a menu is open — hovering from one tab to
another overwrites HeaderMenuState and switches the menu instantly.
Behaviour preserved from the old PopupWindow menus:
- hover-to-open and click-to-open
- 1.5s idle auto-close, restarted on pointer activity over the panel
or its rows and on every tab (re)publish via a changed handler
- click-outside-close via a full-window transparent scrim
- Discover home-item row still switches to ContentView.home
- glyphs on all menus except Discover
Menu panel visuals are unchanged: surface-main bg, 1px border-muted,
Radius.sm, 24px drop shadow, 188px wide.
cc
The four header nav tabs each owned a PopupWindow for their dropdown.
A PopupWindow grabs the pointer while open, so hovering a sibling tab
delivered no hover event and the menus could not be hover-switched.
Add a HeaderMenuState global (open-index, anchor-x, items, home-item)
and the NavMenuEntry struct to state.slint. Tabs now write their
identity into the global on hover/click instead of showing a popup;
the menu panel itself will be rendered by a separate in-window overlay.
Move NavMenuItem into its own file so the overlay can reuse it.
cc
The Discover toolbar's side layouts had no explicit height, so they
filled the 56px bar and y resolved to 0 — the tab pill, genre filter
and gear stuck to the top instead of centering. Both now carry
height = preferred-height.
The header sidebar toggle dropped from a 19px to a 14px icon: it now
matches the nav-tab icon size and tint, and the 30px box centers it
on whole pixels (was 5.5px → blurry).
The queue row's click TouchArea was declared after the content, so it
sat on top and ate the context-menu button's clicks — the [...] just
skipped to the track. It is now declared before the content, so the
menu button keeps its own clicks.
cc
Search placeholders rode at the top of the input because the text
had no height to center within — they now span the box like the
TextInput. The queue row context-menu trigger is always visible with
a proper 32px hit area (matching the album track rows) instead of a
hover-only 24px dot cluster, so a near-miss no longer falls through
to the row and skips the queue.
cc
The sidebar collapse toggle is now an icon-only button at the far
left of the header, before the Discover tab. Removed from NavButtons,
which is now just the back/forward history pair.
cc
Replace the header search field's static placeholder text with a real
TextInput plus an empty-state placeholder, matching the album
track-search pattern so the field clearly reads as an input. Reduce the
search field width to 80% of its prior size (224/320px to 179/256px).
cc
Add a 1.5s idle-close timer to each header nav-tab dropdown. The timer
is armed when the menu opens and reset on any pointer hover or movement
over a menu row; on timeout the popup closes itself. Single-open is
already enforced by Slint, since only one PopupWindow can be open per
window, so sweeping across hover-to-open tabs closes the previous menu
automatically. Hover-to-open and click-to-open both remain working.
cc
Make the sidebar collapse toggle a bare icon button with no background
pill, brightest text-primary tint, and a 2px larger icon. Restyle the
back/forward history buttons to match the Discover carousel page
navigators (circular elevated buttons) instead of the tab pill style.
cc
Footer now carries the five Tauri controls with Tauri's icons — clear
(trash-list), save (add-to-list), infinite, sleep timer (clock), and
search; the timer button is visual-only for now. Queue rows gain a
context menu 1:1 with the Tauri queue track menu (Remove from queue,
Stop after this, Remove all after, Add to playlist, Track info); only
Remove is wired, the rest are placeholders.
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
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 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
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