feat(slint): collapsible sidebar, Queue/Lyrics panels, nav menus
Shell chrome (steps 1-3): - Left sidebar is collapsible — a toggle in the nav cluster (left of the [<] [>] pair) and a draggable right edge that resizes 200-420px and collapses below 160px. ShellState global drives it. - Queue and Lyrics right sidebars: 320px panels rendered as layout siblings, so opening one pushes the content area narrower rather than overlaying it. The header captions/queue buttons toggle them (mutually exclusive) and light up when active. - The four nav tabs (Discover/Library/Local Library/My QBZ) open dropdown menus listing their sub-sections; Discover -> Home is wired. Detail views (steps 4-5): - Track rows: the even-row tint is now clearly fainter than the hover tint so the two states no longer look identical. - The favorite and offline-cache glyphs in album track rows are real buttons (hover state, emit media-action). - Album header artist link uses the metadata text color, bold, instead of the accent blue. - Album description and artist biography span the full content width below the header block. cc
This commit is contained in:
10 files changed
+461
-80
No files matched your search
@@ -56,9 +56,11 @@ component TrackRow inherits Rectangle {
|
||||
|
||||
height: 56px;
|
||||
border-radius: 8px;
|
||||
// The even-row tint is clearly fainter than the hover tint so the two
|
||||
// never read as the same state.
|
||||
background: ta.has-hover
|
||||
? Theme.surface-hover
|
||||
: (Math.mod(root.index, 2) == 1 ? Theme.surface-card : transparent);
|
||||
? #ffffff16
|
||||
: (Math.mod(root.index, 2) == 1 ? #ffffff07 : transparent);
|
||||
|
||||
// Row hover/background detector — declared before the content so the
|
||||
// more-button keeps its own clicks.
|
||||
@@ -144,28 +146,52 @@ component TrackRow inherits Rectangle {
|
||||
vertical-alignment: center;
|
||||
}
|
||||
|
||||
// Favorite column (visual).
|
||||
Rectangle {
|
||||
width: 28px;
|
||||
QbzIcon {
|
||||
source: @image-url("../assets/icons/heart.svg");
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
tint: Theme.text-muted;
|
||||
// Favorite button.
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
Rectangle {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: Radius.sm;
|
||||
background: fav-ta.has-hover ? Theme.surface-elevated : transparent;
|
||||
QbzIcon {
|
||||
source: @image-url("../assets/icons/heart.svg");
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
tint: fav-ta.has-hover ? Theme.text-primary : Theme.text-muted;
|
||||
}
|
||||
fav-ta := TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
clicked => {
|
||||
root.media-action("track", root.track.id, "favorite");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Offline-cache column (visual).
|
||||
Rectangle {
|
||||
width: 28px;
|
||||
QbzIcon {
|
||||
source: @image-url("../assets/icons/cloud-download.svg");
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
tint: Theme.text-muted;
|
||||
// Offline-cache button.
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
Rectangle {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: Radius.sm;
|
||||
background: dl-ta.has-hover ? Theme.surface-elevated : transparent;
|
||||
QbzIcon {
|
||||
source: @image-url("../assets/icons/cloud-download.svg");
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
tint: dl-ta.has-hover ? Theme.text-primary : Theme.text-muted;
|
||||
}
|
||||
dl-ta := TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
clicked => {
|
||||
root.media-action("track", root.track.id, "download");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,11 +403,13 @@ export component AlbumPageView inherits Rectangle {
|
||||
}
|
||||
artist-text := Text {
|
||||
text: AlbumState.artist;
|
||||
// Same color as the surrounding metadata,
|
||||
// bold to read as a link.
|
||||
color: artist-area.has-hover
|
||||
? Theme.accent-hover
|
||||
: Theme.accent;
|
||||
? Theme.text-primary
|
||||
: Theme.text-secondary;
|
||||
font-size: Typography.heading;
|
||||
font-weight: Typography.medium;
|
||||
font-weight: Typography.bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -393,34 +421,6 @@ export component AlbumPageView inherits Rectangle {
|
||||
overflow: elide;
|
||||
}
|
||||
|
||||
// Editorial description — fills the metadata column's
|
||||
// width (the dead space right of the artwork), truncated,
|
||||
// with a Read more link to the full modal.
|
||||
if AlbumState.description != "": Rectangle { height: 12px; }
|
||||
if AlbumState.description != "": Text {
|
||||
text: AlbumState.description-short;
|
||||
color: Theme.text-secondary;
|
||||
font-size: Typography.legal;
|
||||
wrap: word-wrap;
|
||||
}
|
||||
if AlbumState.description != AlbumState.description-short: Rectangle { height: 4px; }
|
||||
if AlbumState.description != AlbumState.description-short: read-more := TouchArea {
|
||||
width: read-more-row.preferred-width;
|
||||
height: 20px;
|
||||
mouse-cursor: pointer;
|
||||
clicked => {
|
||||
root.show-description = true;
|
||||
}
|
||||
read-more-row := HorizontalLayout {
|
||||
Text {
|
||||
text: @tr("Read more");
|
||||
color: read-more.has-hover ? Theme.accent-hover : Theme.accent;
|
||||
font-size: Typography.legal;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { height: 20px; }
|
||||
HorizontalLayout {
|
||||
alignment: start;
|
||||
@@ -474,6 +474,33 @@ export component AlbumPageView inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// Editorial description — full content width (the artwork no
|
||||
// longer caps it), truncated, with a Read more link.
|
||||
if AlbumState.description != "": Rectangle { height: 18px; }
|
||||
if AlbumState.description != "": Text {
|
||||
text: AlbumState.description-short;
|
||||
color: Theme.text-secondary;
|
||||
font-size: Typography.body;
|
||||
wrap: word-wrap;
|
||||
}
|
||||
if AlbumState.description != AlbumState.description-short: Rectangle { height: 4px; }
|
||||
if AlbumState.description != AlbumState.description-short: read-more := TouchArea {
|
||||
width: read-more-row.preferred-width;
|
||||
height: 20px;
|
||||
mouse-cursor: pointer;
|
||||
clicked => {
|
||||
root.show-description = true;
|
||||
}
|
||||
read-more-row := HorizontalLayout {
|
||||
Text {
|
||||
text: @tr("Read more");
|
||||
color: read-more.has-hover ? Theme.accent-hover : Theme.accent;
|
||||
font-size: Typography.legal;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { height: 20px; }
|
||||
Rectangle {
|
||||
height: 1px;
|
||||
|
||||
@@ -54,9 +54,11 @@ component TrackRow inherits Rectangle {
|
||||
|
||||
height: 52px;
|
||||
border-radius: 8px;
|
||||
// The even-row tint is clearly fainter than the hover tint so the two
|
||||
// never read as the same state.
|
||||
background: ta.has-hover
|
||||
? Theme.surface-hover
|
||||
: (Math.mod(root.index, 2) == 1 ? Theme.surface-card : transparent);
|
||||
? #ffffff16
|
||||
: (Math.mod(root.index, 2) == 1 ? #ffffff07 : transparent);
|
||||
|
||||
ta := TouchArea {
|
||||
mouse-cursor: default;
|
||||
@@ -253,14 +255,6 @@ export component ArtistPageView inherits Rectangle {
|
||||
font-weight: Typography.bold;
|
||||
overflow: elide;
|
||||
}
|
||||
Rectangle { height: 12px; }
|
||||
Text {
|
||||
text: ArtistState.bio;
|
||||
visible: ArtistState.bio != "";
|
||||
color: Theme.text-secondary;
|
||||
font-size: Typography.body;
|
||||
wrap: word-wrap;
|
||||
}
|
||||
Rectangle { height: 18px; }
|
||||
HorizontalLayout {
|
||||
alignment: start;
|
||||
@@ -290,6 +284,15 @@ export component ArtistPageView inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// Biography — full content width, below the header block.
|
||||
if ArtistState.bio != "": Rectangle { height: 18px; }
|
||||
if ArtistState.bio != "": Text {
|
||||
text: ArtistState.bio;
|
||||
color: Theme.text-secondary;
|
||||
font-size: Typography.body;
|
||||
wrap: word-wrap;
|
||||
}
|
||||
|
||||
Rectangle { height: 36px; }
|
||||
|
||||
if ArtistState.loading && ArtistState.top-tracks.length == 0: Text {
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect width="18" height="18" x="3" y="3" rx="2" />
|
||||
<path d="M9 3v18" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 260 B |
@@ -6,10 +6,12 @@
|
||||
// content well); no hairline borders.
|
||||
|
||||
import { Theme } from "../foundation/semantic-colors.slint";
|
||||
import { NavState, ContentView } from "../state.slint";
|
||||
import { NavState, ContentView, ShellState } from "../state.slint";
|
||||
import { Sidebar } from "Sidebar.slint";
|
||||
import { HeaderBar } from "HeaderBar.slint";
|
||||
import { PlayerBar } from "PlayerBar.slint";
|
||||
import { QueueSidebar } from "QueueSidebar.slint";
|
||||
import { LyricsSidebar } from "LyricsSidebar.slint";
|
||||
import { HomeView } from "../discover/HomeView.slint";
|
||||
import { AlbumPageView } from "../album/AlbumPageView.slint";
|
||||
import { ArtistPageView } from "../artist/ArtistPageView.slint";
|
||||
@@ -105,6 +107,11 @@ export component AppShell inherits Rectangle {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Right-side panel — a layout sibling of the content area,
|
||||
// so it shrinks the content rather than overlaying it.
|
||||
if ShellState.right-panel == 1: QueueSidebar { }
|
||||
if ShellState.right-panel == 2: LyricsSidebar { }
|
||||
}
|
||||
|
||||
PlayerBar { }
|
||||
|
||||
@@ -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 } from "../state.slint";
|
||||
import { SessionState, NavState, ContentView, ShellState } from "../state.slint";
|
||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||
|
||||
component NavTab inherits Rectangle {
|
||||
@@ -97,15 +97,104 @@ component MenuItem inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// Square icon button for the header's right-side controls.
|
||||
// Text-only menu row for the nav-tab dropdowns (no leading icon).
|
||||
component NavMenuItem inherits Rectangle {
|
||||
in property <string> label;
|
||||
callback clicked();
|
||||
|
||||
height: 32px;
|
||||
background: ta.has-hover ? Theme.surface-hover : transparent;
|
||||
|
||||
HorizontalLayout {
|
||||
padding-left: 14px;
|
||||
padding-right: 22px;
|
||||
Text {
|
||||
text: root.label;
|
||||
color: Theme.text-secondary;
|
||||
font-size: 13px;
|
||||
vertical-alignment: center;
|
||||
horizontal-stretch: 1;
|
||||
}
|
||||
}
|
||||
|
||||
ta := TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
clicked => {
|
||||
root.clicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A nav tab plus its dropdown menu. Clicking the tab opens a
|
||||
// PopupWindow positioned just below it, styled like the hamburger menu.
|
||||
// Menu rows are visual placeholders for now; the optional `home-item`
|
||||
// index marks the one row that keeps the app on the working Home view.
|
||||
component NavTabWithMenu inherits Rectangle {
|
||||
in property <image> icon;
|
||||
in property <string> label;
|
||||
in property <bool> active: false;
|
||||
in property <bool> show-icon: true;
|
||||
in property <[string]> items;
|
||||
// Index of the row wired to Home (-1 = none). Clicking it switches
|
||||
// to ContentView.home; all other rows are harmless no-ops.
|
||||
in property <int> home-item: -1;
|
||||
|
||||
width: tab.width;
|
||||
height: 30px;
|
||||
|
||||
tab := NavTab {
|
||||
icon: root.icon;
|
||||
label: root.label;
|
||||
active: root.active;
|
||||
show-icon: root.show-icon;
|
||||
clicked => {
|
||||
menu.show();
|
||||
}
|
||||
}
|
||||
|
||||
menu := PopupWindow {
|
||||
x: 0;
|
||||
y: parent.height + 6px;
|
||||
width: 188px;
|
||||
height: menu-box.preferred-height;
|
||||
|
||||
menu-box := Rectangle {
|
||||
background: Theme.surface-elevated;
|
||||
border-radius: Radius.md;
|
||||
border-width: 1px;
|
||||
border-color: Theme.border-subtle;
|
||||
drop-shadow-blur: 24px;
|
||||
drop-shadow-color: Theme.card-shadow;
|
||||
drop-shadow-offset-y: 6px;
|
||||
|
||||
VerticalLayout {
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
for entry[idx] in root.items: NavMenuItem {
|
||||
label: entry;
|
||||
clicked => {
|
||||
if (idx == root.home-item) {
|
||||
NavState.view = ContentView.home;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Square icon button for the header's right-side controls. `active`
|
||||
// gives it a highlighted look when its associated panel is open.
|
||||
component HeaderIconButton inherits Rectangle {
|
||||
in property <image> icon;
|
||||
in property <bool> active: false;
|
||||
callback clicked();
|
||||
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: Radius.sm;
|
||||
background: ta.has-hover ? Theme.surface-hover : transparent;
|
||||
background: root.active ? Theme.surface-elevated
|
||||
: ta.has-hover ? Theme.surface-hover : transparent;
|
||||
|
||||
QbzIcon {
|
||||
source: root.icon;
|
||||
@@ -113,7 +202,8 @@ component HeaderIconButton inherits Rectangle {
|
||||
height: 19px;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
tint: ta.has-hover ? Theme.text-primary : Theme.text-secondary;
|
||||
tint: root.active ? Theme.accent
|
||||
: ta.has-hover ? Theme.text-primary : Theme.text-secondary;
|
||||
}
|
||||
|
||||
ta := TouchArea {
|
||||
@@ -141,26 +231,34 @@ export component HeaderBar inherits Rectangle {
|
||||
y: (root.height - self.height) / 2;
|
||||
height: 30px;
|
||||
spacing: Spacing.xs;
|
||||
NavTab {
|
||||
NavTabWithMenu {
|
||||
icon: @image-url("../assets/icons/compass.svg");
|
||||
label: "Discover";
|
||||
label: @tr("Discover");
|
||||
active: true;
|
||||
show-icon: root.show-tab-icons;
|
||||
items: [@tr("Home"), @tr("Editor's Picks"), @tr("For You")];
|
||||
// Row 0 = Home, the one working destination.
|
||||
home-item: 0;
|
||||
}
|
||||
NavTab {
|
||||
NavTabWithMenu {
|
||||
icon: @image-url("../assets/icons/library-big.svg");
|
||||
label: "Library";
|
||||
label: @tr("Library");
|
||||
show-icon: root.show-tab-icons;
|
||||
items: [@tr("Tracks"), @tr("Albums"), @tr("Artists"),
|
||||
@tr("Playlists"), @tr("Labels")];
|
||||
}
|
||||
NavTab {
|
||||
NavTabWithMenu {
|
||||
icon: @image-url("../assets/icons/hard-drive.svg");
|
||||
label: "Local Library";
|
||||
label: @tr("Local Library");
|
||||
show-icon: root.show-tab-icons;
|
||||
items: [@tr("Albums"), @tr("Artists"), @tr("Folders"),
|
||||
@tr("Tracks")];
|
||||
}
|
||||
NavTab {
|
||||
NavTabWithMenu {
|
||||
icon: @image-url("../assets/icons/cassette-tape.svg");
|
||||
label: "My QBZ";
|
||||
label: @tr("My QBZ");
|
||||
show-icon: root.show-tab-icons;
|
||||
items: [@tr("Collections"), @tr("Mixtapes")];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,11 +304,21 @@ export component HeaderBar inherits Rectangle {
|
||||
width: 112px;
|
||||
height: 36px;
|
||||
spacing: 2px;
|
||||
// Lyrics panel toggle.
|
||||
HeaderIconButton {
|
||||
icon: @image-url("../assets/icons/captions.svg");
|
||||
active: ShellState.right-panel == 2;
|
||||
clicked => {
|
||||
ShellState.right-panel = ShellState.right-panel == 2 ? 0 : 2;
|
||||
}
|
||||
}
|
||||
// Queue panel toggle.
|
||||
HeaderIconButton {
|
||||
icon: @image-url("../assets/icons/list-ordered.svg");
|
||||
active: ShellState.right-panel == 1;
|
||||
clicked => {
|
||||
ShellState.right-panel = ShellState.right-panel == 1 ? 0 : 1;
|
||||
}
|
||||
}
|
||||
HeaderIconButton {
|
||||
icon: @image-url("../assets/icons/menu.svg");
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
// Right-side Lyrics panel.
|
||||
//
|
||||
// Rendered as a layout sibling of the content area (see AppShell), so it
|
||||
// shrinks the content rather than overlaying it. Closing sets
|
||||
// ShellState.right-panel = 0. The body is a structural placeholder;
|
||||
// lyric contents are wired in a later milestone.
|
||||
|
||||
import { Theme } from "../foundation/semantic-colors.slint";
|
||||
import { Spacing } from "../foundation/spacing.slint";
|
||||
import { Typography } from "../foundation/typography.slint";
|
||||
import { Radius } from "../foundation/radius.slint";
|
||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||
import { ShellState } from "../state.slint";
|
||||
|
||||
component CloseButton inherits Rectangle {
|
||||
callback clicked();
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: Radius.sm;
|
||||
background: ta.has-hover ? Theme.surface-hover : transparent;
|
||||
|
||||
QbzIcon {
|
||||
source: @image-url("../assets/icons/x.svg");
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
tint: ta.has-hover ? Theme.text-primary : Theme.text-secondary;
|
||||
}
|
||||
|
||||
ta := TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
clicked => {
|
||||
root.clicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export component LyricsSidebar inherits Rectangle {
|
||||
width: 320px;
|
||||
background: Theme.surface-card;
|
||||
|
||||
VerticalLayout {
|
||||
spacing: 0px;
|
||||
|
||||
// Header row — title + close button.
|
||||
HorizontalLayout {
|
||||
padding-left: Spacing.md;
|
||||
padding-right: Spacing.sm;
|
||||
padding-top: Spacing.sm;
|
||||
padding-bottom: Spacing.sm;
|
||||
height: 44px;
|
||||
Text {
|
||||
text: @tr("Lyrics");
|
||||
color: Theme.text-primary;
|
||||
font-size: Typography.heading;
|
||||
font-weight: Typography.semibold;
|
||||
vertical-alignment: center;
|
||||
horizontal-stretch: 1;
|
||||
}
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
CloseButton {
|
||||
clicked => {
|
||||
ShellState.right-panel = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
height: 1px;
|
||||
background: Theme.border-subtle;
|
||||
}
|
||||
|
||||
// Body — placeholder until lyric contents are wired.
|
||||
Rectangle {
|
||||
vertical-stretch: 1;
|
||||
Text {
|
||||
text: @tr("No lyrics");
|
||||
color: Theme.text-muted;
|
||||
font-size: Typography.body;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
// Rust nav history records every move.
|
||||
|
||||
import { Theme } from "../foundation/semantic-colors.slint";
|
||||
import { NavState } from "../state.slint";
|
||||
import { NavState, ShellState } from "../state.slint";
|
||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||
|
||||
component NavBtn inherits Rectangle {
|
||||
@@ -48,6 +48,19 @@ export component NavButtons inherits Rectangle {
|
||||
btn-row := HorizontalLayout {
|
||||
padding: 3px;
|
||||
spacing: 4px;
|
||||
// Leading button: collapses / expands the left sidebar. Lives
|
||||
// here so it appears just left of the [<] [>] pair in every view.
|
||||
NavBtn {
|
||||
icon: @image-url("../assets/icons/panel-left.svg");
|
||||
clicked => {
|
||||
ShellState.sidebar-collapsed = !ShellState.sidebar-collapsed;
|
||||
}
|
||||
}
|
||||
// 1px separator between the collapse button and the history pair.
|
||||
Rectangle {
|
||||
width: 1px;
|
||||
background: Theme.border-subtle;
|
||||
}
|
||||
NavBtn {
|
||||
icon: @image-url("../assets/icons/chevron-left.svg");
|
||||
enabled: NavState.can-back;
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
// Right-side Queue panel.
|
||||
//
|
||||
// Rendered as a layout sibling of the content area (see AppShell), so it
|
||||
// shrinks the content rather than overlaying it. Closing sets
|
||||
// ShellState.right-panel = 0. The body is a structural placeholder;
|
||||
// queue contents are wired in a later milestone.
|
||||
|
||||
import { Theme } from "../foundation/semantic-colors.slint";
|
||||
import { Spacing } from "../foundation/spacing.slint";
|
||||
import { Typography } from "../foundation/typography.slint";
|
||||
import { Radius } from "../foundation/radius.slint";
|
||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||
import { ShellState } from "../state.slint";
|
||||
|
||||
component CloseButton inherits Rectangle {
|
||||
callback clicked();
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: Radius.sm;
|
||||
background: ta.has-hover ? Theme.surface-hover : transparent;
|
||||
|
||||
QbzIcon {
|
||||
source: @image-url("../assets/icons/x.svg");
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
tint: ta.has-hover ? Theme.text-primary : Theme.text-secondary;
|
||||
}
|
||||
|
||||
ta := TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
clicked => {
|
||||
root.clicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export component QueueSidebar inherits Rectangle {
|
||||
width: 320px;
|
||||
background: Theme.surface-card;
|
||||
|
||||
VerticalLayout {
|
||||
spacing: 0px;
|
||||
|
||||
// Header row — title + close button.
|
||||
HorizontalLayout {
|
||||
padding-left: Spacing.md;
|
||||
padding-right: Spacing.sm;
|
||||
padding-top: Spacing.sm;
|
||||
padding-bottom: Spacing.sm;
|
||||
height: 44px;
|
||||
Text {
|
||||
text: @tr("Queue");
|
||||
color: Theme.text-primary;
|
||||
font-size: Typography.heading;
|
||||
font-weight: Typography.semibold;
|
||||
vertical-alignment: center;
|
||||
horizontal-stretch: 1;
|
||||
}
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
CloseButton {
|
||||
clicked => {
|
||||
ShellState.right-panel = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
height: 1px;
|
||||
background: Theme.border-subtle;
|
||||
}
|
||||
|
||||
// Body — placeholder until queue contents are wired.
|
||||
Rectangle {
|
||||
vertical-stretch: 1;
|
||||
Text {
|
||||
text: @tr("Nothing queued");
|
||||
color: Theme.text-muted;
|
||||
font-size: Typography.body;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,18 +3,31 @@
|
||||
// The playlist list is empty until real playlist data is wired (the
|
||||
// sidebar is a later milestone). The user element now lives in the
|
||||
// header, not at the bottom of the sidebar.
|
||||
//
|
||||
// The sidebar is collapsible: its rendered width is driven by
|
||||
// ShellState (0px when collapsed, otherwise the user-set width). A thin
|
||||
// drag handle on the right edge resizes it, clamped to [200px, 420px];
|
||||
// dragging below ~160px collapses it entirely.
|
||||
|
||||
import { Theme } from "../foundation/semantic-colors.slint";
|
||||
import { Spacing } from "../foundation/spacing.slint";
|
||||
import { Layout } from "../foundation/layout.slint";
|
||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||
import { ShellState } from "../state.slint";
|
||||
import { ScrollView } from "std-widgets.slint";
|
||||
|
||||
export component Sidebar inherits Rectangle {
|
||||
width: Layout.sidebar-width;
|
||||
// Rendered width — 0 when collapsed, otherwise the user-set width.
|
||||
width: ShellState.sidebar-collapsed ? 0px : ShellState.sidebar-width;
|
||||
// The sidebar sits one surface above the content area, matching the
|
||||
// QBZ dark theme (--bg-secondary sidebar over --bg-primary content).
|
||||
background: Theme.surface-card;
|
||||
// Clip so content doesn't spill while the width animates to 0.
|
||||
clip: true;
|
||||
animate width { duration: 160ms; easing: ease-in-out; }
|
||||
|
||||
// Captured sidebar width at pointer-down, so drag deltas are
|
||||
// measured from a stable origin.
|
||||
property <length> drag-start-width: ShellState.sidebar-width;
|
||||
|
||||
VerticalLayout {
|
||||
padding: Spacing.md;
|
||||
@@ -22,7 +35,7 @@ export component Sidebar inherits Rectangle {
|
||||
|
||||
HorizontalLayout {
|
||||
Text {
|
||||
text: "PLAYLISTS";
|
||||
text: @tr("PLAYLISTS");
|
||||
color: Theme.text-muted;
|
||||
font-size: 11px;
|
||||
letter-spacing: 1.5px;
|
||||
@@ -50,4 +63,30 @@ export component Sidebar inherits Rectangle {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Drag handle — a thin strip pinned to the right edge. Resizes the
|
||||
// sidebar; below ~160px it collapses instead of resizing.
|
||||
drag-handle := TouchArea {
|
||||
x: parent.width - self.width;
|
||||
width: 6px;
|
||||
height: parent.height;
|
||||
mouse-cursor: ew-resize;
|
||||
|
||||
pointer-event(e) => {
|
||||
if (e.kind == PointerEventKind.down) {
|
||||
root.drag-start-width = ShellState.sidebar-width;
|
||||
}
|
||||
}
|
||||
moved => {
|
||||
if (self.pressed) {
|
||||
if (root.drag-start-width + (self.mouse-x - self.pressed-x) < 160px) {
|
||||
ShellState.sidebar-collapsed = true;
|
||||
} else {
|
||||
ShellState.sidebar-width = Math.min(Math.max(
|
||||
root.drag-start-width + (self.mouse-x - self.pressed-x),
|
||||
200px), 420px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,6 +168,16 @@ export global ArtistState {
|
||||
in property <bool> loading: false;
|
||||
}
|
||||
|
||||
// Shell chrome state — collapsible left sidebar and right-side panels.
|
||||
// Globals are imported directly by the shell components, so toggles
|
||||
// need no callback plumbing.
|
||||
export global ShellState {
|
||||
in-out property <bool> sidebar-collapsed: false;
|
||||
in-out property <length> sidebar-width: 280px;
|
||||
// 0 = none, 1 = queue, 2 = lyrics
|
||||
in-out property <int> right-panel: 0;
|
||||
}
|
||||
|
||||
// Authenticated user, for the header user element.
|
||||
export global SessionState {
|
||||
in property <string> user-name: "";
|
||||
|
||||
Reference in new issue
Block a user