Relocate the QConnect diagnostics launcher out of the now-playing bar flyout into a new opt-in Settings > Developer section (the modal + DEV log/status code is unchanged). Also make the status block selectable (read-only TextEdit) so session_uuid etc. can be copied, and relabel the event log. cc
756 lines
36 KiB
Plaintext
756 lines
36 KiB
Plaintext
// Bottom now-playing / transport bar — Default mode.
|
|
//
|
|
// Three clusters over a full-width seek bar: the SONG CARD (left), the INFO +
|
|
// TRANSPORT cluster (absolutely centred so it never drifts with the song-card
|
|
// width), and the SECONDARY actions + volume cluster (right). Grouped popovers
|
|
// open upward: "+" add menu, the Qobuz Connect flyout, the audio-settings
|
|
// (normalization + gapless) popover, and the layout-mode popover.
|
|
|
|
import { Theme } from "../foundation/semantic-colors.slint";
|
|
import { Spacing } from "../foundation/spacing.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 { QbzToggle } from "../primitives/QbzToggle.slint";
|
|
import { ContextMenu } from "../primitives/ContextMenu.slint";
|
|
import { ContextMenuItem } from "../primitives/ContextMenuItem.slint";
|
|
import { IconButton, PlayButton } from "../primitives/BarControls.slint";
|
|
import { SeekBar } from "SeekBar.slint";
|
|
import { SongCard } from "SongCard.slint";
|
|
import { TransportControls } from "TransportControls.slint";
|
|
import { NowPlayingState, ShellState, AppearanceState, SettingsState, TooltipState } from "../state.slint";
|
|
|
|
// IconButton + PlayButton now live in primitives/BarControls.slint (shared
|
|
// with TransportControls). ConnectButton + ToggleRow stay local (bar-only).
|
|
|
|
// Qobuz Connect button — monitor-speaker glyph; golden over a soft golden tint
|
|
// when a remote renderer owns playback. The renderer name lives in the song
|
|
// card (the remote badge under artist·album), not here.
|
|
component ConnectButton inherits Rectangle {
|
|
in property <bool> active: false;
|
|
in property <string> tooltip;
|
|
callback clicked();
|
|
|
|
property <color> gold: #e0b341;
|
|
|
|
width: 28px;
|
|
height: 32px;
|
|
border-radius: Radius.sm;
|
|
background: root.active
|
|
? root.gold.with-alpha(0.16)
|
|
: (ta.has-hover ? Theme.surface-hover : transparent);
|
|
border-width: root.active ? 1px : 0px;
|
|
border-color: root.gold.with-alpha(0.45);
|
|
|
|
QbzIcon {
|
|
source: @image-url("../assets/icons/monitor-speaker.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: root.active
|
|
? root.gold
|
|
: (ta.has-hover ? Theme.text-primary : Theme.text-secondary);
|
|
}
|
|
|
|
ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
root.clicked();
|
|
}
|
|
changed has-hover => {
|
|
if (self.has-hover && root.tooltip != "" && !root.active) {
|
|
TooltipState.text = root.tooltip;
|
|
TooltipState.anchor-x = root.absolute-position.x + root.width / 2;
|
|
TooltipState.anchor-y = root.absolute-position.y;
|
|
TooltipState.show = true;
|
|
} else if (!self.has-hover) {
|
|
TooltipState.show = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// A labelled toggle row for the audio-settings popover.
|
|
component ToggleRow inherits Rectangle {
|
|
in property <string> label;
|
|
in property <string> hint;
|
|
in property <bool> checked;
|
|
callback toggled(bool);
|
|
|
|
height: 44px;
|
|
HorizontalLayout {
|
|
padding-left: 12px;
|
|
padding-right: 12px;
|
|
spacing: 12px;
|
|
VerticalLayout {
|
|
alignment: center;
|
|
horizontal-stretch: 1;
|
|
spacing: 1px;
|
|
Text {
|
|
text: root.label;
|
|
color: Theme.text-primary;
|
|
font-size: 13px;
|
|
}
|
|
if root.hint != "": Text {
|
|
text: root.hint;
|
|
color: Theme.text-muted;
|
|
font-size: 10px;
|
|
}
|
|
}
|
|
VerticalLayout {
|
|
alignment: center;
|
|
QbzToggle {
|
|
checked: root.checked;
|
|
toggled(v) => {
|
|
root.toggled(v);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export component PlayerBar inherits Rectangle {
|
|
callback open-album(string);
|
|
callback open-artist(string);
|
|
callback media-action(string, string, string);
|
|
callback settings-bool(string, bool);
|
|
|
|
height: Layout.player-bar-height;
|
|
background: Theme.surface-card;
|
|
|
|
// Responsive column zones. The song-card (left) and the buttons (right)
|
|
// share the SAME fraction so they are symmetric — which makes the centre
|
|
// transport column absolutely centred (PLAY lands on the window centre) at
|
|
// every width. The side fraction shrinks as the window grows so the song
|
|
// card / buttons never sprawl on wide screens.
|
|
// width < 1366 -> 39% | 22% | 39%
|
|
// 1366..<1920 -> 30% | 40% | 30%
|
|
// width >= 1920 -> 25% | 50% | 25%
|
|
property <float> side-frac: root.width >= 1920px
|
|
? 0.25
|
|
: (root.width >= 1366px ? 0.30 : 0.39);
|
|
// Classic uses a fixed ~32.4/35.2/32.4 split (the centre song card is 12%
|
|
// narrower than a plain 40% so it isn't oversized in the open view); New
|
|
// uses the responsive side fraction above. BOTH modes keep the two side
|
|
// columns equal so the centre column stays dead centre.
|
|
property <float> col-side: ShellState.npb-mode == 1 ? 0.324 : root.side-frac;
|
|
property <float> col-centre: 1.0 - 2.0 * root.col-side;
|
|
|
|
VerticalLayout {
|
|
Rectangle { height: 6px; }
|
|
|
|
// Full-width seek bar (6px side margins, times at the ends).
|
|
HorizontalLayout {
|
|
padding-left: 6px;
|
|
padding-right: 6px;
|
|
SeekBar {
|
|
height: 20px;
|
|
progress: NowPlayingState.progress;
|
|
cache: NowPlayingState.cache;
|
|
seekable-max: NowPlayingState.seekable-max;
|
|
elapsed: NowPlayingState.elapsed;
|
|
remaining: NowPlayingState.remaining;
|
|
duration-secs: NowPlayingState.duration-secs;
|
|
seek(fraction) => {
|
|
NowPlayingState.seek(fraction);
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle { height: 6px; }
|
|
|
|
// --- Controls: responsive symmetric zones (39/22/39, 30/40/30,
|
|
// 25/50/25 by window width). The side columns share one fraction so
|
|
// the transport (centre) is symmetric and PLAY lands on the window
|
|
// centre (aligned with the header search + title). The album art
|
|
// fills the row height. Columns clip; tooltips are a global overlay
|
|
// so clipping is safe.
|
|
HorizontalLayout {
|
|
vertical-stretch: 1;
|
|
padding-left: 6px;
|
|
padding-right: 6px;
|
|
spacing: 0px;
|
|
|
|
// LEFT column. New = the song card (audio stamp floats to its
|
|
// right wall). Classic = the transport cluster hugging the left
|
|
// edge with a plain play glyph (the Tauri arrangement).
|
|
Rectangle {
|
|
width: (parent.width - 12px) * root.col-side;
|
|
clip: true;
|
|
// New: SongCard (a Rectangle) fills the column by default (a
|
|
// Rectangle child takes 100% of its parent), so its inner
|
|
// HorizontalLayout's stretch text column can push the audio
|
|
// stamp to the right wall. Vertical centring is handled by the
|
|
// card's own sub-columns. Do NOT wrap it in an `alignment:
|
|
// start` layout — that forces preferred sizing and kills both.
|
|
if ShellState.npb-mode == 0: SongCard {
|
|
// Fill the controls-row height (bar - top6 - seek20
|
|
// - gap6 - bottom6 = bar - 38).
|
|
art-size: Layout.player-bar-height - 38px;
|
|
// Below 1366px shrink the stamp 10% so it stays
|
|
// clear of the controls column; full size above.
|
|
narrow: root.width < 1366px;
|
|
open-album(id) => {
|
|
root.open-album(id);
|
|
}
|
|
open-artist(id) => {
|
|
root.open-artist(id);
|
|
}
|
|
open-context(kind, id) => {
|
|
root.media-action(kind, id, "open");
|
|
}
|
|
track-info() => {
|
|
root.media-action("track", NowPlayingState.track-id, "track-info");
|
|
}
|
|
}
|
|
// Classic: transport cluster, left-aligned, plain play glyph.
|
|
if ShellState.npb-mode == 1: VerticalLayout {
|
|
alignment: center;
|
|
HorizontalLayout {
|
|
alignment: start;
|
|
TransportControls {
|
|
play-circle: false;
|
|
classic-actions: true;
|
|
media-action(k, i, a) => {
|
|
root.media-action(k, i, a);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// CENTRE column. New = the transport cluster (symmetric, filled
|
|
// disc, absolutely centred). Classic = the glass song card, centred.
|
|
Rectangle {
|
|
width: (parent.width - 12px) * root.col-centre;
|
|
clip: true;
|
|
// New: centred transport, PLAY on the window centre.
|
|
if ShellState.npb-mode == 0: VerticalLayout {
|
|
alignment: center;
|
|
HorizontalLayout {
|
|
alignment: center;
|
|
TransportControls {
|
|
play-circle: true;
|
|
media-action(k, i, a) => {
|
|
root.media-action(k, i, a);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Classic: a contained glass card, centred and content-sized
|
|
// (clamped 354-718 like Tauri) so the audio stamp stays close
|
|
// to the track info instead of drifting to a far wall.
|
|
// Classic: the glass song card, mounted DIRECTLY in the column
|
|
// (a Rectangle child fills its Rectangle parent — exactly like
|
|
// the New layout). That fill is what lets the text column stretch
|
|
// and push the audio stamp FLUSH to the card's right edge. A
|
|
// VerticalLayout wrapper here breaks the fill and floats the
|
|
// stamp with a trailing gap — do NOT reintroduce one.
|
|
//
|
|
// `max-width` is the cap: the card grows with the window only up
|
|
// to here, then stays put, so the audio stamp settles at a
|
|
// consistent right edge instead of drifting across a too-wide
|
|
// card. `height` + `y` float it (centred) in the 72px row.
|
|
if ShellState.npb-mode == 1: SongCard {
|
|
glass: true;
|
|
// Fixed cap. `max-width` is IGNORED on a hand-positioned
|
|
// Rectangle (only layouts honour it) — set the width
|
|
// directly: 520px cap on big screens, the full column when
|
|
// it is narrower. The text column then stretches inside this
|
|
// fixed card and pins the 140px audio stamp to its right edge.
|
|
width: Math.min(parent.width, 520px);
|
|
height: 60px;
|
|
y: (parent.height - self.height) / 2;
|
|
art-size: 56px;
|
|
// Full-size 1:1-Tauri stamp at every width.
|
|
compact-stamp: false;
|
|
narrow: root.width < 1366px;
|
|
open-album(id) => {
|
|
root.open-album(id);
|
|
}
|
|
open-artist(id) => {
|
|
root.open-artist(id);
|
|
}
|
|
open-context(kind, id) => {
|
|
root.media-action(kind, id, "open");
|
|
}
|
|
track-info() => {
|
|
root.media-action("track", NowPlayingState.track-id, "track-info");
|
|
}
|
|
}
|
|
}
|
|
|
|
// RIGHT column (responsive 39/30/25%, symmetric with the song
|
|
// card) — secondary actions + volume, clustered at the right wall.
|
|
Rectangle {
|
|
width: (parent.width - 12px) * root.col-side;
|
|
clip: true;
|
|
HorizontalLayout {
|
|
alignment: end;
|
|
spacing: 1px;
|
|
|
|
// Qobuz Connect — golden when remote; opens the connect flyout.
|
|
VerticalLayout {
|
|
alignment: center;
|
|
Rectangle {
|
|
width: 28px;
|
|
height: 32px;
|
|
ConnectButton {
|
|
active: NowPlayingState.is-remote;
|
|
tooltip: @tr("Qobuz Connect");
|
|
clicked => {
|
|
qconnect-menu.show();
|
|
}
|
|
}
|
|
qconnect-menu := PopupWindow {
|
|
x: 32px - 280px;
|
|
y: -252px;
|
|
width: 280px;
|
|
height: 244px;
|
|
close-policy: close-on-click-outside;
|
|
Rectangle {
|
|
background: Theme.surface-main;
|
|
border-radius: Radius.sm;
|
|
border-width: 1px;
|
|
border-color: Theme.border-muted;
|
|
drop-shadow-blur: 22px;
|
|
drop-shadow-color: Theme.card-shadow;
|
|
drop-shadow-offset-y: 6px;
|
|
VerticalLayout {
|
|
padding: 14px;
|
|
spacing: 12px;
|
|
// Header.
|
|
HorizontalLayout {
|
|
spacing: 8px;
|
|
QbzIcon {
|
|
source: @image-url("../assets/icons/monitor-speaker.svg");
|
|
width: 16px;
|
|
height: 16px;
|
|
tint: Theme.text-primary;
|
|
}
|
|
Text {
|
|
text: @tr("Qobuz Connect");
|
|
color: Theme.text-primary;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
vertical-alignment: center;
|
|
horizontal-stretch: 1;
|
|
}
|
|
}
|
|
// Status row.
|
|
Rectangle {
|
|
height: 1px;
|
|
background: Theme.border-subtle;
|
|
}
|
|
HorizontalLayout {
|
|
spacing: 8px;
|
|
VerticalLayout {
|
|
alignment: center;
|
|
Rectangle {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 4px;
|
|
background: NowPlayingState.qconnect-connected ? #22c55e : Theme.text-muted;
|
|
}
|
|
}
|
|
Text {
|
|
text: NowPlayingState.qconnect-connected
|
|
? @tr("Connected")
|
|
: @tr("Disconnected");
|
|
color: Theme.text-secondary;
|
|
font-size: 12px;
|
|
vertical-alignment: center;
|
|
horizontal-stretch: 1;
|
|
}
|
|
}
|
|
// Device list / empty state.
|
|
Rectangle {
|
|
vertical-stretch: 1;
|
|
background: Theme.surface-elevated;
|
|
border-radius: Radius.sm;
|
|
VerticalLayout {
|
|
alignment: center;
|
|
spacing: 4px;
|
|
Text {
|
|
text: @tr("No devices found");
|
|
color: Theme.text-secondary;
|
|
font-size: 12px;
|
|
horizontal-alignment: center;
|
|
}
|
|
Text {
|
|
text: @tr("Make sure your renderer is on the same network");
|
|
color: Theme.text-muted;
|
|
font-size: 10px;
|
|
horizontal-alignment: center;
|
|
wrap: word-wrap;
|
|
}
|
|
}
|
|
}
|
|
// Connect / Disconnect toggle.
|
|
Rectangle {
|
|
height: 34px;
|
|
border-radius: Radius.sm;
|
|
background: connect-ta.pressed
|
|
? Theme.accent-pressed
|
|
: connect-ta.has-hover ? Theme.accent-hover : Theme.accent;
|
|
connect-ta := TouchArea {
|
|
clicked => {
|
|
NowPlayingState.qconnect-toggle();
|
|
qconnect-menu.close();
|
|
}
|
|
}
|
|
Text {
|
|
text: NowPlayingState.qconnect-connected
|
|
? @tr("Disconnect")
|
|
: @tr("Connect");
|
|
color: Theme.accent-text;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
horizontal-alignment: center;
|
|
vertical-alignment: center;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cast (Chromecast / DLNA) — opens the cast flyout.
|
|
VerticalLayout {
|
|
alignment: center;
|
|
Rectangle {
|
|
width: 28px;
|
|
height: 32px;
|
|
IconButton {
|
|
icon: @image-url("../assets/icons/cast.svg");
|
|
icon-size: 16px;
|
|
btn-width: 28px;
|
|
tooltip: @tr("Cast");
|
|
active: NowPlayingState.cast-active;
|
|
clicked => {
|
|
cast-menu.show();
|
|
}
|
|
}
|
|
cast-menu := PopupWindow {
|
|
x: 32px - 280px;
|
|
y: -252px;
|
|
width: 280px;
|
|
height: 244px;
|
|
close-policy: close-on-click-outside;
|
|
Rectangle {
|
|
background: Theme.surface-main;
|
|
border-radius: Radius.sm;
|
|
border-width: 1px;
|
|
border-color: Theme.border-muted;
|
|
drop-shadow-blur: 22px;
|
|
drop-shadow-color: Theme.card-shadow;
|
|
drop-shadow-offset-y: 6px;
|
|
VerticalLayout {
|
|
padding: 14px;
|
|
spacing: 12px;
|
|
HorizontalLayout {
|
|
spacing: 8px;
|
|
QbzIcon {
|
|
source: @image-url("../assets/icons/cast.svg");
|
|
width: 16px;
|
|
height: 16px;
|
|
tint: Theme.text-primary;
|
|
}
|
|
Text {
|
|
text: @tr("Cast");
|
|
color: Theme.text-primary;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
vertical-alignment: center;
|
|
horizontal-stretch: 1;
|
|
}
|
|
}
|
|
Rectangle {
|
|
height: 1px;
|
|
background: Theme.border-subtle;
|
|
}
|
|
HorizontalLayout {
|
|
spacing: 8px;
|
|
VerticalLayout {
|
|
alignment: center;
|
|
Rectangle {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 4px;
|
|
background: NowPlayingState.cast-active ? #a855f7 : Theme.text-muted;
|
|
}
|
|
}
|
|
Text {
|
|
text: NowPlayingState.cast-active
|
|
? @tr("Casting")
|
|
: @tr("Not casting");
|
|
color: Theme.text-secondary;
|
|
font-size: 12px;
|
|
vertical-alignment: center;
|
|
horizontal-stretch: 1;
|
|
}
|
|
}
|
|
Rectangle {
|
|
vertical-stretch: 1;
|
|
background: Theme.surface-elevated;
|
|
border-radius: Radius.sm;
|
|
VerticalLayout {
|
|
alignment: center;
|
|
spacing: 4px;
|
|
Text {
|
|
text: @tr("No cast devices found");
|
|
color: Theme.text-secondary;
|
|
font-size: 12px;
|
|
horizontal-alignment: center;
|
|
}
|
|
Text {
|
|
text: @tr("Looking for Chromecast & DLNA on your network");
|
|
color: Theme.text-muted;
|
|
font-size: 10px;
|
|
horizontal-alignment: center;
|
|
wrap: word-wrap;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Audio settings — normalization + gapless.
|
|
VerticalLayout {
|
|
alignment: center;
|
|
Rectangle {
|
|
width: 28px;
|
|
height: 32px;
|
|
IconButton {
|
|
icon: @image-url("../assets/icons/settings-2.svg");
|
|
icon-size: 16px;
|
|
btn-width: 28px;
|
|
tooltip: @tr("Audio settings");
|
|
active: SettingsState.normalization;
|
|
clicked => {
|
|
audio-menu.show();
|
|
}
|
|
}
|
|
audio-menu := PopupWindow {
|
|
x: 32px - 252px;
|
|
y: -126px;
|
|
width: 252px;
|
|
height: 118px;
|
|
close-policy: close-on-click-outside;
|
|
ContextMenu {
|
|
ToggleRow {
|
|
label: @tr("Normalization");
|
|
hint: @tr("Level loudness across tracks");
|
|
checked: SettingsState.normalization;
|
|
toggled(v) => {
|
|
root.settings-bool("normalization", v);
|
|
}
|
|
}
|
|
ToggleRow {
|
|
label: @tr("Gapless");
|
|
hint: @tr("Seamless track transitions");
|
|
checked: SettingsState.gapless;
|
|
toggled(v) => {
|
|
root.settings-bool("gapless", v);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Now-Playing view — unified switch for the bar layouts
|
|
// (Default / Small / Large) + the window modes (Miniplayer /
|
|
// Immersive). All are "presentation modes" of now-playing, so
|
|
// one menu. Small/Large/window modes are disabled until those
|
|
// layouts/features land.
|
|
VerticalLayout {
|
|
alignment: center;
|
|
Rectangle {
|
|
width: 28px;
|
|
height: 32px;
|
|
IconButton {
|
|
icon: @image-url("../assets/icons/layout-grid.svg");
|
|
icon-size: 16px;
|
|
btn-width: 28px;
|
|
tooltip: @tr("Now Playing view");
|
|
clicked => {
|
|
layout-menu.show();
|
|
}
|
|
}
|
|
layout-menu := PopupWindow {
|
|
x: 32px - 196px;
|
|
y: -242px;
|
|
width: 196px;
|
|
height: 234px;
|
|
close-policy: close-on-click;
|
|
ContextMenu {
|
|
ContextMenuItem {
|
|
icon: @image-url("../assets/icons/panel-left.svg");
|
|
label: @tr("New");
|
|
selected: ShellState.npb-mode == 0;
|
|
clicked => {
|
|
root.media-action("npb-view", "", "new");
|
|
}
|
|
}
|
|
ContextMenuItem {
|
|
icon: @image-url("../assets/icons/panel-right-close.svg");
|
|
label: @tr("Classic");
|
|
selected: ShellState.npb-mode == 1;
|
|
clicked => {
|
|
root.media-action("npb-view", "", "classic");
|
|
}
|
|
}
|
|
ContextMenuItem {
|
|
icon: @image-url("../assets/icons/rows-3.svg");
|
|
label: @tr("Small");
|
|
enabled: false;
|
|
clicked => {
|
|
root.media-action("npb-view", "", "small");
|
|
}
|
|
}
|
|
ContextMenuItem {
|
|
icon: @image-url("../assets/icons/layout-grid.svg");
|
|
label: @tr("Large");
|
|
enabled: false;
|
|
clicked => {
|
|
root.media-action("npb-view", "", "large");
|
|
}
|
|
}
|
|
Rectangle {
|
|
height: 7px;
|
|
Rectangle {
|
|
height: 1px;
|
|
y: 3px;
|
|
background: Theme.border-subtle;
|
|
}
|
|
}
|
|
ContextMenuItem {
|
|
icon: @image-url("../assets/icons/picture-in-picture-2.svg");
|
|
label: @tr("Miniplayer");
|
|
enabled: false;
|
|
clicked => {
|
|
root.media-action("npb-view", "", "miniplayer");
|
|
}
|
|
}
|
|
ContextMenuItem {
|
|
icon: @image-url("../assets/icons/maximize-2.svg");
|
|
label: @tr("Immersive");
|
|
enabled: false;
|
|
clicked => {
|
|
root.media-action("npb-view", "", "immersive");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Lyrics.
|
|
VerticalLayout {
|
|
alignment: center;
|
|
IconButton {
|
|
icon: @image-url("../assets/icons/mic-vocal.svg");
|
|
icon-size: 16px;
|
|
btn-width: 28px;
|
|
tooltip: @tr("Lyrics");
|
|
active: ShellState.right-panel == 2;
|
|
clicked => {
|
|
ShellState.right-panel = ShellState.right-panel == 2 ? 0 : 2;
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle { width: 6px; }
|
|
|
|
// Volume — disabled while a remote renderer locks it.
|
|
VerticalLayout {
|
|
alignment: center;
|
|
IconButton {
|
|
icon: NowPlayingState.muted
|
|
? @image-url("../assets/icons/volume-x.svg")
|
|
: @image-url("../assets/icons/volume-2.svg");
|
|
icon-size: 16px;
|
|
btn-width: 28px;
|
|
tooltip: NowPlayingState.muted ? @tr("Unmute") : @tr("Mute");
|
|
active: NowPlayingState.muted;
|
|
clicked => {
|
|
if (!NowPlayingState.volume-locked) {
|
|
NowPlayingState.toggle-mute();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
VerticalLayout {
|
|
alignment: center;
|
|
opacity: NowPlayingState.volume-locked ? 0.4 : 1.0;
|
|
QbzSlider {
|
|
width: 90px;
|
|
minimum: 0;
|
|
maximum: 100;
|
|
value: NowPlayingState.volume * 100;
|
|
changed(v) => {
|
|
if (!NowPlayingState.volume-locked) {
|
|
NowPlayingState.set-volume(v / 100.0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if AppearanceState.show-volume-steppers: VerticalLayout {
|
|
alignment: center;
|
|
IconButton {
|
|
icon: @image-url("../assets/icons/minus.svg");
|
|
icon-size: 15px;
|
|
btn-width: 28px;
|
|
tooltip: @tr("Volume down");
|
|
clicked => {
|
|
if (!NowPlayingState.volume-locked) {
|
|
NowPlayingState.set-volume(Math.max(0.0, NowPlayingState.volume - 0.05));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if AppearanceState.show-volume-steppers: VerticalLayout {
|
|
alignment: center;
|
|
IconButton {
|
|
icon: @image-url("../assets/icons/plus.svg");
|
|
icon-size: 15px;
|
|
btn-width: 28px;
|
|
tooltip: @tr("Volume up");
|
|
clicked => {
|
|
if (!NowPlayingState.volume-locked) {
|
|
NowPlayingState.set-volume(Math.min(1.0, NowPlayingState.volume + 0.05));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle { width: 6px; }
|
|
|
|
// Queue panel toggle.
|
|
VerticalLayout {
|
|
alignment: center;
|
|
IconButton {
|
|
icon: @image-url("../assets/icons/list-ordered.svg");
|
|
icon-size: 16px;
|
|
btn-width: 28px;
|
|
tooltip: @tr("Queue");
|
|
active: ShellState.right-panel == 1;
|
|
clicked => {
|
|
ShellState.right-panel = ShellState.right-panel == 1 ? 0 : 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Rectangle { height: 6px; }
|
|
}
|
|
}
|