feat(slint): playlists flyout from the header when the sidebar is closed
With the sidebar fully closed there was no way to reach playlists. Add a playlists button to the closed-sidebar header cluster that opens a flyout over SidebarState's entries (folders + playlists), six rows visible with the rest scrolling, plus an always-visible footer linking to the Playlist Manager. Rendered at the AppShell level (SidebarPlaylistsPopup + SidebarPlaylistsPopupState); header button needs no absolute-position correction (not inside a Flickable). cc
This commit is contained in:
4 files changed
+258
-1
No files matched your search
@@ -13,6 +13,7 @@ import { HeaderBar } from "HeaderBar.slint";
|
||||
import { HeaderMenuOverlay } from "HeaderMenuOverlay.slint";
|
||||
import { SidebarFolderPopup } from "SidebarFolderPopup.slint";
|
||||
import { SidebarTooltip } from "SidebarTooltip.slint";
|
||||
import { SidebarPlaylistsPopup } from "SidebarPlaylistsPopup.slint";
|
||||
import { PlaylistPickerModal } from "../primitives/PlaylistPickerModal.slint";
|
||||
import { CreatePlaylistModal } from "../primitives/CreatePlaylistModal.slint";
|
||||
import { EditPlaylistModal } from "../primitives/EditPlaylistModal.slint";
|
||||
@@ -303,6 +304,9 @@ export component AppShell inherits Rectangle {
|
||||
// Collapsed-sidebar hover tooltip (mini state) — name + folder count.
|
||||
SidebarTooltip { }
|
||||
|
||||
// Closed-sidebar playlists flyout (opened from the header playlists button).
|
||||
SidebarPlaylistsPopup { }
|
||||
|
||||
// Global "Add to playlist" picker overlay, opened from any track
|
||||
// context menu.
|
||||
PlaylistPickerModal { }
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Spacing } from "../foundation/spacing.slint";
|
||||
import { Typography } from "../foundation/typography.slint";
|
||||
import { Layout } from "../foundation/layout.slint";
|
||||
import { Radius } from "../foundation/radius.slint";
|
||||
import { SessionState, NavState, ContentView, ShellState, HeaderMenuState, NavMenuEntry, SearchActions } from "../state.slint";
|
||||
import { SessionState, NavState, ContentView, ShellState, HeaderMenuState, NavMenuEntry, SearchActions, SidebarPlaylistsPopupState } from "../state.slint";
|
||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||
|
||||
component NavTab inherits Rectangle {
|
||||
@@ -231,6 +231,36 @@ component CompactNavBtn inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// Like CompactNavBtn, but instead of a section dropdown it opens the playlists
|
||||
// flyout (SidebarPlaylistsPopup). Shown in the closed-sidebar header cluster so
|
||||
// playlists stay reachable with the sidebar fully collapsed. Header buttons are
|
||||
// not inside a Flickable, so absolute-position needs no correction here.
|
||||
component CompactPlaylistsBtn inherits Rectangle {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: Radius.sm;
|
||||
property <bool> active: SidebarPlaylistsPopupState.open;
|
||||
background: root.active ? Theme.surface-elevated
|
||||
: cp-ta.has-hover ? Theme.surface-hover : transparent;
|
||||
|
||||
QbzIcon {
|
||||
source: @image-url("../assets/icons/list-music.svg");
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
tint: root.active ? Theme.text-primary : Theme.text-secondary;
|
||||
}
|
||||
cp-ta := TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
clicked => {
|
||||
SidebarPlaylistsPopupState.anchor-x = root.absolute-position.x;
|
||||
SidebarPlaylistsPopupState.anchor-y = root.absolute-position.y + root.height + 6px;
|
||||
SidebarPlaylistsPopupState.open = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Circular history / collapse button for the header's right cluster —
|
||||
// same look as the Discover carousel navigators. `enabled` dims + disables.
|
||||
component HeaderNavBtn inherits Rectangle {
|
||||
@@ -530,6 +560,18 @@ export component HeaderBar inherits Rectangle {
|
||||
];
|
||||
}
|
||||
}
|
||||
// Thin separator, then the playlists flyout button (no sidebar list
|
||||
// to reach playlists from while fully closed).
|
||||
Rectangle {
|
||||
width: 1px;
|
||||
height: 18px;
|
||||
y: (parent.height - self.height) / 2;
|
||||
background: Theme.border-subtle;
|
||||
}
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
CompactPlaylistsBtn { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
// Closed-sidebar playlists flyout. With the sidebar fully closed there is no
|
||||
// playlist list anywhere, so a header button (CompactPlaylistsBtn) opens this
|
||||
// popup. It lists SidebarState's entries (folders + playlists, the same flat
|
||||
// tree the sidebar shows), six rows visible with the rest scrolling, and an
|
||||
// always-visible footer linking to the Playlist Manager. Rendered at the
|
||||
// AppShell level because the header clips its overflow.
|
||||
|
||||
import { Theme } from "../foundation/semantic-colors.slint";
|
||||
import { Radius } from "../foundation/radius.slint";
|
||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||
import { ListScrollbar } from "../primitives/ListScrollbar.slint";
|
||||
import { SidebarPlaylistsPopupState, SidebarState, SidebarActions, SidebarEntry } from "../state.slint";
|
||||
|
||||
// One entry row — a folder (bold, toggles expand) or a playlist (opens it).
|
||||
component PpRow inherits Rectangle {
|
||||
in property <SidebarEntry> entry;
|
||||
property <bool> is-folder: root.entry.kind == "folder";
|
||||
height: 32px;
|
||||
border-radius: 5px;
|
||||
background: ta.has-hover ? Theme.surface-hover : transparent;
|
||||
HorizontalLayout {
|
||||
// Nested playlists indent under their folder, matching the sidebar.
|
||||
padding-left: root.entry.indent ? 26px : 12px;
|
||||
padding-right: 12px;
|
||||
spacing: 8px;
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
QbzIcon {
|
||||
source: root.is-folder
|
||||
? (root.entry.expanded
|
||||
? @image-url("../assets/icons/folder-open.svg")
|
||||
: @image-url("../assets/icons/folder.svg"))
|
||||
: @image-url("../assets/icons/list-music.svg");
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
tint: root.is-folder ? Theme.accent : Theme.text-muted;
|
||||
}
|
||||
}
|
||||
Text {
|
||||
text: root.entry.name;
|
||||
color: root.is-folder ? Theme.text-primary : Theme.text-secondary;
|
||||
font-size: 13px;
|
||||
font-weight: root.is-folder ? 600 : 400;
|
||||
vertical-alignment: center;
|
||||
horizontal-stretch: 1;
|
||||
overflow: elide;
|
||||
}
|
||||
// Folder playlist count, right-aligned (matches the sidebar hover count).
|
||||
if root.is-folder && root.entry.count > 0: Text {
|
||||
text: root.entry.count;
|
||||
color: Theme.text-muted;
|
||||
font-size: 11px;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
ta := TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
clicked => {
|
||||
if (root.is-folder) {
|
||||
// Expand/collapse in place — re-renders the list, like the sidebar.
|
||||
SidebarActions.toggle-folder(root.entry.id);
|
||||
} else {
|
||||
SidebarActions.open-playlist(root.entry.id);
|
||||
SidebarPlaylistsPopupState.open = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export component SidebarPlaylistsPopup inherits Rectangle {
|
||||
visible: SidebarPlaylistsPopupState.open;
|
||||
|
||||
property <length> row-h: 32px;
|
||||
property <int> visible-rows: Math.min(SidebarState.entries.length, 6);
|
||||
property <length> list-h: Math.max(root.row-h, root.visible-rows * root.row-h);
|
||||
|
||||
// Scrim — a click anywhere outside the panel closes the popup.
|
||||
if SidebarPlaylistsPopupState.open: TouchArea {
|
||||
clicked => {
|
||||
SidebarPlaylistsPopupState.open = false;
|
||||
}
|
||||
}
|
||||
|
||||
if SidebarPlaylistsPopupState.open: Rectangle {
|
||||
x: SidebarPlaylistsPopupState.anchor-x;
|
||||
y: SidebarPlaylistsPopupState.anchor-y;
|
||||
width: 248px;
|
||||
// header(28) + list + divider(1) + footer(40) + paddings(8).
|
||||
height: 28px + root.list-h + 1px + 40px + 8px;
|
||||
|
||||
Rectangle {
|
||||
background: Theme.surface-main;
|
||||
border-radius: Radius.sm;
|
||||
border-width: 1px;
|
||||
border-color: Theme.border-muted;
|
||||
drop-shadow-blur: 24px;
|
||||
drop-shadow-color: Theme.card-shadow;
|
||||
drop-shadow-offset-y: 6px;
|
||||
|
||||
// Swallow clicks so the scrim doesn't close while interacting.
|
||||
TouchArea { }
|
||||
|
||||
VerticalLayout {
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
|
||||
// Section header.
|
||||
HorizontalLayout {
|
||||
height: 24px;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
Text {
|
||||
text: @tr("PLAYLISTS");
|
||||
color: Theme.text-muted;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
vertical-alignment: center;
|
||||
horizontal-stretch: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Entries — capped at six rows; scrolls beyond. The Flickable is
|
||||
// unconditional (an empty list just renders no rows) so the
|
||||
// sibling ListScrollbar can reference `pp-flick` — an id declared
|
||||
// inside an `if` is not visible to a sibling `if`.
|
||||
Rectangle {
|
||||
height: root.list-h;
|
||||
pp-flick := Flickable {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
viewport-width: self.width;
|
||||
viewport-height: SidebarState.entries.length * root.row-h;
|
||||
VerticalLayout {
|
||||
width: parent.width;
|
||||
alignment: start;
|
||||
for entry in SidebarState.entries: PpRow {
|
||||
entry: entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
if SidebarState.entries.length > 6: ListScrollbar {
|
||||
x: parent.width - self.width;
|
||||
width: 10px;
|
||||
height: 100%;
|
||||
viewport-height: pp-flick.viewport-height;
|
||||
visible-height: pp-flick.height;
|
||||
viewport-y <=> pp-flick.viewport-y;
|
||||
}
|
||||
if SidebarState.entries.length == 0: Text {
|
||||
x: 12px;
|
||||
y: 8px;
|
||||
text: @tr("No playlists yet.");
|
||||
color: Theme.text-muted;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 1px;
|
||||
background: Theme.border-subtle;
|
||||
}
|
||||
|
||||
// Footer — always visible link to the Playlist Manager.
|
||||
Rectangle {
|
||||
height: 40px;
|
||||
border-radius: 5px;
|
||||
background: footer-ta.has-hover ? Theme.surface-hover : transparent;
|
||||
HorizontalLayout {
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
spacing: 8px;
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
QbzIcon {
|
||||
source: @image-url("../assets/icons/library-big.svg");
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
tint: Theme.text-secondary;
|
||||
}
|
||||
}
|
||||
Text {
|
||||
text: @tr("Manage playlists");
|
||||
color: Theme.text-secondary;
|
||||
font-size: 13px;
|
||||
vertical-alignment: center;
|
||||
horizontal-stretch: 1;
|
||||
}
|
||||
}
|
||||
footer-ta := TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
clicked => {
|
||||
SidebarActions.manage-playlists();
|
||||
SidebarPlaylistsPopupState.open = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -832,6 +832,16 @@ export global SidebarTooltipState {
|
||||
in-out property <length> anchor-y; // window y (row top)
|
||||
}
|
||||
|
||||
// Closed-sidebar playlists flyout. When the sidebar is fully closed there is no
|
||||
// playlist list on screen; a header button opens this popup over SidebarState's
|
||||
// entries (folders + playlists), capped at six visible rows with the rest
|
||||
// scrolling, plus an always-visible footer link to the Playlist Manager.
|
||||
export global SidebarPlaylistsPopupState {
|
||||
in-out property <bool> open: false;
|
||||
in-out property <length> anchor-x; // window x (header button left)
|
||||
in-out property <length> anchor-y; // window y (below the button)
|
||||
}
|
||||
|
||||
// The sidebar's playlists + folders. Populated by crate::sidebar on
|
||||
// shell entry and after create/delete/move.
|
||||
export global SidebarState {
|
||||
|
||||
Reference in new issue
Block a user