add wavyline slider to dash media widget
This commit is contained in:
@@ -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
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user