NowPlayingBar / SongCard: - Audio stamp homologated New<->Classic + vertically centred (alignment:center on the stamp column; New had a binding-loop card height, now explicit). - Dot-LED output indicators (circle-dot/circle) in both modes; render-target badge moved into the LED row when a peer/cast device owns playback. - Right-cluster buttons made square (32x32); context glyph -> disc-album, flush-left; classic stamp right margin; quality tooltip = the quality text; heart tooltip = Add to library; Qobuz Connect tooltip always shows On/Off. Qobuz Connect: - Clear the session topology on disconnect so a late event cannot resurrect the golden render badge (badge-persists-after-disconnect fix). Queue / Lyrics right panel: - Queue and Lyrics can both be open at once, stacked vertically in one column (independent queue-open/lyrics-open flags replace the exclusive right-panel int); each closable from its bar button and its own X. - Panel width 280; compact rows (zebra), now-playing card homologated with a row, flexible footer search, Lyrics title sized like the Queue tabs. Shell layout: - Pin AppShell to the window and size the content area arithmetically (window - header - player-bar - padding). A Slint VerticalLayout ignores an explicit height and does not auto-fill its parent Rectangle, so the content row collapsed once the right panels became conditionally mounted; a Rectangle takes the explicit height and it cascades up. Bar height references the single Layout.player-bar-height source (note left for the future Small/Large bars). cc
229 lines
8.9 KiB
Plaintext
229 lines
8.9 KiB
Plaintext
// Now-playing transport cluster: shuffle, previous, play/pause, next, repeat,
|
|
// and a trailing add affordance. Extracted from PlayerBar so the same cluster
|
|
// can sit in the CENTRE column (New layout) or the LEFT column (Classic layout)
|
|
// without duplicating popups.
|
|
//
|
|
// `play-circle` = New filled disc vs Classic plain glyph.
|
|
// `classic-actions` = New keeps the grouped "+" add menu; Classic swaps it for
|
|
// an inline one-click add-to-playlist + a favorite toggle
|
|
// (filled-heart state), 1:1 with the Tauri Classic transport.
|
|
// The track-info "(i)" lives in the song card, not here.
|
|
//
|
|
// Reads NowPlayingState + QueueState directly; bubbles add/menu actions through
|
|
// `media-action` to PlayerBar.
|
|
|
|
import { IconButton, PlayButton } from "../primitives/BarControls.slint";
|
|
import { ContextMenu } from "../primitives/ContextMenu.slint";
|
|
import { ContextMenuItem } from "../primitives/ContextMenuItem.slint";
|
|
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
|
import { Theme } from "../foundation/semantic-colors.slint";
|
|
import { Radius } from "../foundation/radius.slint";
|
|
import { NowPlayingState, QueueState, TooltipState } from "../state.slint";
|
|
|
|
export component TransportControls inherits HorizontalLayout {
|
|
// New layout = filled white disc; Classic = plain glyph.
|
|
in property <bool> play-circle: true;
|
|
// Classic = inline add-to-playlist + favorite; New = grouped "+" menu.
|
|
in property <bool> classic-actions: false;
|
|
callback media-action(string /* kind */, string /* id */, string /* action */);
|
|
|
|
spacing: 2px;
|
|
|
|
// Track info — the first control (left of shuffle), 1:1 with the other
|
|
// views. The song-card title is the second trigger. Keeping it here also
|
|
// restores PLAY's symmetric centring (3 controls each side) in New.
|
|
VerticalLayout {
|
|
alignment: center;
|
|
IconButton {
|
|
icon: @image-url("../assets/icons/info.svg");
|
|
tooltip: @tr("Track info");
|
|
clicked => {
|
|
if (NowPlayingState.has-track) {
|
|
root.media-action("track", NowPlayingState.track-id, "track-info");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
VerticalLayout {
|
|
alignment: center;
|
|
IconButton {
|
|
icon: @image-url("../assets/icons/shuffle.svg");
|
|
icon-size: 16px;
|
|
tooltip: @tr("Shuffle");
|
|
active: NowPlayingState.shuffle;
|
|
clicked => {
|
|
NowPlayingState.toggle-shuffle();
|
|
}
|
|
}
|
|
}
|
|
VerticalLayout {
|
|
alignment: center;
|
|
IconButton {
|
|
icon: @image-url("../assets/icons/skip-back.svg");
|
|
tooltip: @tr("Previous");
|
|
clicked => {
|
|
NowPlayingState.previous();
|
|
}
|
|
}
|
|
}
|
|
VerticalLayout {
|
|
alignment: center;
|
|
PlayButton {
|
|
circle: root.play-circle;
|
|
icon: NowPlayingState.playing
|
|
? @image-url("../assets/icons/pause.svg")
|
|
: @image-url("../assets/icons/play-fill.svg");
|
|
tooltip: NowPlayingState.playing ? @tr("Pause") : @tr("Play");
|
|
clicked => {
|
|
NowPlayingState.toggle-play();
|
|
}
|
|
}
|
|
}
|
|
VerticalLayout {
|
|
alignment: center;
|
|
IconButton {
|
|
icon: @image-url("../assets/icons/skip-forward.svg");
|
|
tooltip: @tr("Next");
|
|
clicked => {
|
|
NowPlayingState.next();
|
|
}
|
|
}
|
|
}
|
|
VerticalLayout {
|
|
alignment: center;
|
|
IconButton {
|
|
icon: NowPlayingState.repeat-mode == 2
|
|
? @image-url("../assets/icons/repeat-1.svg")
|
|
: @image-url("../assets/icons/repeat.svg");
|
|
icon-size: 16px;
|
|
tooltip: @tr("Repeat");
|
|
active: NowPlayingState.repeat-mode != 0;
|
|
clicked => {
|
|
NowPlayingState.cycle-repeat();
|
|
}
|
|
}
|
|
}
|
|
|
|
// New: "+" — grouped add menu.
|
|
if !root.classic-actions: VerticalLayout {
|
|
alignment: center;
|
|
Rectangle {
|
|
width: 32px;
|
|
height: 32px;
|
|
IconButton {
|
|
icon: @image-url("../assets/icons/plus.svg");
|
|
icon-size: 16px;
|
|
tooltip: @tr("Add to…");
|
|
clicked => {
|
|
add-menu.show();
|
|
}
|
|
}
|
|
add-menu := PopupWindow {
|
|
x: 0px;
|
|
y: -222px;
|
|
width: 232px;
|
|
height: 214px;
|
|
close-policy: close-on-click;
|
|
ContextMenu {
|
|
ContextMenuItem {
|
|
icon: @image-url("../assets/icons/heart.svg");
|
|
label: @tr("Add to library");
|
|
clicked => {
|
|
root.media-action("track", NowPlayingState.track-id, "favorite");
|
|
}
|
|
}
|
|
ContextMenuItem {
|
|
icon: @image-url("../assets/icons/list-music.svg");
|
|
label: @tr("Add to playlist");
|
|
clicked => {
|
|
root.media-action("track", NowPlayingState.track-id, "add-to-playlist");
|
|
}
|
|
}
|
|
ContextMenuItem {
|
|
icon: @image-url("../assets/icons/list-end.svg");
|
|
label: @tr("Add to queue");
|
|
clicked => {
|
|
root.media-action("track", NowPlayingState.track-id, "queue");
|
|
}
|
|
}
|
|
ContextMenuItem {
|
|
icon: @image-url("../assets/icons/list-plus.svg");
|
|
label: @tr("Play next");
|
|
clicked => {
|
|
root.media-action("track", NowPlayingState.track-id, "play-next");
|
|
}
|
|
}
|
|
ContextMenuItem {
|
|
icon: @image-url("../assets/icons/cassette-tape.svg");
|
|
label: @tr("Add to mixtape");
|
|
clicked => {
|
|
root.media-action("track", NowPlayingState.track-id, "add-to-mixtape");
|
|
}
|
|
}
|
|
ContextMenuItem {
|
|
icon: @image-url("../assets/icons/disc-3.svg");
|
|
label: @tr("Add album to collection");
|
|
enabled: NowPlayingState.album-id != "";
|
|
clicked => {
|
|
root.media-action("album", NowPlayingState.album-id, "favorite");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Classic: inline add-to-playlist (one click) — 1:1 with Tauri.
|
|
if root.classic-actions: VerticalLayout {
|
|
alignment: center;
|
|
IconButton {
|
|
icon: @image-url("../assets/icons/plus.svg");
|
|
icon-size: 16px;
|
|
tooltip: @tr("Add to playlist");
|
|
clicked => {
|
|
root.media-action("track", NowPlayingState.track-id, "add-to-playlist");
|
|
}
|
|
}
|
|
}
|
|
// Classic: inline favorite toggle with filled-heart state — 1:1 with Tauri.
|
|
if root.classic-actions: VerticalLayout {
|
|
alignment: center;
|
|
Rectangle {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: Radius.sm;
|
|
background: fav-ta.has-hover ? Theme.surface-hover : transparent;
|
|
QbzIcon {
|
|
source: QueueState.now-playing-favorite
|
|
? @image-url("../assets/icons/heart-filled.svg")
|
|
: @image-url("../assets/icons/heart.svg");
|
|
width: 16px;
|
|
height: 16px;
|
|
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
|
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
|
tint: QueueState.now-playing-favorite
|
|
? Theme.favorite
|
|
: (fav-ta.has-hover ? Theme.text-primary : Theme.text-secondary);
|
|
}
|
|
fav-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
QueueState.toggle-now-playing-favorite();
|
|
}
|
|
changed has-hover => {
|
|
if (self.has-hover) {
|
|
TooltipState.text = QueueState.now-playing-favorite
|
|
? @tr("Remove from library")
|
|
: @tr("Add to library");
|
|
TooltipState.anchor-x = parent.absolute-position.x + parent.width / 2;
|
|
TooltipState.anchor-y = parent.absolute-position.y;
|
|
TooltipState.show = true;
|
|
} else {
|
|
TooltipState.show = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|