C++ / fmt (pull_request) Successful in 4s
JS/TS / fmt (pull_request) Successful in 20s
JS/TS / lint (pull_request) Successful in 22s
Python / static (pull_request) Successful in 57s
Rust / fmt (pull_request) Successful in 1m2s
C++ / build (pull_request) Successful in 2m10s
Rust / build (pull_request) Successful in 1m50s
Rust / clippy (pull_request) Successful in 1m25s
Python / verify (pull_request) Successful in 2m58s
C++ / clang-tidy (pull_request) Successful in 7m8s
330 lines
8.4 KiB
QML
330 lines
8.4 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import Quickshell
|
|
import Quickshell.Services.Mpris
|
|
import QtQuick
|
|
import QtQuick.Layouts
|
|
import QtQuick.Shapes
|
|
import QtQuick.Effects
|
|
import ZShell.Components
|
|
import ZShell.Services
|
|
import ZShell.Config
|
|
import qs.Modules.Dashboard.Dash.Media
|
|
import qs.Daemons
|
|
import qs.Components
|
|
import qs.Helpers
|
|
import qs.Services
|
|
|
|
Item {
|
|
id: root
|
|
readonly property bool hasUnknownLength: (Players.active?.length ?? 0) > 2147483647
|
|
property real playerProgress: {
|
|
const active = Players.active;
|
|
return active?.length ? (active.position % active.length) / active.length : 0;
|
|
}
|
|
property int rowHeight: Tokens.padding.large + Config.dashboard.sizes.mediaProgressThickness + Tokens.spacing.small
|
|
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
|
|
function lengthStr(length: int): string {
|
|
if (length < 0)
|
|
return "-1:-1";
|
|
|
|
const hours = Math.floor(length / 3600);
|
|
const mins = Math.floor((length % 3600) / 60);
|
|
const secs = Math.floor(length % 60).toString().padStart(2, "0");
|
|
|
|
if (hours > 0)
|
|
return `${hours}:${mins.toString().padStart(2, "0")}:${secs}`;
|
|
return `${mins}:${secs}`;
|
|
}
|
|
|
|
Behavior on playerProgress {
|
|
Anim {
|
|
duration: Tokens.anim.durations.large
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
running: Players.active?.isPlaying ?? false
|
|
interval: Config.dashboard.mediaUpdateInterval
|
|
triggeredOnStart: true
|
|
repeat: true
|
|
onTriggered: Players.active?.positionChanged()
|
|
}
|
|
|
|
ServiceRef {
|
|
service: Audio.cava
|
|
}
|
|
|
|
BackgroundShapes {
|
|
anchors.fill: parent
|
|
}
|
|
|
|
Shape {
|
|
id: visualizer
|
|
|
|
readonly property int bars: Config.services.visualizerBars
|
|
property color color: Qt.alpha(Colors.palette.m3primary, 0.5)
|
|
property color fillColor: Qt.alpha(color, 0.1)
|
|
|
|
anchors.fill: parent
|
|
anchors.leftMargin: -(shape.strokeWidth / 2)
|
|
layer.enabled: true
|
|
asynchronous: true
|
|
preferredRendererType: Shape.CurveRenderer
|
|
|
|
ShapePath {
|
|
id: shape
|
|
strokeColor: visualizer.color
|
|
fillColor: visualizer.fillColor
|
|
strokeWidth: 2
|
|
|
|
PathSvg {
|
|
path: shape.curvePath
|
|
}
|
|
|
|
property string curvePath: {
|
|
const values = Audio.cava.values;
|
|
const n = values.length;
|
|
|
|
if (n < 2)
|
|
return "";
|
|
|
|
const h = root.height + shape.strokeWidth / 2;
|
|
const w = root.width + shape.strokeWidth;
|
|
|
|
function x(i) {
|
|
return i * w / (n - 1);
|
|
}
|
|
|
|
function y(i) {
|
|
return h - (values[i] * Config.dashboard.sizes.mediaVisualizerSize) / 2;
|
|
}
|
|
|
|
let d = `M 0 ${h} `;
|
|
d += `L ${x(0)} ${y(0)} `;
|
|
|
|
for (let i = 0; i < n - 1; ++i) {
|
|
const x0 = x(i);
|
|
const y0 = y(i);
|
|
|
|
const x1 = x(i + 1);
|
|
const y1 = y(i + 1);
|
|
|
|
const prev = Math.max(0, i - 1);
|
|
const next = Math.min(n - 1, i + 2);
|
|
|
|
const c1x = x0 + (x1 - x(prev)) / 6;
|
|
const c1y = y0 + (y1 - y(prev)) / 6;
|
|
|
|
const c2x = x1 - (x(next) - x0) / 6;
|
|
const c2y = y1 - (y(next) - y0) / 6;
|
|
|
|
d += `C ${c1x} ${c1y}, ${c2x} ${c2y}, ${x1} ${y1} `;
|
|
}
|
|
|
|
d += `L ${w} ${h} `;
|
|
d += `L 0 ${h} Z`;
|
|
|
|
return d;
|
|
}
|
|
}
|
|
}
|
|
|
|
CustomRect {
|
|
anchors.fill: visualizer
|
|
|
|
color: Qt.alpha(Colors.palette.m3shadow, 0.2)
|
|
layer.enabled: true
|
|
layer.effect: MultiEffect {
|
|
maskEnabled: true
|
|
maskSource: visualizer
|
|
}
|
|
}
|
|
|
|
RowLayout {
|
|
id: layout
|
|
|
|
anchors.fill: parent
|
|
anchors.margins: Tokens.padding.large
|
|
anchors.leftMargin: Tokens.padding.extraLarge
|
|
|
|
CoverVisualizer {
|
|
Layout.fillHeight: true
|
|
implicitWidth: Config.dashboard.sizes.mediaCoverArtSize
|
|
}
|
|
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
Layout.leftMargin: Tokens.spacing.largeIncreased
|
|
Layout.rightMargin: Tokens.spacing.largeIncreased
|
|
|
|
spacing: Tokens.spacing.small
|
|
|
|
MarqueeText {
|
|
id: title
|
|
|
|
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
|
color: Colors.palette.m3primary
|
|
font.pointSize: Tokens.font.size.normal
|
|
horizontalAlignment: Text.AlignHCenter
|
|
pauseMs: 4000
|
|
text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title")
|
|
implicitWidth: parent.width - Tokens.padding.large * 4
|
|
}
|
|
|
|
MarqueeText {
|
|
id: album
|
|
|
|
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
|
color: Colors.palette.m3outline
|
|
font.pointSize: Tokens.font.size.small
|
|
horizontalAlignment: Text.AlignHCenter
|
|
pauseMs: 4000
|
|
text: (Players.active?.trackAlbum ?? qsTr("No media")) || qsTr("Unknown album")
|
|
implicitWidth: parent.width - Tokens.padding.large * 4
|
|
}
|
|
|
|
MarqueeText {
|
|
id: artist
|
|
|
|
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
|
color: Colors.palette.m3secondary
|
|
horizontalAlignment: Text.AlignHCenter
|
|
pauseMs: 4000
|
|
text: (Players.active?.trackArtist ?? qsTr("No media")) || qsTr("Unknown artist")
|
|
implicitWidth: parent.width - Tokens.padding.large * 4
|
|
}
|
|
|
|
RowLayout {
|
|
id: positionSliderLayout
|
|
Layout.topMargin: Tokens.spacing.large
|
|
Layout.fillWidth: true
|
|
spacing: Tokens.spacing.small
|
|
|
|
TextMetrics {
|
|
id: timeMetrics
|
|
|
|
text: Players.active ? root.lengthStr(Math.max(Players.active.position, root.hasUnknownLength ? 0 : Players.active.length)).replace(/[1-9]/g, "0") : "00:00"
|
|
}
|
|
|
|
CustomText {
|
|
id: positionLabel
|
|
|
|
Layout.preferredWidth: timeMetrics.width
|
|
text: root.lengthStr(Players.active?.position ?? -1)
|
|
color: Colors.palette.m3onSurfaceVariant
|
|
font: timeMetrics.font
|
|
horizontalAlignment: Text.AlignHCenter
|
|
}
|
|
|
|
CustomSlider {
|
|
id: positionSlider
|
|
|
|
Layout.fillWidth: true
|
|
value: Players.active ? Players.active.position / (Players.active.length || 1) : 0
|
|
enabled: (Players.active?.canSeek ?? false) && !root.hasUnknownLength
|
|
wavy: true
|
|
animateWave: Players.active?.isPlaying ?? false
|
|
waveFrequency: 5
|
|
waveDuration: 2000
|
|
interactionOnMove: false
|
|
onInteraction: value => {
|
|
const active = Players.active;
|
|
if (active?.canSeek && active?.positionSupported)
|
|
active.position = value * active.length;
|
|
}
|
|
Binding {
|
|
target: positionLabel
|
|
property: "text"
|
|
value: root.lengthStr(positionSlider.pos * (Players.active?.length ?? 0))
|
|
when: positionSlider.dragging
|
|
}
|
|
}
|
|
|
|
CustomText {
|
|
Layout.preferredWidth: timeMetrics.width
|
|
text: root.hasUnknownLength ? "--:--" : root.lengthStr(Players.active?.length ?? -1)
|
|
color: Colors.palette.m3onSurfaceVariant
|
|
font: timeMetrics.font
|
|
horizontalAlignment: Text.AlignHCenter
|
|
}
|
|
}
|
|
ButtonRow {
|
|
Layout.topMargin: Tokens.spacing.largeIncreased
|
|
Layout.fillWidth: true
|
|
spacing: Tokens.spacing.extraSmall
|
|
Layout.leftMargin: positionSliderLayout.implicitWidth / 4
|
|
Layout.rightMargin: positionSliderLayout.implicitWidth / 4
|
|
|
|
IconButton {
|
|
type: IconButton.Tonal
|
|
icon: "shuffle"
|
|
isRound: true
|
|
shapeMorph: true
|
|
checked: Players.active?.shuffle ?? false
|
|
enabled: Players.active?.shuffleSupported
|
|
onClicked: Players.active.shuffle = !Players.active?.shuffle
|
|
}
|
|
|
|
IconButton {
|
|
id: previousBtn
|
|
|
|
type: IconButton.Tonal
|
|
icon: "skip_previous"
|
|
isRound: true
|
|
shapeMorph: true
|
|
enabled: Players.active?.canGoPrevious
|
|
onClicked: Players.active?.previous()
|
|
font.pointSize: Tokens.font.size.large
|
|
}
|
|
|
|
IconButton {
|
|
id: playPauseBtn
|
|
|
|
icon: Players.active?.isPlaying ? "pause" : "play_arrow"
|
|
isRound: true
|
|
shapeMorph: true
|
|
fillWidth: true
|
|
checked: Players.active?.isPlaying ?? false
|
|
enabled: Players.active?.canTogglePlaying
|
|
onClicked: Players.active?.togglePlaying()
|
|
font.pointSize: Tokens.font.size.large
|
|
}
|
|
|
|
IconButton {
|
|
id: nextBtn
|
|
|
|
type: IconButton.Tonal
|
|
icon: "skip_next"
|
|
isRound: true
|
|
shapeMorph: true
|
|
enabled: Players.active?.canGoNext
|
|
onClicked: Players.active?.next()
|
|
font.pointSize: Tokens.font.size.large
|
|
}
|
|
|
|
IconButton {
|
|
type: IconButton.Tonal
|
|
icon: Players.active?.loopState === MprisLoopState.Track ? "repeat_one" : "repeat"
|
|
isRound: true
|
|
shapeMorph: true
|
|
checked: Players.active?.loopState === MprisLoopState.Track || Players.active?.loopState === MprisLoopState.Playlist
|
|
enabled: Players.active?.loopSupported
|
|
onClicked: {
|
|
const state = Players.active.loopState;
|
|
if (state === MprisLoopState.None)
|
|
Players.active.loopState = MprisLoopState.Track;
|
|
else if (state === MprisLoopState.Track)
|
|
Players.active.loopState = MprisLoopState.Playlist;
|
|
else
|
|
Players.active.loopState = MprisLoopState.None;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|