feat(slint): sidebar playlists list + create-playlist modal

- 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
This commit is contained in:
vicrodh committed 2026-05-22 03:57:54 -06:00
1 parent 1be78193e2
commit f387114ecf
7 files changed
+429 -9

No files matched your search

+4
View File
@@ -11,6 +11,7 @@ import { Sidebar } from "Sidebar.slint";
import { HeaderBar } from "HeaderBar.slint";
import { HeaderMenuOverlay } from "HeaderMenuOverlay.slint";
import { PlaylistPickerModal } from "../primitives/PlaylistPickerModal.slint";
import { CreatePlaylistModal } from "../primitives/CreatePlaylistModal.slint";
import { PlayerBar } from "PlayerBar.slint";
import { QueueSidebar } from "QueueSidebar.slint";
import { LyricsSidebar } from "LyricsSidebar.slint";
@@ -222,4 +223,7 @@ export component AppShell inherits Rectangle {
// Global "Add to playlist" picker overlay, opened from any track
// context menu.
PlaylistPickerModal { }
// Global "Create playlist" modal, opened from the sidebar "+".
CreatePlaylistModal { }
}
+74 -8
View File
@@ -12,9 +12,49 @@
import { Theme } from "../foundation/semantic-colors.slint";
import { Spacing } from "../foundation/spacing.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
import { ShellState } from "../state.slint";
import { ShellState, SidebarState, SidebarActions, SidebarPlaylistItem } from "../state.slint";
import { ScrollView } from "std-widgets.slint";
// One playlist row in the sidebar.
component PlaylistRow inherits Rectangle {
in property <SidebarPlaylistItem> item;
in property <bool> active: false;
callback clicked(string);
height: 32px;
border-radius: 6px;
background: ta.has-hover || root.active ? Theme.surface-hover : transparent;
HorizontalLayout {
padding-left: 8px;
padding-right: 8px;
spacing: 8px;
VerticalLayout {
alignment: center;
QbzIcon {
source: @image-url("../assets/icons/list-music.svg");
width: 15px;
height: 15px;
tint: root.active ? Theme.text-primary : Theme.text-muted;
}
}
Text {
text: root.item.name;
color: root.active ? Theme.text-primary : Theme.text-secondary;
font-size: 13px;
vertical-alignment: center;
horizontal-stretch: 1;
overflow: elide;
}
}
ta := TouchArea {
mouse-cursor: pointer;
clicked => {
root.clicked(root.item.id);
}
}
}
export component Sidebar inherits Rectangle {
// Rendered width — 0 when collapsed, otherwise the user-set width.
width: ShellState.sidebar-collapsed ? 0px : ShellState.sidebar-width;
@@ -44,11 +84,25 @@ export component Sidebar inherits Rectangle {
}
VerticalLayout {
alignment: center;
QbzIcon {
source: @image-url("../assets/icons/plus.svg");
width: 16px;
height: 16px;
tint: Theme.text-muted;
Rectangle {
width: 22px;
height: 22px;
border-radius: 4px;
background: plus-ta.has-hover ? Theme.surface-hover : transparent;
QbzIcon {
source: @image-url("../assets/icons/plus.svg");
width: 16px;
height: 16px;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
tint: plus-ta.has-hover ? Theme.text-primary : Theme.text-muted;
}
plus-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
SidebarActions.create-playlist();
}
}
}
}
}
@@ -58,8 +112,20 @@ export component Sidebar inherits Rectangle {
horizontal-scrollbar-policy: ScrollBarPolicy.always-off;
VerticalLayout {
spacing: 2px;
// Real playlist rows are populated when the sidebar
// milestone wires playlist data. No fixtures.
alignment: start;
for pl in SidebarState.playlists: PlaylistRow {
item: pl;
active: pl.id == SidebarState.active-id;
clicked(id) => {
SidebarActions.open-playlist(id);
}
}
if SidebarState.playlists.length == 0 && !SidebarState.loading: Text {
text: @tr("No playlists yet.");
color: Theme.text-muted;
font-size: 12px;
wrap: word-wrap;
}
}
}
}