add wavyline slider to dash media widget

This commit is contained in:
2026-08-17 21:01:04 +02:00
parent db76ae7dc6
commit 98fc9aa97a
2 changed files with 145 additions and 149 deletions
+5 -2
View File
@@ -13,11 +13,13 @@ Slider {
property bool animateWave property bool animateWave
property color bgColor: enabled ? Colors.palette.m3secondaryContainer : Qt.alpha(Colors.palette.m3onSurface, 0.1) property color bgColor: enabled ? Colors.palette.m3secondaryContainer : Qt.alpha(Colors.palette.m3onSurface, 0.1)
readonly property bool dragging: mouse.pressed
property color fgColor: enabled ? Colors.palette.m3primary : Qt.alpha(Colors.palette.m3onSurface, 0.38) property color fgColor: enabled ? Colors.palette.m3primary : Qt.alpha(Colors.palette.m3onSurface, 0.38)
property real filledWidth property real filledWidth
property alias inset: inset property alias inset: inset
property color insetColor: enabled ? (inset.attached ? Colors.palette.m3onSecondaryContainer : Colors.palette.m3onPrimary) : Colors.palette.m3onSurface property color insetColor: enabled ? (inset.attached ? Colors.palette.m3onSecondaryContainer : Colors.palette.m3onPrimary) : Colors.palette.m3onSurface
property string insetIcon: "" property string insetIcon: ""
property bool interactionOnMove: true
property real pos: visualPosition property real pos: visualPosition
property int radius: Tokens.rounding.small property int radius: Tokens.rounding.small
property int waveDuration: 1000 property int waveDuration: 1000
@@ -139,7 +141,7 @@ Slider {
CAnim { CAnim {
} }
} }
Anim on waveProgress { NumberAnimation on waveProgress {
duration: root.waveDuration duration: root.waveDuration
easing.type: Easing.Linear easing.type: Easing.Linear
from: 0 from: 0
@@ -186,7 +188,8 @@ Slider {
onPositionChanged: e => { onPositionChanged: e => {
dragMovement = (e.x - pressStartX) / width; dragMovement = (e.x - pressStartX) / width;
root.interaction(posBinding.value); if (root.interactionOnMove)
root.interaction(posBinding.value);
} }
onPressed: e => { onPressed: e => {
widthBehavior.enabled = false; widthBehavior.enabled = false;
+140 -147
View File
@@ -1,10 +1,12 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import Quickshell.Services.Mpris
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Shapes import QtQuick.Shapes
import QtQuick.Effects import QtQuick.Effects
import ZShell.Components
import ZShell.Services import ZShell.Services
import ZShell.Config import ZShell.Config
import qs.Daemons import qs.Daemons
@@ -14,7 +16,7 @@ import qs.Services
Item { Item {
id: root id: root
readonly property bool hasUnknownLength: (Players.active?.length ?? 0) > 2147483647
property real playerProgress: { property real playerProgress: {
const active = Players.active; const active = Players.active;
return active?.length ? (active.position % active.length) / active.length : 0; return active?.length ? (active.position % active.length) / active.length : 0;
@@ -23,7 +25,19 @@ Item {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
implicitHeight: cover.height + rowHeight * 2 implicitHeight: layout.implicitHeight
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 { Behavior on playerProgress {
Anim { Anim {
@@ -32,11 +46,10 @@ Item {
} }
Timer { Timer {
interval: Config.dashboard.mediaUpdateInterval
repeat: true
running: Players.active?.isPlaying ?? false running: Players.active?.isPlaying ?? false
interval: Config.dashboard.mediaUpdateInterval
triggeredOnStart: true triggeredOnStart: true
repeat: true
onTriggered: Players.active?.positionChanged() onTriggered: Players.active?.positionChanged()
} }
@@ -72,7 +85,7 @@ Item {
const n = values.length; const n = values.length;
if (n < 2) if (n < 2)
return ""; return "";
const h = layout.height + shape.strokeWidth / 2; const h = layout.height + shape.strokeWidth / 2;
const w = layout.width + shape.strokeWidth; const w = layout.width + shape.strokeWidth;
@@ -126,58 +139,11 @@ Item {
} }
} }
Shape {
preferredRendererType: Shape.CurveRenderer
ShapePath {
capStyle: Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap
fillColor: "transparent"
strokeColor: Colors.layer(Colors.palette.m3surfaceContainerHigh, 2)
strokeWidth: Config.dashboard.sizes.mediaProgressThickness
Behavior on strokeColor {
CAnim {
}
}
PathAngleArc {
centerX: cover.x + cover.width / 2
centerY: cover.y + cover.height / 2
radiusX: (cover.width + Config.dashboard.sizes.mediaProgressThickness) / 2 + Tokens.spacing.small
radiusY: (cover.height + Config.dashboard.sizes.mediaProgressThickness) / 2 + Tokens.spacing.small
startAngle: -90 - Config.dashboard.sizes.mediaProgressSweep / 2
sweepAngle: Config.dashboard.sizes.mediaProgressSweep
}
}
ShapePath {
capStyle: Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap
fillColor: "transparent"
strokeColor: Colors.palette.m3primary
strokeWidth: Config.dashboard.sizes.mediaProgressThickness
Behavior on strokeColor {
CAnim {
}
}
PathAngleArc {
centerX: cover.x + cover.width / 2
centerY: cover.y + cover.height / 2
radiusX: (cover.width + Config.dashboard.sizes.mediaProgressThickness) / 2 + Tokens.spacing.small
radiusY: (cover.height + Config.dashboard.sizes.mediaProgressThickness) / 2 + Tokens.spacing.small
startAngle: -90 - Config.dashboard.sizes.mediaProgressSweep / 2
sweepAngle: Config.dashboard.sizes.mediaProgressSweep * root.playerProgress
}
}
}
RowLayout { RowLayout {
id: layout id: layout
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
implicitHeight: root.implicitHeight
CustomClippingRect { CustomClippingRect {
id: cover id: cover
@@ -211,147 +177,174 @@ Item {
} }
} }
CustomRect { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: Tokens.spacing.large
Layout.rightMargin: Tokens.spacing.large
Layout.preferredHeight: childrenRect.height Layout.preferredHeight: childrenRect.height
spacing: Tokens.spacing.normal
MarqueeText { MarqueeText {
id: title id: title
anchors.horizontalCenter: parent.horizontalCenter Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
anchors.top: parent.top
color: Colors.palette.m3primary color: Colors.palette.m3primary
font.pointSize: Tokens.font.size.normal font.pointSize: Tokens.font.size.normal
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
pauseMs: 4000 pauseMs: 4000
text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title") text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title")
width: parent.width - Tokens.padding.large * 4 implicitWidth: parent.width - Tokens.padding.large * 4
} }
MarqueeText { MarqueeText {
id: album id: album
anchors.horizontalCenter: parent.horizontalCenter Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
anchors.top: title.bottom
anchors.topMargin: Tokens.spacing.small
color: Colors.palette.m3outline color: Colors.palette.m3outline
font.pointSize: Tokens.font.size.small font.pointSize: Tokens.font.size.small
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
pauseMs: 4000 pauseMs: 4000
text: (Players.active?.trackAlbum ?? qsTr("No media")) || qsTr("Unknown album") text: (Players.active?.trackAlbum ?? qsTr("No media")) || qsTr("Unknown album")
width: parent.width - Tokens.padding.large * 4 implicitWidth: parent.width - Tokens.padding.large * 4
} }
MarqueeText { MarqueeText {
id: artist id: artist
anchors.horizontalCenter: parent.horizontalCenter Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
anchors.top: album.bottom
anchors.topMargin: Tokens.spacing.small
color: Colors.palette.m3secondary color: Colors.palette.m3secondary
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
pauseMs: 4000 pauseMs: 4000
text: (Players.active?.trackArtist ?? qsTr("No media")) || qsTr("Unknown artist") text: (Players.active?.trackArtist ?? qsTr("No media")) || qsTr("Unknown artist")
width: parent.width - Tokens.padding.large * 4 implicitWidth: parent.width - Tokens.padding.large * 4
} }
RowLayout { RowLayout {
id: controls id: positionSliderLayout
Layout.topMargin: Tokens.spacing.extraLargeIncreased
anchors.horizontalCenter: parent.horizontalCenter Layout.fillWidth: true
anchors.top: artist.bottom
anchors.topMargin: Tokens.spacing.smaller
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
Control { TextMetrics {
function onClicked(): void { id: timeMetrics
Players.active?.previous();
}
canUse: Players.active?.canGoPrevious ?? false 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
implicitWidth: Math.round(implicitHeight * 0.9)
}
IconButton {
id: previousBtn
type: IconButton.Tonal
icon: "skip_previous" icon: "skip_previous"
isRound: true
shapeMorph: true
enabled: Players.active?.canGoPrevious
onClicked: Players.active?.previous()
} }
Control { IconButton {
function onClicked(): void { id: playPauseBtn
Players.active?.togglePlaying();
}
canUse: Players.active?.canTogglePlaying ?? false
icon: Players.active?.isPlaying ? "pause" : "play_arrow" 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()
} }
Control { IconButton {
function onClicked(): void { id: nextBtn
Players.active?.next();
}
canUse: Players.active?.canGoNext ?? false type: IconButton.Tonal
icon: "skip_next" icon: "skip_next"
isRound: true
shapeMorph: true
enabled: Players.active?.canGoNext
onClicked: Players.active?.next()
}
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;
}
implicitWidth: Math.round(implicitHeight * 0.9)
} }
} }
} }
} }
component Control: CustomRect {
id: control
required property bool canUse
required property string icon
property int level: 1
property string set_color: "Secondary"
function onClicked(): void {
}
Layout.preferredWidth: implicitWidth + (controlState.pressed ? Tokens.padding.normal * 2 : 0)
color: canUse ? Colors.palette[`m3${set_color.toLowerCase()}`] : Colors.palette[`m3${set_color.toLowerCase()}Container`]
implicitHeight: implicitWidth
implicitWidth: Math.max(icon.implicitHeight, icon.implicitHeight) + Tokens.padding.small
radius: Tokens.rounding.full
Behavior on Layout.preferredWidth {
Anim {
duration: Tokens.anim.durations.expressiveFastSpatial
easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial
}
}
Behavior on radius {
Anim {
duration: Tokens.anim.durations.expressiveFastSpatial
easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial
}
}
Elevation {
anchors.fill: parent
level: controlState.containsMouse && !controlState.pressed ? control.level + 1 : control.level
radius: parent.radius
z: -1
}
StateLayer {
id: controlState
color: control.canUse ? Colors.palette[`m3on${control.set_color}`] : Colors.palette[`m3on${control.set_color}Container`]
enabled: control.canUse
onClicked: {
control.onClicked();
}
// radius: Tokens.rounding.full
}
MaterialIcon {
id: icon
anchors.centerIn: parent
anchors.verticalCenterOffset: font.pointSize * 0.05
animate: true
color: control.canUse ? Colors.palette[`m3on${control.set_color}`] : Colors.palette[`m3on${control.set_color}Container`]
fill: control.canUse ? 1 : 0
font.pointSize: Tokens.font.size.large
text: control.icon
}
}
} }