fix(slint): flyouts/idle, header nav layout, toasts, settings, search
Flyouts & idle (main effort): - Header dropdowns now glue under the tab: open-menu resets from-sidebar + title (a stale sidebar flyout state was flinging the panel far away). - Idle-close now pauses while the cursor rests on the TRIGGER too (full header tabs gained the trigger-hovered logic the others already had). - Dropped the dropdown caret (saves width on small screens). Header nav layout: - Section nav moved INTO the left-controls cluster, after the three sacred buttons, so it never overlaps them. Full text tabs when nav-in-sidebar off and sidebar not closed; compact icons when closed — mutually exclusive (no more duplicate/cluttered header). - Search field gives up 60px (animated) when the header hosts the nav. Settings made functional: - Toast component mounted in AppShell (it was never rendered) + the in-app toasts toggle drives ToastState.enabled. - "Show album artwork in Local Library track list" now drives the Tracks/ Folders rows. Both rows un-flagged as unsupported. Collapsible search (ExpandableSearch, app-wide): - Smooth open AND close (width animates both ways; magnifier fades). - Reliable auto-focus on open (deferred via a one-shot timer). cc
This commit is contained in:
5 files changed
+128
-95
No files matched your search
@@ -18,7 +18,7 @@
|
||||
|
||||
import { Theme } from "../foundation/semantic-colors.slint";
|
||||
import { Typography } from "../foundation/typography.slint";
|
||||
import { LocalLibraryState, LocalLibraryActions, FolderNode, LocalArtistItem, AlphaJump, LibAlbumFilterState } from "../state.slint";
|
||||
import { LocalLibraryState, LocalLibraryActions, FolderNode, LocalArtistItem, AlphaJump, LibAlbumFilterState, AppearanceState } from "../state.slint";
|
||||
import { NavButtons } from "../shell/NavButtons.slint";
|
||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||
import { AlbumCollectionView } from "../discover/AlbumCollectionView.slint";
|
||||
@@ -993,7 +993,7 @@ export component LocalLibraryView inherits Rectangle {
|
||||
TrackRow {
|
||||
track: track;
|
||||
index: i;
|
||||
show-artwork: false;
|
||||
show-artwork: AppearanceState.local-library-track-artwork;
|
||||
show-album: LocalLibraryState.tracks-group-mode != "album";
|
||||
show-favorite: false;
|
||||
show-download: false;
|
||||
@@ -1309,7 +1309,7 @@ export component LocalLibraryView inherits Rectangle {
|
||||
for track[ti] in LocalLibraryState.folder-detail-tracks: TrackRow {
|
||||
track: track;
|
||||
index: ti;
|
||||
show-artwork: false;
|
||||
show-artwork: AppearanceState.local-library-track-artwork;
|
||||
show-album: true;
|
||||
show-favorite: false;
|
||||
show-download: false;
|
||||
|
||||
@@ -19,9 +19,21 @@ export component ExpandableSearch inherits Rectangle {
|
||||
in property <bool> sm: false;
|
||||
callback edited(string);
|
||||
|
||||
// Defer the focus: focusing the input synchronously inside `changed open`
|
||||
// races the open animation/layout (and can panic with "Recursion
|
||||
// detected"). A one-shot timer grabs focus a tick later, reliably.
|
||||
focus-timer := Timer {
|
||||
interval: 30ms;
|
||||
running: false;
|
||||
triggered => {
|
||||
input.focus();
|
||||
self.running = false;
|
||||
}
|
||||
}
|
||||
changed open => {
|
||||
if (root.open) {
|
||||
input.focus();
|
||||
focus-timer.running = true;
|
||||
focus-timer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,9 +41,11 @@ export component ExpandableSearch inherits Rectangle {
|
||||
width: root.sm ? 30px : 34px;
|
||||
height: root.sm ? 30px : 34px;
|
||||
|
||||
// Closed: the magnifier toggle.
|
||||
// Closed: the magnifier toggle. Fades (not toggled) so open/close is
|
||||
// smooth in both directions.
|
||||
Rectangle {
|
||||
visible: !root.open;
|
||||
opacity: root.open ? 0.0 : 1.0;
|
||||
animate opacity { duration: 120ms; easing: ease-in-out; }
|
||||
width: root.sm ? 30px : 34px;
|
||||
height: root.sm ? 30px : 34px;
|
||||
border-radius: 6px;
|
||||
@@ -45,6 +59,7 @@ export component ExpandableSearch inherits Rectangle {
|
||||
tint: open-ta.has-hover ? Theme.text-primary : Theme.text-muted;
|
||||
}
|
||||
open-ta := TouchArea {
|
||||
enabled: !root.open;
|
||||
mouse-cursor: pointer;
|
||||
clicked => {
|
||||
root.open = true;
|
||||
@@ -52,15 +67,15 @@ export component ExpandableSearch inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// Open: right-anchored overlay that grows left.
|
||||
// Open: right-anchored overlay that grows left. Always rendered; its width
|
||||
// animates to 0 when closed (clip hides it), so the shrink is also smooth.
|
||||
overlay := Rectangle {
|
||||
visible: root.open;
|
||||
x: parent.width - self.width;
|
||||
width: root.open ? root.max-open-width : 0px;
|
||||
height: root.sm ? 30px : 34px;
|
||||
background: Theme.surface-card;
|
||||
border-radius: 6px;
|
||||
border-width: 1px;
|
||||
border-width: root.open ? 1px : 0px;
|
||||
border-color: input.has-focus ? Theme.accent : Theme.border-subtle;
|
||||
clip: true;
|
||||
animate width { duration: 200ms; easing: ease-in-out; }
|
||||
|
||||
@@ -14,7 +14,7 @@ import { Theme } from "../foundation/semantic-colors.slint";
|
||||
import { ThemeState } from "../foundation/theme-state.slint";
|
||||
import { Typography } from "../foundation/typography.slint";
|
||||
import { Radius } from "../foundation/radius.slint";
|
||||
import { AppearanceState, SidebarState, ShellState } from "../state.slint";
|
||||
import { AppearanceState, SidebarState, ShellState, ToastState } from "../state.slint";
|
||||
import { QbzToggle } from "../primitives/QbzToggle.slint";
|
||||
import { QbzSelect } from "../primitives/QbzSelect.slint";
|
||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||
@@ -312,7 +312,6 @@ export component AppearanceSettings inherits VerticalLayout {
|
||||
}
|
||||
}
|
||||
SettingRow {
|
||||
unsupported: true;
|
||||
label: @tr("Show album artwork in Local Library track list");
|
||||
description: @tr("Display the album cover thumbnail next to each track in the Tracks and Folders views. Off by default — large libraries pay a per-row image-decode cost.");
|
||||
QbzToggle {
|
||||
@@ -334,12 +333,12 @@ export component AppearanceSettings inherits VerticalLayout {
|
||||
GroupHeader { text: @tr("NOTIFICATIONS"); }
|
||||
|
||||
SettingRow {
|
||||
unsupported: true;
|
||||
label: @tr("In-app toasts notifications");
|
||||
QbzToggle {
|
||||
checked: AppearanceState.in-app-toasts;
|
||||
toggled(v) => {
|
||||
AppearanceState.in-app-toasts = v;
|
||||
ToastState.enabled = v;
|
||||
AppearanceState.appearance-bool("in-app-toasts", v);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { FolderEditModal } from "../primitives/FolderEditModal.slint";
|
||||
import { LibFolderEditModal } from "../settings/LibFolderEditModal.slint";
|
||||
import { TagEditorModal } from "../primitives/TagEditorModal.slint";
|
||||
import { DragGhost } from "../primitives/DragGhost.slint";
|
||||
import { Toast } from "../primitives/Toast.slint";
|
||||
import { PlayerBar } from "PlayerBar.slint";
|
||||
import { QueueSidebar } from "QueueSidebar.slint";
|
||||
import { LyricsSidebar } from "LyricsSidebar.slint";
|
||||
@@ -322,4 +323,7 @@ export component AppShell inherits Rectangle {
|
||||
|
||||
// Floating track-drag ghost (follows the cursor, on top of all).
|
||||
DragGhost { }
|
||||
|
||||
// In-app toast notifications — bottom-center, above everything.
|
||||
Toast { }
|
||||
}
|
||||
@@ -24,6 +24,9 @@ component NavTab inherits Rectangle {
|
||||
callback clicked();
|
||||
// Fires when the pointer first enters the tab — drives hover-to-open.
|
||||
callback hovered();
|
||||
// Fires on hover enter/leave so the host can keep the menu open while the
|
||||
// pointer rests on the tab (idle-close must pause then).
|
||||
callback hover-changed(bool);
|
||||
|
||||
height: 30px;
|
||||
width: tab-row.preferred-width;
|
||||
@@ -33,7 +36,7 @@ component NavTab inherits Rectangle {
|
||||
|
||||
tab-row := HorizontalLayout {
|
||||
padding-left: root.show-icon ? 9px : 11px;
|
||||
padding-right: 7px;
|
||||
padding-right: 11px;
|
||||
spacing: 6px;
|
||||
if root.show-icon: VerticalLayout {
|
||||
alignment: center;
|
||||
@@ -44,9 +47,8 @@ component NavTab inherits Rectangle {
|
||||
tint: root.active ? Theme.text-primary : Theme.text-secondary;
|
||||
}
|
||||
}
|
||||
// An explicit row-height box plus vertical-alignment centers the
|
||||
// label optically against the glyph and caret — a bare Text box
|
||||
// rides high because of the font's descender space.
|
||||
// The dropdown caret was dropped (it ate width on small screens; the
|
||||
// dropdown nature is learned after first use).
|
||||
Text {
|
||||
text: root.label;
|
||||
height: root.height;
|
||||
@@ -55,16 +57,6 @@ component NavTab inherits Rectangle {
|
||||
font-size: 11px;
|
||||
font-weight: root.active ? Typography.semibold : Typography.regular;
|
||||
}
|
||||
// Caret marking the tab as a dropdown.
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
QbzIcon {
|
||||
source: @image-url("../assets/icons/chevron-down.svg");
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
tint: root.active ? Theme.text-secondary : Theme.text-muted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ta := TouchArea {
|
||||
@@ -73,6 +65,7 @@ component NavTab inherits Rectangle {
|
||||
root.clicked();
|
||||
}
|
||||
changed has-hover => {
|
||||
root.hover-changed(self.has-hover);
|
||||
if (self.has-hover) {
|
||||
root.hovered();
|
||||
}
|
||||
@@ -154,6 +147,11 @@ component NavTabWithMenu inherits Rectangle {
|
||||
function open-menu() {
|
||||
HeaderMenuState.open-index = root.tab-index;
|
||||
HeaderMenuState.anchor-x = root.absolute-position.x;
|
||||
// Header dropdown — NOT a sidebar flyout. Resetting these is what
|
||||
// glues the panel under the tab: a stale from-sidebar/anchor-y from a
|
||||
// previous sidebar flyout would otherwise fling it far away.
|
||||
HeaderMenuState.from-sidebar = false;
|
||||
HeaderMenuState.title = "";
|
||||
HeaderMenuState.items = root.items;
|
||||
HeaderMenuState.home-item = root.home-item;
|
||||
}
|
||||
@@ -171,6 +169,15 @@ component NavTabWithMenu inherits Rectangle {
|
||||
hovered => {
|
||||
root.open-menu();
|
||||
}
|
||||
// Pause the idle-close while the cursor rests on the tab; only the
|
||||
// active tab clears the flag (avoids a leave/enter race).
|
||||
hover-changed(h) => {
|
||||
if (h) {
|
||||
HeaderMenuState.trigger-hovered = true;
|
||||
} else if (root.active) {
|
||||
HeaderMenuState.trigger-hovered = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,82 +308,22 @@ export component HeaderBar inherits Rectangle {
|
||||
|
||||
// Responsive breakpoints.
|
||||
property <bool> show-tab-icons: root.width >= 1140px;
|
||||
// 80% of the prior search width (was 224px / 320px).
|
||||
property <length> search-width: root.width < 960px ? 179px : 256px;
|
||||
// 80% of the prior search width (was 224px / 320px). When the section nav
|
||||
// is in the header (nav-in-sidebar off) it gives up 60px to make room.
|
||||
property <length> search-width:
|
||||
(root.width < 960px ? 179px : 256px) - (ShellState.nav-in-sidebar ? 0px : 60px);
|
||||
|
||||
// --- Tabs (anchored left) ----------------------------------------------
|
||||
tabs := HorizontalLayout {
|
||||
// xs (not md): the sidebar toggle is a bare icon with ~10px of
|
||||
// breathing room to the Discover tab; the window-edge margin is
|
||||
// matched to that so the toggle glyph sits symmetrically.
|
||||
x: Spacing.xs;
|
||||
y: (root.height - self.height) / 2;
|
||||
height: 30px;
|
||||
spacing: 2px;
|
||||
// Section nav lives in the header by default; when the user moves it
|
||||
// to the sidebar (ShellState.nav-in-sidebar) the header tabs hide.
|
||||
// The sidebar-collapse toggle no longer lives here — it moved next to
|
||||
// the back/forward nav buttons (NavButtons), present in every view.
|
||||
if !ShellState.nav-in-sidebar: HorizontalLayout {
|
||||
spacing: 2px;
|
||||
NavTabWithMenu {
|
||||
tab-index: 0;
|
||||
icon: @image-url("../assets/icons/compass.svg");
|
||||
label: @tr("Discover");
|
||||
active: true;
|
||||
show-icon: root.show-tab-icons;
|
||||
// Discover rows are text-only (no glyphs).
|
||||
items: [
|
||||
{ label: @tr("Home"), has-glyph: false, route: "discover-home" },
|
||||
{ label: @tr("Editor's Picks"), has-glyph: false, route: "discover-editorPicks" },
|
||||
{ label: @tr("For You"), has-glyph: false, route: "discover-forYou" },
|
||||
];
|
||||
// Row 0 = Home, the one working destination.
|
||||
home-item: 0;
|
||||
}
|
||||
NavTabWithMenu {
|
||||
tab-index: 1;
|
||||
icon: @image-url("../assets/icons/music-library-2.svg");
|
||||
label: @tr("Library");
|
||||
show-icon: root.show-tab-icons;
|
||||
items: [
|
||||
{ label: @tr("Tracks"), icon: @image-url("../assets/icons/music.svg"), has-glyph: true, route: "favorites-tracks" },
|
||||
{ label: @tr("Albums"), icon: @image-url("../assets/icons/disc.svg"), has-glyph: true, route: "favorites-albums" },
|
||||
{ label: @tr("Artists"), icon: @image-url("../assets/icons/user.svg"), has-glyph: true, route: "favorites-artists" },
|
||||
{ label: @tr("Playlists"), icon: @image-url("../assets/icons/list-music.svg"), has-glyph: true, route: "favorites-playlists" },
|
||||
{ label: @tr("Labels"), icon: @image-url("../assets/icons/disc-3.svg"), has-glyph: true, route: "favorites-labels" },
|
||||
];
|
||||
}
|
||||
NavTabWithMenu {
|
||||
tab-index: 2;
|
||||
icon: @image-url("../assets/icons/hard-drive.svg");
|
||||
label: @tr("Local Library");
|
||||
show-icon: root.show-tab-icons;
|
||||
items: [
|
||||
{ label: @tr("Albums"), icon: @image-url("../assets/icons/disc.svg"), has-glyph: true, route: "local-albums" },
|
||||
{ label: @tr("Artists"), icon: @image-url("../assets/icons/user.svg"), has-glyph: true, route: "local-artists" },
|
||||
{ label: @tr("Folders"), icon: @image-url("../assets/icons/folder.svg"), has-glyph: true, route: "local-folders" },
|
||||
{ label: @tr("Tracks"), icon: @image-url("../assets/icons/music.svg"), has-glyph: true, route: "local-tracks" },
|
||||
];
|
||||
}
|
||||
NavTabWithMenu {
|
||||
tab-index: 3;
|
||||
icon: @image-url("../assets/icons/cassette-tape.svg");
|
||||
label: @tr("My QBZ");
|
||||
show-icon: root.show-tab-icons;
|
||||
items: [
|
||||
{ label: @tr("Collections"), icon: @image-url("../assets/icons/library-big.svg"), has-glyph: true },
|
||||
{ label: @tr("Mixtapes"), icon: @image-url("../assets/icons/cassette-tape.svg"), has-glyph: true },
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
// The section nav now lives inside the left-controls cluster (after the
|
||||
// three sacred nav buttons), so it can never overlap them.
|
||||
|
||||
// --- Search (absolutely centered on the window) ------------------------
|
||||
Rectangle {
|
||||
x: (root.width - self.width) / 2;
|
||||
y: (root.height - self.height) / 2;
|
||||
width: root.search-width;
|
||||
// Smoothly animate the width change when the nav moves in/out of the
|
||||
// header (and the x recenters with it).
|
||||
animate width { duration: 220ms; easing: ease-in-out; }
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
border-width: 1px;
|
||||
@@ -453,6 +400,74 @@ export component HeaderBar inherits Rectangle {
|
||||
clicked => { NavState.request-forward(); }
|
||||
}
|
||||
}
|
||||
// Full section nav (text tabs) — header-nav mode (nav-in-sidebar OFF)
|
||||
// while the sidebar isn't fully closed. Sits AFTER the three sacred
|
||||
// buttons, so it never overlaps them. Mutually exclusive with the
|
||||
// compact icons below (which only show when fully closed).
|
||||
if !ShellState.nav-in-sidebar && ShellState.sidebar-state != 2: HorizontalLayout {
|
||||
spacing: 2px;
|
||||
Rectangle { width: 6px; }
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
NavTabWithMenu {
|
||||
tab-index: 0;
|
||||
icon: @image-url("../assets/icons/compass.svg");
|
||||
label: @tr("Discover");
|
||||
active: true;
|
||||
show-icon: root.show-tab-icons;
|
||||
home-item: 0;
|
||||
items: [
|
||||
{ label: @tr("Home"), has-glyph: false, route: "discover-home" },
|
||||
{ label: @tr("Editor's Picks"), has-glyph: false, route: "discover-editorPicks" },
|
||||
{ label: @tr("For You"), has-glyph: false, route: "discover-forYou" },
|
||||
];
|
||||
}
|
||||
}
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
NavTabWithMenu {
|
||||
tab-index: 1;
|
||||
icon: @image-url("../assets/icons/music-library-2.svg");
|
||||
label: @tr("Library");
|
||||
show-icon: root.show-tab-icons;
|
||||
items: [
|
||||
{ label: @tr("Tracks"), icon: @image-url("../assets/icons/music.svg"), has-glyph: true, route: "favorites-tracks" },
|
||||
{ label: @tr("Albums"), icon: @image-url("../assets/icons/disc.svg"), has-glyph: true, route: "favorites-albums" },
|
||||
{ label: @tr("Artists"), icon: @image-url("../assets/icons/user.svg"), has-glyph: true, route: "favorites-artists" },
|
||||
{ label: @tr("Playlists"), icon: @image-url("../assets/icons/list-music.svg"), has-glyph: true, route: "favorites-playlists" },
|
||||
{ label: @tr("Labels"), icon: @image-url("../assets/icons/disc-3.svg"), has-glyph: true, route: "favorites-labels" },
|
||||
];
|
||||
}
|
||||
}
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
NavTabWithMenu {
|
||||
tab-index: 2;
|
||||
icon: @image-url("../assets/icons/hard-drive.svg");
|
||||
label: @tr("Local Library");
|
||||
show-icon: root.show-tab-icons;
|
||||
items: [
|
||||
{ label: @tr("Albums"), icon: @image-url("../assets/icons/disc.svg"), has-glyph: true, route: "local-albums" },
|
||||
{ label: @tr("Artists"), icon: @image-url("../assets/icons/user.svg"), has-glyph: true, route: "local-artists" },
|
||||
{ label: @tr("Folders"), icon: @image-url("../assets/icons/folder.svg"), has-glyph: true, route: "local-folders" },
|
||||
{ label: @tr("Tracks"), icon: @image-url("../assets/icons/music.svg"), has-glyph: true, route: "local-tracks" },
|
||||
];
|
||||
}
|
||||
}
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
NavTabWithMenu {
|
||||
tab-index: 3;
|
||||
icon: @image-url("../assets/icons/cassette-tape.svg");
|
||||
label: @tr("My QBZ");
|
||||
show-icon: root.show-tab-icons;
|
||||
items: [
|
||||
{ label: @tr("Collections"), icon: @image-url("../assets/icons/library-big.svg"), has-glyph: true },
|
||||
{ label: @tr("Mixtapes"), icon: @image-url("../assets/icons/cassette-tape.svg"), has-glyph: true },
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
// Compact section nav — only while the sidebar is fully closed
|
||||
// (Cerrado), so Discover / Library / Local Library / My QBZ stay
|
||||
// reachable. Icon-only; each opens a titled downward dropdown.
|
||||
|
||||
Reference in new issue
Block a user