import QtQuick import Quickshell.Services.Mpris import qs.Common import qs.Services import qs.Widgets Item { id: root property MprisPlayer activePlayer property real value: { if (!activePlayer || activePlayer.length <= 0) return 0 const pos = (activePlayer.position || 0) % Math.max(1, activePlayer.length) const calculatedRatio = pos / activePlayer.length return Math.max(0, Math.min(1, calculatedRatio)) } property bool isSeeking: false implicitHeight: 20 Loader { anchors.fill: parent visible: activePlayer && activePlayer.length > 0 sourceComponent: SettingsData.waveProgressEnabled ? waveProgressComponent : flatProgressComponent z: 1 Component { id: waveProgressComponent M3WaveProgress { value: root.value isPlaying: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing MouseArea { anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor enabled: activePlayer && activePlayer.canSeek && activePlayer.length > 0 property real pendingSeekPosition: -1 Timer { id: waveSeekDebounceTimer interval: 150 onTriggered: { if (parent.pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) { const clamped = Math.min(parent.pendingSeekPosition, activePlayer.length * 0.99) activePlayer.position = clamped parent.pendingSeekPosition = -1 } } } onPressed: (mouse) => { root.isSeeking = true if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) { const r = Math.max(0, Math.min(1, mouse.x / parent.width)) pendingSeekPosition = r * activePlayer.length waveSeekDebounceTimer.restart() } } onReleased: { root.isSeeking = false waveSeekDebounceTimer.stop() if (pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) { const clamped = Math.min(pendingSeekPosition, activePlayer.length * 0.99) activePlayer.position = clamped pendingSeekPosition = -1 } } onPositionChanged: (mouse) => { if (pressed && root.isSeeking && activePlayer && activePlayer.length > 0 && activePlayer.canSeek) { const r = Math.max(0, Math.min(1, mouse.x / parent.width)) pendingSeekPosition = r * activePlayer.length waveSeekDebounceTimer.restart() } } onClicked: (mouse) => { if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) { const r = Math.max(0, Math.min(1, mouse.x / parent.width)) activePlayer.position = r * activePlayer.length } } } } } Component { id: flatProgressComponent Item { property real lineWidth: 3 property color trackColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.40) property color fillColor: Theme.primary property color playheadColor: Theme.primary readonly property real midY: height / 2 Rectangle { width: parent.width height: parent.lineWidth anchors.verticalCenter: parent.verticalCenter color: parent.trackColor radius: height / 2 } Rectangle { width: Math.max(0, Math.min(parent.width, parent.width * root.value)) height: parent.lineWidth anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter color: parent.fillColor radius: height / 2 Behavior on width { NumberAnimation { duration: 80 } } } Rectangle { id: playhead width: 3 height: Math.max(parent.lineWidth + 8, 14) radius: width / 2 color: parent.playheadColor x: Math.max(0, Math.min(parent.width, parent.width * root.value)) - width / 2 y: parent.midY - height / 2 z: 3 Behavior on x { NumberAnimation { duration: 80 } } } MouseArea { anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor enabled: activePlayer && activePlayer.canSeek && activePlayer.length > 0 property real pendingSeekPosition: -1 Timer { id: flatSeekDebounceTimer interval: 150 onTriggered: { if (parent.pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) { const clamped = Math.min(parent.pendingSeekPosition, activePlayer.length * 0.99) activePlayer.position = clamped parent.pendingSeekPosition = -1 } } } onPressed: (mouse) => { root.isSeeking = true if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) { const r = Math.max(0, Math.min(1, mouse.x / parent.width)) pendingSeekPosition = r * activePlayer.length flatSeekDebounceTimer.restart() } } onReleased: { root.isSeeking = false flatSeekDebounceTimer.stop() if (pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) { const clamped = Math.min(pendingSeekPosition, activePlayer.length * 0.99) activePlayer.position = clamped pendingSeekPosition = -1 } } onPositionChanged: (mouse) => { if (pressed && root.isSeeking && activePlayer && activePlayer.length > 0 && activePlayer.canSeek) { const r = Math.max(0, Math.min(1, mouse.x / parent.width)) pendingSeekPosition = r * activePlayer.length flatSeekDebounceTimer.restart() } } onClicked: (mouse) => { if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) { const r = Math.max(0, Math.min(1, mouse.x / parent.width)) activePlayer.position = r * activePlayer.length } } } } } } }