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
690 lines
27 KiB
Plaintext
690 lines
27 KiB
Plaintext
// Top header bar — navigation tabs anchored left, the user element
|
|
// anchored right, and the search field absolutely centered on the window.
|
|
//
|
|
// The search field is positioned at the true window center (not inside a
|
|
// layout column) so it stays centered regardless of how wide the tab row
|
|
// or the user element become.
|
|
//
|
|
// Responsive: below the first breakpoint the tabs drop their icons;
|
|
// below the second the search field narrows.
|
|
|
|
import { Theme } from "../foundation/semantic-colors.slint";
|
|
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, SidebarPlaylistsPopupState } from "../state.slint";
|
|
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
|
|
|
component NavTab inherits Rectangle {
|
|
in property <image> icon;
|
|
in property <string> label;
|
|
in property <bool> active: false;
|
|
in property <bool> show-icon: true;
|
|
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;
|
|
border-radius: Radius.sm;
|
|
background: root.active ? Theme.surface-elevated
|
|
: ta.has-hover ? Theme.surface-hover : transparent;
|
|
|
|
tab-row := HorizontalLayout {
|
|
padding-left: root.show-icon ? 9px : 11px;
|
|
padding-right: 11px;
|
|
spacing: 6px;
|
|
if root.show-icon: VerticalLayout {
|
|
alignment: center;
|
|
QbzIcon {
|
|
source: root.icon;
|
|
width: 14px;
|
|
height: 14px;
|
|
tint: root.active ? Theme.text-primary : Theme.text-secondary;
|
|
}
|
|
}
|
|
// 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;
|
|
vertical-alignment: center;
|
|
color: Theme.text-primary;
|
|
font-size: 11px;
|
|
font-weight: root.active ? Typography.semibold : Typography.regular;
|
|
}
|
|
}
|
|
|
|
ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
root.clicked();
|
|
}
|
|
changed has-hover => {
|
|
root.hover-changed(self.has-hover);
|
|
if (self.has-hover) {
|
|
root.hovered();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
component MenuItem inherits Rectangle {
|
|
in property <image> icon;
|
|
in property <string> label;
|
|
callback clicked();
|
|
|
|
height: 34px;
|
|
background: ta.has-hover ? Theme.surface-hover : transparent;
|
|
|
|
HorizontalLayout {
|
|
padding-left: 12px;
|
|
padding-right: 18px;
|
|
spacing: 10px;
|
|
VerticalLayout {
|
|
alignment: center;
|
|
QbzIcon {
|
|
source: root.icon;
|
|
width: 15px;
|
|
height: 15px;
|
|
tint: Theme.text-secondary;
|
|
}
|
|
}
|
|
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.
|
|
//
|
|
// The tab no longer owns a PopupWindow — a PopupWindow grabs the pointer
|
|
// while open, so hovering a sibling tab delivered no hover event and the
|
|
// menus could not be hover-switched. Instead, hovering or clicking the
|
|
// tab writes this tab's identity (index, header x-position, menu rows,
|
|
// home-item) into the HeaderMenuState global. The actual menu panel is
|
|
// drawn by HeaderMenuOverlay, a late child of the AppShell root, so it
|
|
// renders on top of the content area and does not grab the pointer.
|
|
//
|
|
// Hovering a different tab simply overwrites HeaderMenuState, so the new
|
|
// menu opens and the previous one closes instantly. Single-open is
|
|
// automatic — there is one global `open-index`.
|
|
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 <[NavMenuEntry]> items;
|
|
// This tab's index (0..3) — written into HeaderMenuState.open-index.
|
|
in property <int> tab-index;
|
|
// 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;
|
|
|
|
// Publishes this tab's menu into the shared overlay state. The
|
|
// overlay aligns the panel under `anchor-x`; `absolute-position`
|
|
// resolves to a window-relative coordinate, which is what the
|
|
// AppShell-level overlay needs (the tab itself sits inside the
|
|
// header's tab-row layout, several offsets deep).
|
|
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;
|
|
}
|
|
|
|
tab := NavTab {
|
|
icon: root.icon;
|
|
label: root.label;
|
|
active: root.active;
|
|
show-icon: root.show-icon;
|
|
clicked => {
|
|
root.open-menu();
|
|
}
|
|
// Open the dropdown on hover, no click needed. Hovering a
|
|
// different tab overwrites the global → instant switch.
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Compact icon-only nav button shown in the header ONLY while the sidebar is
|
|
// fully closed (Cerrado), so the section nav stays reachable. Opens a downward
|
|
// dropdown headed by the section name (label + hairline + the clickable rows).
|
|
component CompactNavBtn inherits Rectangle {
|
|
in property <image> icon;
|
|
in property <string> title;
|
|
in property <[NavMenuEntry]> items;
|
|
in property <int> tab-index;
|
|
in property <int> home-item: -1;
|
|
|
|
width: 30px;
|
|
height: 30px;
|
|
border-radius: Radius.sm;
|
|
property <bool> active: HeaderMenuState.open-index == root.tab-index;
|
|
background: root.active ? Theme.surface-elevated
|
|
: cn-ta.has-hover ? Theme.surface-hover : transparent;
|
|
|
|
function open-menu() {
|
|
HeaderMenuState.open-index = root.tab-index;
|
|
HeaderMenuState.anchor-x = root.absolute-position.x;
|
|
HeaderMenuState.from-sidebar = false;
|
|
HeaderMenuState.title = root.title;
|
|
HeaderMenuState.items = root.items;
|
|
HeaderMenuState.home-item = root.home-item;
|
|
}
|
|
|
|
QbzIcon {
|
|
source: root.icon;
|
|
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;
|
|
}
|
|
cn-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => { root.open-menu(); }
|
|
changed has-hover => {
|
|
if (self.has-hover) {
|
|
HeaderMenuState.trigger-hovered = true;
|
|
root.open-menu();
|
|
} else if (root.active) {
|
|
// Only the trigger owning the open menu clears the flag — this
|
|
// avoids a leave/enter race when sliding between triggers.
|
|
HeaderMenuState.trigger-hovered = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
in property <image> icon;
|
|
in property <bool> enabled: true;
|
|
callback clicked();
|
|
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: 14px;
|
|
opacity: root.enabled ? 1.0 : 0.4;
|
|
background: hn-ta.has-hover && root.enabled
|
|
? Theme.surface-hover
|
|
: Theme.surface-elevated;
|
|
|
|
QbzIcon {
|
|
source: root.icon;
|
|
width: 15px;
|
|
height: 15px;
|
|
x: (parent.width - self.width) / 2;
|
|
y: (parent.height - self.height) / 2;
|
|
tint: root.enabled ? Theme.text-primary : Theme.text-muted;
|
|
}
|
|
|
|
hn-ta := TouchArea {
|
|
enabled: root.enabled;
|
|
mouse-cursor: root.enabled ? pointer : default;
|
|
clicked => {
|
|
if (root.enabled) {
|
|
root.clicked();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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: root.active ? Theme.surface-elevated
|
|
: ta.has-hover ? Theme.surface-hover : transparent;
|
|
|
|
QbzIcon {
|
|
source: root.icon;
|
|
width: 19px;
|
|
height: 19px;
|
|
// Round the centering to whole pixels — a fractional offset
|
|
// (here 8.5px) makes the glyph render blurry.
|
|
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
|
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
|
tint: root.active ? Theme.accent
|
|
: ta.has-hover ? Theme.text-primary : Theme.text-secondary;
|
|
}
|
|
|
|
ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
root.clicked();
|
|
}
|
|
}
|
|
}
|
|
|
|
export component HeaderBar inherits Rectangle {
|
|
callback logout();
|
|
callback close-app();
|
|
|
|
height: Layout.header-height;
|
|
background: Theme.surface-card;
|
|
|
|
// Responsive breakpoints.
|
|
property <bool> show-tab-icons: root.width >= 1140px;
|
|
// 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);
|
|
|
|
// 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;
|
|
border-color: Theme.border-subtle;
|
|
background: Theme.surface-elevated;
|
|
|
|
// Magnifier — absolutely positioned on the left so it does NOT take
|
|
// layout space; the text/placeholder center against the full input
|
|
// width as if the icon weren't there.
|
|
QbzIcon {
|
|
source: @image-url("../assets/icons/search.svg");
|
|
width: 14px;
|
|
height: 14px;
|
|
x: 10px;
|
|
y: (parent.height - self.height) / 2;
|
|
tint: Theme.text-muted;
|
|
}
|
|
|
|
// Full-width input — text centered against the whole box.
|
|
search-input := TextInput {
|
|
width: parent.width;
|
|
height: 100%;
|
|
color: Theme.text-primary;
|
|
font-size: 13px;
|
|
vertical-alignment: center;
|
|
horizontal-alignment: center;
|
|
single-line: true;
|
|
// Enter submits to the full results page; every edit feeds the
|
|
// debounced live search (Rust side).
|
|
accepted => { SearchActions.submit(self.text); }
|
|
edited => { SearchActions.live(self.text); }
|
|
}
|
|
Text {
|
|
visible: search-input.text == "";
|
|
text: @tr("Search");
|
|
width: parent.width;
|
|
height: 100%;
|
|
color: Theme.text-muted;
|
|
font-size: 13px;
|
|
vertical-alignment: center;
|
|
horizontal-alignment: center;
|
|
}
|
|
}
|
|
|
|
// --- Left controls: history nav + collapse (anchored left) -------------
|
|
// The three navigation buttons (back / forward / sidebar-collapse) float
|
|
// at the header's LEFT edge.
|
|
left-controls := HorizontalLayout {
|
|
x: Spacing.md;
|
|
y: (root.height - self.height) / 2;
|
|
width: self.preferred-width;
|
|
height: 36px;
|
|
spacing: 6px;
|
|
VerticalLayout {
|
|
alignment: center;
|
|
HeaderNavBtn {
|
|
icon: @image-url("../assets/icons/panel-left.svg");
|
|
clicked => { ShellState.cycle-sidebar(); }
|
|
}
|
|
}
|
|
VerticalLayout {
|
|
alignment: center;
|
|
HeaderNavBtn {
|
|
icon: @image-url("../assets/icons/chevron-left.svg");
|
|
enabled: NavState.can-back;
|
|
clicked => { NavState.request-back(); }
|
|
}
|
|
}
|
|
VerticalLayout {
|
|
alignment: center;
|
|
HeaderNavBtn {
|
|
icon: @image-url("../assets/icons/chevron-right.svg");
|
|
enabled: NavState.can-forward;
|
|
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.
|
|
if ShellState.sidebar-state == 2: HorizontalLayout {
|
|
spacing: 2px;
|
|
// A small gap from the history buttons.
|
|
Rectangle { width: 6px; }
|
|
VerticalLayout {
|
|
alignment: center;
|
|
CompactNavBtn {
|
|
tab-index: 0;
|
|
title: @tr("Discover");
|
|
icon: @image-url("../assets/icons/compass.svg");
|
|
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;
|
|
CompactNavBtn {
|
|
tab-index: 1;
|
|
title: @tr("Library");
|
|
icon: @image-url("../assets/icons/music-library-2.svg");
|
|
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;
|
|
CompactNavBtn {
|
|
tab-index: 2;
|
|
title: @tr("Local Library");
|
|
icon: @image-url("../assets/icons/hard-drive.svg");
|
|
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;
|
|
CompactNavBtn {
|
|
tab-index: 3;
|
|
title: @tr("My QBZ");
|
|
icon: @image-url("../assets/icons/cassette-tape.svg");
|
|
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 },
|
|
];
|
|
}
|
|
}
|
|
// 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 { }
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Right controls: app menu (anchored right) -------------------------
|
|
right-controls := HorizontalLayout {
|
|
x: root.width - self.width - Spacing.md + 2px;
|
|
y: (root.height - self.height) / 2;
|
|
width: 36px;
|
|
height: 36px;
|
|
VerticalLayout {
|
|
alignment: center;
|
|
HeaderIconButton {
|
|
icon: @image-url("../assets/icons/menu.svg");
|
|
clicked => {
|
|
menu.show();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
menu := PopupWindow {
|
|
x: parent.width - 234px - Spacing.md;
|
|
y: Layout.header-height - 4px;
|
|
width: 234px;
|
|
height: 310px;
|
|
|
|
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;
|
|
|
|
VerticalLayout {
|
|
padding-top: 6px;
|
|
padding-bottom: 6px;
|
|
|
|
// Signed-in user — name and subscription tier.
|
|
HorizontalLayout {
|
|
padding-left: 14px;
|
|
padding-right: 14px;
|
|
padding-top: 6px;
|
|
padding-bottom: 10px;
|
|
VerticalLayout {
|
|
spacing: 2px;
|
|
Text {
|
|
text: SessionState.user-name == ""
|
|
? @tr("Guest")
|
|
: SessionState.user-name;
|
|
color: Theme.text-primary;
|
|
font-size: Typography.body;
|
|
font-weight: Typography.semibold;
|
|
}
|
|
Text {
|
|
text: SessionState.subscription == ""
|
|
? @tr("Not signed in")
|
|
: SessionState.subscription;
|
|
color: Theme.text-muted;
|
|
font-size: 12px;
|
|
}
|
|
}
|
|
}
|
|
Rectangle {
|
|
height: 1px;
|
|
background: Theme.border-subtle;
|
|
}
|
|
Rectangle { height: 4px; }
|
|
|
|
MenuItem {
|
|
icon: @image-url("../assets/icons/settings.svg");
|
|
label: @tr("Settings");
|
|
clicked => {
|
|
NavState.request-settings();
|
|
}
|
|
}
|
|
MenuItem {
|
|
icon: @image-url("../assets/icons/keyboard.svg");
|
|
label: @tr("Keyboard Shortcuts");
|
|
}
|
|
MenuItem {
|
|
icon: @image-url("../assets/icons/book-open.svg");
|
|
label: @tr("Documentation");
|
|
}
|
|
MenuItem {
|
|
icon: @image-url("../assets/icons/bug.svg");
|
|
label: @tr("Report an Issue");
|
|
}
|
|
MenuItem {
|
|
icon: @image-url("../assets/icons/info.svg");
|
|
label: @tr("About");
|
|
}
|
|
Rectangle {
|
|
height: 1px;
|
|
background: Theme.border-subtle;
|
|
}
|
|
MenuItem {
|
|
icon: @image-url("../assets/icons/log-out.svg");
|
|
label: @tr("Log Out");
|
|
clicked => {
|
|
root.logout();
|
|
}
|
|
}
|
|
MenuItem {
|
|
icon: @image-url("../assets/icons/x.svg");
|
|
label: @tr("Close");
|
|
clicked => {
|
|
root.close-app();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|