From 28c37c08d2bc24dac474dd37def2a668dd2dbb7d Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sun, 28 Sep 2025 10:45:16 +0200 Subject: [PATCH] make media control seekable (closes #1615) --- .../widgets/LightDarkPreferenceButton.qml | 4 +- .../common/widgets/StyledProgressBar.qml | 95 +++++++++---------- .../modules/common/widgets/StyledSlider.qml | 65 ++++++++++--- .../ii/modules/common/widgets/WavyLine.qml | 34 +++++++ .../modules/mediaControls/PlayerControl.qml | 40 ++++++-- .../volumeMixer/VolumeMixerEntry.qml | 2 +- 6 files changed, 166 insertions(+), 74 deletions(-) create mode 100644 .config/quickshell/ii/modules/common/widgets/WavyLine.qml diff --git a/.config/quickshell/ii/modules/common/widgets/LightDarkPreferenceButton.qml b/.config/quickshell/ii/modules/common/widgets/LightDarkPreferenceButton.qml index 5ad1316b9..efef4c1af 100644 --- a/.config/quickshell/ii/modules/common/widgets/LightDarkPreferenceButton.qml +++ b/.config/quickshell/ii/modules/common/widgets/LightDarkPreferenceButton.qml @@ -72,8 +72,8 @@ RippleButton { Layout.bottomMargin: 5 Layout.fillWidth: true value: 0.7 - sperm: true - animateSperm: lightDarkButtonRoot.toggled + wavy: true + animateWave: lightDarkButtonRoot.toggled highlightColor: lightDarkButtonRoot.toggled ? Appearance.m3colors.m3primary : lightDarkButtonRoot.previewFg trackColor: ColorUtils.mix(lightDarkButtonRoot.previewBg, lightDarkButtonRoot.previewFg, 0.5) } diff --git a/.config/quickshell/ii/modules/common/widgets/StyledProgressBar.qml b/.config/quickshell/ii/modules/common/widgets/StyledProgressBar.qml index 8197b29f9..62b8dcf74 100644 --- a/.config/quickshell/ii/modules/common/widgets/StyledProgressBar.qml +++ b/.config/quickshell/ii/modules/common/widgets/StyledProgressBar.qml @@ -1,12 +1,9 @@ -import qs.services +pragma ComponentBehavior: Bound import qs.modules.common import qs.modules.common.widgets import QtQuick import QtQuick.Controls -import QtQuick.Layouts -import Quickshell -import Quickshell.Widgets -import Qt5Compat.GraphicalEffects + /** * Material 3 progress bar. See https://m3.material.io/components/progress-indicators/overview @@ -18,13 +15,13 @@ ProgressBar { property real valueBarGap: 4 property color highlightColor: Appearance?.colors.colPrimary ?? "#685496" property color trackColor: Appearance?.m3colors.m3secondaryContainer ?? "#F1D3F9" - property bool sperm: false // If true, the progress bar will have a wavy fill effect - property bool animateSperm: true - property real spermAmplitudeMultiplier: sperm ? 0.5 : 0 - property real spermFrequency: 6 - property real spermFps: 60 + property bool wavy: false // If true, the progress bar will have a wavy fill effect + property bool animateWave: true + property real waveAmplitudeMultiplier: wavy ? 0.5 : 0 + property real waveFrequency: 6 + property real waveFps: 60 - Behavior on spermAmplitudeMultiplier { + Behavior on waveAmplitudeMultiplier { animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this) } @@ -38,64 +35,62 @@ ProgressBar { } contentItem: Item { + id: contentItem anchors.fill: parent - Canvas { - id: wavyFill + Loader { anchors { left: parent.left - right: parent.right verticalCenter: parent.verticalCenter } - height: parent.height * 6 - onPaint: { - var ctx = getContext("2d"); - ctx.clearRect(0, 0, width, height); - - var progress = root.visualPosition; - var fillWidth = progress * width; - var amplitude = parent.height * root.spermAmplitudeMultiplier; - var frequency = root.spermFrequency; - var phase = Date.now() / 400.0; - var centerY = height / 2; - - ctx.strokeStyle = root.highlightColor; - ctx.lineWidth = parent.height; - ctx.lineCap = "round"; - ctx.beginPath(); - for (var x = ctx.lineWidth / 2; x <= fillWidth; x += 1) { - var waveY = centerY + amplitude * Math.sin(frequency * 2 * Math.PI * x / width + phase); - if (x === 0) - ctx.moveTo(x, waveY); - else - ctx.lineTo(x, waveY); + active: root.wavy + sourceComponent: WavyLine { + id: wavyFill + frequency: root.waveFrequency + color: root.highlightColor + amplitudeMultiplier: root.wavy ? 0.5 : 0 + height: contentItem.height * 6 + width: contentItem.width * root.visualPosition + lineWidth: contentItem.height + fullLength: root.width + Connections { + target: root + function onValueChanged() { wavyFill.requestPaint(); } + function onHighlightColorChanged() { wavyFill.requestPaint(); } + } + FrameAnimation { + running: root.animateWave + onTriggered: { + wavyFill.requestPaint() + } } - ctx.stroke(); - } - Connections { - target: root - function onValueChanged() { wavyFill.requestPaint(); } - function onHighlightColorChanged() { wavyFill.requestPaint(); } - } - Timer { - interval: 1000 / root.spermFps - running: root.animateSperm - repeat: root.sperm - onTriggered: wavyFill.requestPaint() } } + + Loader { + active: !root.wavy + sourceComponent: Rectangle { + anchors.left: parent.left + width: contentItem.width * root.visualPosition + height: contentItem.height + radius: height / 2 + color: root.highlightColor + } + } + Rectangle { // Right remaining part fill anchors.right: parent.right width: (1 - root.visualPosition) * parent.width - valueBarGap height: parent.height - radius: Appearance?.rounding.full ?? 9999 + radius: height / 2 color: root.trackColor } + Rectangle { // Stop point anchors.right: parent.right width: valueBarGap height: valueBarGap - radius: Appearance?.rounding.full ?? 9999 + radius: height / 2 color: root.highlightColor } } diff --git a/.config/quickshell/ii/modules/common/widgets/StyledSlider.qml b/.config/quickshell/ii/modules/common/widgets/StyledSlider.qml index 6f2fc2314..99e7b8d6b 100644 --- a/.config/quickshell/ii/modules/common/widgets/StyledSlider.qml +++ b/.config/quickshell/ii/modules/common/widgets/StyledSlider.qml @@ -1,3 +1,4 @@ +pragma ComponentBehavior: Bound import qs.modules.common import qs.modules.common.widgets import qs.services @@ -17,6 +18,7 @@ Slider { property list stopIndicatorValues: [1] enum Configuration { + Wavy = 4, XS = 12, S = 18, M = 30, @@ -28,10 +30,9 @@ Slider { property real handleDefaultWidth: 3 property real handlePressedWidth: 1.5 - property color highlightColor: Appearance.colors.colPrimary property color trackColor: Appearance.colors.colSecondaryContainer - property color handleColor: Appearance.m3colors.m3onSecondaryContainer + property color handleColor: Appearance.colors.colPrimary property color dotColor: Appearance.m3colors.m3onSecondaryContainer property color dotColorHighlighted: Appearance.m3colors.m3onPrimary property real unsharpenRadius: Appearance.rounding.unsharpen @@ -39,15 +40,18 @@ Slider { property real trackRadius: trackWidth >= StyledSlider.Configuration.XL ? 21 : trackWidth >= StyledSlider.Configuration.L ? 12 : trackWidth >= StyledSlider.Configuration.M ? 9 - : 6 - property real handleHeight: Math.max(33, trackWidth + 9) + : trackWidth >= StyledSlider.Configuration.S ? 6 + : height / 2 + property real handleHeight: (configuration === StyledSlider.Configuration.Wavy) ? 24 : Math.max(33, trackWidth + 9) property real handleWidth: root.pressed ? handlePressedWidth : handleDefaultWidth property real handleMargins: 4 - onHandleMarginsChanged: { - console.log("Handle margins changed to", handleMargins); - } property real trackDotSize: 3 property string tooltipContent: `${Math.round(value * 100)}%` + property bool wavy: configuration === StyledSlider.Configuration.Wavy // If true, the progress bar will have a wavy fill effect + property bool animateWave: true + property real waveAmplitudeMultiplier: wavy ? 0.5 : 0 + property real waveFrequency: 6 + property real waveFps: 60 leftPadding: handleMargins rightPadding: handleMargins @@ -93,18 +97,51 @@ Slider { implicitHeight: trackWidth // Fill left - Rectangle { + Loader { anchors { verticalCenter: parent.verticalCenter left: parent.left } width: root.handleMargins + (root.visualPosition * root.effectiveDraggingWidth) - (root.handleWidth / 2 + root.handleMargins) - height: trackWidth - color: root.highlightColor - topLeftRadius: root.trackRadius - bottomLeftRadius: root.trackRadius - topRightRadius: root.unsharpenRadius - bottomRightRadius: root.unsharpenRadius + height: root.trackWidth + active: !root.wavy + sourceComponent: Rectangle { + color: root.highlightColor + topLeftRadius: root.trackRadius + bottomLeftRadius: root.trackRadius + topRightRadius: root.unsharpenRadius + bottomRightRadius: root.unsharpenRadius + } + } + + Loader { + anchors { + verticalCenter: parent.verticalCenter + left: parent.left + } + width: root.handleMargins + (root.visualPosition * root.effectiveDraggingWidth) - (root.handleWidth / 2 + root.handleMargins) + height: root.height + active: root.wavy + sourceComponent: WavyLine { + id: wavyFill + frequency: root.waveFrequency + fullLength: root.width + color: root.highlightColor + amplitudeMultiplier: root.wavy ? 0.5 : 0 + width: root.handleMargins + (root.visualPosition * root.effectiveDraggingWidth) - (root.handleWidth / 2 + root.handleMargins) + height: root.trackWidth + Connections { + target: root + function onValueChanged() { wavyFill.requestPaint(); } + function onHighlightColorChanged() { wavyFill.requestPaint(); } + } + FrameAnimation { + running: root.animateWave + onTriggered: { + wavyFill.requestPaint() + } + } + } } // Fill right diff --git a/.config/quickshell/ii/modules/common/widgets/WavyLine.qml b/.config/quickshell/ii/modules/common/widgets/WavyLine.qml new file mode 100644 index 000000000..7c3ff528a --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/WavyLine.qml @@ -0,0 +1,34 @@ +import qs.modules.common +import QtQuick + +Canvas { + id: root + property real amplitudeMultiplier: 0.5 + property real frequency: 6 + property color color: Appearance?.colors.colPrimary ?? "#685496" + property real lineWidth: 4 + property real fullLength: width + + onPaint: { + var ctx = getContext("2d"); + ctx.clearRect(0, 0, width, height); + + var amplitude = root.lineWidth * root.amplitudeMultiplier; + var frequency = root.frequency; + var phase = Date.now() / 400.0; + var centerY = height / 2; + + ctx.strokeStyle = root.color; + ctx.lineWidth = root.lineWidth; + ctx.lineCap = "round"; + ctx.beginPath(); + for (var x = ctx.lineWidth / 2; x <= root.width - ctx.lineWidth / 2; x += 1) { + var waveY = centerY + amplitude * Math.sin(frequency * 2 * Math.PI * x / root.fullLength + phase); + if (x === 0) + ctx.moveTo(x, waveY); + else + ctx.lineTo(x, waveY); + } + ctx.stroke(); + } +} diff --git a/.config/quickshell/ii/modules/mediaControls/PlayerControl.qml b/.config/quickshell/ii/modules/mediaControls/PlayerControl.qml index 5be82f09e..8cef6a861 100644 --- a/.config/quickshell/ii/modules/mediaControls/PlayerControl.qml +++ b/.config/quickshell/ii/modules/mediaControls/PlayerControl.qml @@ -1,3 +1,4 @@ +pragma ComponentBehavior: Bound import qs.modules.common import qs.modules.common.models import qs.modules.common.widgets @@ -233,16 +234,41 @@ Item { // Player instance Item { id: progressBarContainer Layout.fillWidth: true - implicitHeight: progressBar.implicitHeight + implicitHeight: Math.max(sliderLoader.implicitHeight, progressBarLoader.implicitHeight) - StyledProgressBar { - id: progressBar + Loader { + id: sliderLoader anchors.fill: parent - highlightColor: blendedColors.colPrimary - trackColor: blendedColors.colSecondaryContainer - value: playerController.player?.position / playerController.player?.length - sperm: playerController.player?.isPlaying + active: playerController.player?.canSeek ?? false + sourceComponent: StyledSlider { + configuration: StyledSlider.Configuration.Wavy + highlightColor: blendedColors.colPrimary + trackColor: blendedColors.colSecondaryContainer + handleColor: blendedColors.colPrimary + value: playerController.player?.position / playerController.player?.length + onMoved: { + playerController.player.position = value * playerController.player.length; + } + } } + + Loader { + id: progressBarLoader + anchors { + verticalCenter: parent.verticalCenter + left: parent.left + right: parent.right + } + active: !(playerController.player?.canSeek ?? false) + sourceComponent: StyledProgressBar { + wavy: playerController.player?.isPlaying + highlightColor: blendedColors.colPrimary + trackColor: blendedColors.colSecondaryContainer + value: playerController.player?.position / playerController.player?.length + } + } + + } TrackChangeButton { iconName: "skip_next" diff --git a/.config/quickshell/ii/modules/sidebarRight/volumeMixer/VolumeMixerEntry.qml b/.config/quickshell/ii/modules/sidebarRight/volumeMixer/VolumeMixerEntry.qml index 5ee398015..2f605fafe 100644 --- a/.config/quickshell/ii/modules/sidebarRight/volumeMixer/VolumeMixerEntry.qml +++ b/.config/quickshell/ii/modules/sidebarRight/volumeMixer/VolumeMixerEntry.qml @@ -56,7 +56,7 @@ Item { StyledSlider { id: slider value: root.node.audio.volume - onValueChanged: root.node.audio.volume = value + onMoved: root.node.audio.volume = value } } }