Shell & navigation: - Three-state sidebar (open/mini/closed); collapse cycles via header button. - Section nav as right-flyouts (sidebar) + compact header nav (closed) + full header tabs (nav-in-sidebar off), via a shared HeaderMenuOverlay. - Collapsed-folder right-flyout popup (max 4 + scrollbar), Rust-populated. - Thin auto-hiding sidebar scrollbar pinned to the edge (Flickable, drag-safe). - Slack-style bezel: rounded inset content panel over surface-card chrome. - Header restructure: nav buttons, window-centered search, app menu. Themes & components: - Live theme switching: Dark / OLED Black / Tokyo Night / System (std-widgets). - SegmentedTabBar (Discovery-style) with count badges for Library + Local Library. - MP3 quality-badge tier (format-first classification) across badges. - QbzSelect (Group/Sort) trimmed 20px. Behavior fixes: - Artist + Local Library Folder cover art now reach the rendered (visible/ grouped) models, not just the source set. - Favorites Playlists/Labels random buttons. - Playlist track click plays from the clicked track in visible order. - Dropdown idle-close pauses while the cursor rests on the menu. cc
322 lines
12 KiB
Plaintext
322 lines
12 KiB
Plaintext
// Bottom now-playing / transport bar.
|
|
//
|
|
// 110px tall. A seek bar near the top, then a controls row: transport
|
|
// cluster (left), the song card (center, no background/border), and the
|
|
// secondary actions + volume cluster (right). Queue and Lyrics are
|
|
// intentionally absent — they live in the header.
|
|
//
|
|
// Playback state is not wired yet; the values are placeholders until the
|
|
// playback session lands.
|
|
|
|
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 { QbzIcon } from "../primitives/QbzIcon.slint";
|
|
import { QbzSlider } from "../primitives/QbzSlider.slint";
|
|
import { SeekBar } from "SeekBar.slint";
|
|
import { NowPlayingState, ShellState, AppearanceState } from "../state.slint";
|
|
|
|
component IconButton inherits Rectangle {
|
|
in property <image> icon;
|
|
in property <length> icon-size: 18px;
|
|
in property <bool> active: false;
|
|
callback clicked();
|
|
|
|
width: 34px;
|
|
height: 34px;
|
|
border-radius: Radius.sm;
|
|
background: ta.has-hover ? Theme.surface-hover : transparent;
|
|
|
|
QbzIcon {
|
|
source: root.icon;
|
|
width: root.icon-size;
|
|
height: root.icon-size;
|
|
x: (parent.width - self.width) / 2;
|
|
y: (parent.height - self.height) / 2;
|
|
tint: root.active
|
|
? Theme.accent
|
|
: (ta.has-hover ? Theme.text-primary : Theme.text-secondary);
|
|
}
|
|
|
|
ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
root.clicked();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Primary, filled play/pause button.
|
|
component PlayButton inherits Rectangle {
|
|
in property <image> icon;
|
|
callback clicked();
|
|
|
|
width: 38px;
|
|
height: 38px;
|
|
border-radius: 19px;
|
|
background: ta.has-hover ? #ffffffd6 : #ffffff;
|
|
|
|
QbzIcon {
|
|
source: root.icon;
|
|
width: 20px;
|
|
height: 20px;
|
|
x: (parent.width - self.width) / 2;
|
|
y: (parent.height - self.height) / 2;
|
|
tint: #000000;
|
|
}
|
|
|
|
ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
root.clicked();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clickable album/artist link text.
|
|
component MetaLink inherits TouchArea {
|
|
in property <string> label;
|
|
callback activated();
|
|
|
|
width: link-text.preferred-width;
|
|
height: link-text.preferred-height;
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
root.activated();
|
|
}
|
|
link-text := Text {
|
|
text: root.label;
|
|
color: root.has-hover ? Theme.text-primary : Theme.text-muted;
|
|
font-size: 12px;
|
|
}
|
|
}
|
|
|
|
export component PlayerBar inherits Rectangle {
|
|
callback open-album(string);
|
|
callback open-artist(string);
|
|
|
|
height: Layout.player-bar-height;
|
|
background: Theme.surface-card;
|
|
|
|
VerticalLayout {
|
|
spacing: 0px;
|
|
|
|
// Gap from the top edge to the seek bar.
|
|
Rectangle { height: 14px; }
|
|
|
|
// --- Seek bar ----------------------------------------------------
|
|
HorizontalLayout {
|
|
padding-left: Spacing.md;
|
|
padding-right: Spacing.md;
|
|
SeekBar {
|
|
progress: NowPlayingState.progress;
|
|
cache: NowPlayingState.cache;
|
|
elapsed: NowPlayingState.elapsed;
|
|
remaining: NowPlayingState.remaining;
|
|
duration-secs: NowPlayingState.duration-secs;
|
|
seek(fraction) => {
|
|
NowPlayingState.seek(fraction);
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Controls row ------------------------------------------------
|
|
HorizontalLayout {
|
|
vertical-stretch: 1;
|
|
padding-left: Spacing.md;
|
|
padding-right: Spacing.md;
|
|
spacing: Spacing.md;
|
|
|
|
// Transport cluster.
|
|
VerticalLayout {
|
|
alignment: center;
|
|
HorizontalLayout {
|
|
spacing: 2px;
|
|
IconButton {
|
|
icon: @image-url("../assets/icons/shuffle.svg");
|
|
icon-size: 16px;
|
|
active: NowPlayingState.shuffle;
|
|
clicked => {
|
|
NowPlayingState.toggle-shuffle();
|
|
}
|
|
}
|
|
IconButton {
|
|
icon: @image-url("../assets/icons/skip-back.svg");
|
|
clicked => {
|
|
NowPlayingState.previous();
|
|
}
|
|
}
|
|
VerticalLayout {
|
|
alignment: center;
|
|
PlayButton {
|
|
icon: NowPlayingState.playing
|
|
? @image-url("../assets/icons/pause.svg")
|
|
: @image-url("../assets/icons/play-fill.svg");
|
|
clicked => {
|
|
NowPlayingState.toggle-play();
|
|
}
|
|
}
|
|
}
|
|
IconButton {
|
|
icon: @image-url("../assets/icons/skip-forward.svg");
|
|
clicked => {
|
|
NowPlayingState.next();
|
|
}
|
|
}
|
|
IconButton {
|
|
icon: NowPlayingState.repeat-mode == 2
|
|
? @image-url("../assets/icons/repeat-1.svg")
|
|
: @image-url("../assets/icons/repeat.svg");
|
|
icon-size: 16px;
|
|
active: NowPlayingState.repeat-mode != 0;
|
|
clicked => {
|
|
NowPlayingState.cycle-repeat();
|
|
}
|
|
}
|
|
IconButton { icon: @image-url("../assets/icons/plus.svg"); icon-size: 16px; }
|
|
IconButton { icon: @image-url("../assets/icons/heart.svg"); icon-size: 16px; }
|
|
}
|
|
}
|
|
|
|
// Song card — no background, no border.
|
|
HorizontalLayout {
|
|
horizontal-stretch: 1;
|
|
alignment: center;
|
|
HorizontalLayout {
|
|
spacing: 12px;
|
|
VerticalLayout {
|
|
alignment: center;
|
|
Rectangle {
|
|
width: 56px;
|
|
height: 56px;
|
|
border-radius: 6px;
|
|
background: Theme.surface-elevated;
|
|
clip: true;
|
|
if NowPlayingState.has-track && NowPlayingState.artwork.width > 0: Image {
|
|
source: NowPlayingState.artwork;
|
|
width: 100%;
|
|
height: 100%;
|
|
image-fit: cover;
|
|
}
|
|
}
|
|
}
|
|
VerticalLayout {
|
|
alignment: center;
|
|
spacing: 4px;
|
|
Text {
|
|
text: NowPlayingState.has-track
|
|
? NowPlayingState.title
|
|
: @tr("Nothing playing");
|
|
color: Theme.text-primary;
|
|
font-size: Typography.body;
|
|
font-weight: Typography.medium;
|
|
overflow: elide;
|
|
}
|
|
HorizontalLayout {
|
|
spacing: 6px;
|
|
alignment: start;
|
|
if NowPlayingState.has-track: MetaLink {
|
|
label: NowPlayingState.artist;
|
|
activated => {
|
|
if (NowPlayingState.artist-id != "") {
|
|
root.open-artist(NowPlayingState.artist-id);
|
|
}
|
|
}
|
|
}
|
|
if NowPlayingState.has-track: VerticalLayout {
|
|
alignment: center;
|
|
QbzIcon {
|
|
source: @image-url("../assets/icons/layers.svg");
|
|
width: 13px;
|
|
height: 13px;
|
|
tint: Theme.text-muted;
|
|
}
|
|
}
|
|
if NowPlayingState.has-track: MetaLink {
|
|
label: NowPlayingState.album;
|
|
activated => {
|
|
if (NowPlayingState.album-id != "") {
|
|
root.open-album(NowPlayingState.album-id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Secondary actions + volume cluster.
|
|
VerticalLayout {
|
|
alignment: center;
|
|
HorizontalLayout {
|
|
spacing: 2px;
|
|
IconButton { icon: @image-url("../assets/icons/cast.svg"); icon-size: 16px; }
|
|
IconButton { icon: @image-url("../assets/icons/picture-in-picture-2.svg"); icon-size: 16px; }
|
|
IconButton { icon: @image-url("../assets/icons/maximize-2.svg"); icon-size: 16px; }
|
|
IconButton { icon: @image-url("../assets/icons/audio-lines.svg"); icon-size: 16px; }
|
|
|
|
// Lyrics + queue panel toggles (moved here from the header).
|
|
IconButton {
|
|
icon: @image-url("../assets/icons/captions.svg");
|
|
icon-size: 16px;
|
|
active: ShellState.right-panel == 2;
|
|
clicked => {
|
|
ShellState.right-panel = ShellState.right-panel == 2 ? 0 : 2;
|
|
}
|
|
}
|
|
IconButton {
|
|
icon: @image-url("../assets/icons/list-ordered.svg");
|
|
icon-size: 16px;
|
|
active: ShellState.right-panel == 1;
|
|
clicked => {
|
|
ShellState.right-panel = ShellState.right-panel == 1 ? 0 : 1;
|
|
}
|
|
}
|
|
|
|
Rectangle { width: 6px; }
|
|
|
|
IconButton {
|
|
icon: NowPlayingState.muted
|
|
? @image-url("../assets/icons/volume-x.svg")
|
|
: @image-url("../assets/icons/volume-2.svg");
|
|
icon-size: 16px;
|
|
active: NowPlayingState.muted;
|
|
clicked => {
|
|
NowPlayingState.toggle-mute();
|
|
}
|
|
}
|
|
VerticalLayout {
|
|
alignment: center;
|
|
QbzSlider {
|
|
width: 90px;
|
|
minimum: 0;
|
|
maximum: 100;
|
|
value: NowPlayingState.volume * 100;
|
|
changed(v) => {
|
|
NowPlayingState.set-volume(v / 100.0);
|
|
}
|
|
}
|
|
}
|
|
// Volume +/- steppers — opt-in (Settings > Appearance).
|
|
if AppearanceState.show-volume-steppers: IconButton {
|
|
icon: @image-url("../assets/icons/minus.svg");
|
|
icon-size: 15px;
|
|
clicked => {
|
|
NowPlayingState.set-volume(Math.max(0.0, NowPlayingState.volume - 0.05));
|
|
}
|
|
}
|
|
if AppearanceState.show-volume-steppers: IconButton {
|
|
icon: @image-url("../assets/icons/plus.svg");
|
|
icon-size: 15px;
|
|
clicked => {
|
|
NowPlayingState.set-volume(Math.min(1.0, NowPlayingState.volume + 0.05));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|