Possible fix for MediaCard slider

MediaCard: use proper seek binding
MediaService: add seek binding
autoformat
This commit is contained in:
Ly-sec 2025-08-27 14:21:42 +02:00
parent 6f7528c87a
commit 563a151277
3 changed files with 92 additions and 71 deletions

View file

@ -7,19 +7,12 @@ import qs.Commons
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
// Media player area (placeholder until MediaPlayer service is wired)
NBox { NBox {
id: root id: root
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
// Let content dictate the height (no hardcoded height here)
// Height can be overridden by parent layout (SidePanel binds it to stats card)
//implicitHeight: content.implicitHeight + Style.marginL * 2 * scaling
// Component.onCompleted: {
// Logger.logMediaService.trackArtUrl)
// }
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
Layout.fillHeight: true Layout.fillHeight: true
@ -223,78 +216,86 @@ NBox {
} }
// ------------------------- // -------------------------
// Progress bar // Progress slider (uses shared NSlider behavior like BarTab)
Rectangle { Item {
id: progressBarBackground id: progressWrapper
visible: (MediaService.currentPlayer && MediaService.trackLength > 0) visible: (MediaService.currentPlayer && MediaService.trackLength > 0)
width: parent.width
height: 4 * scaling
radius: Style.radiusS * scaling
color: Color.mSurface
Layout.fillWidth: true Layout.fillWidth: true
height: Math.max(Style.baseWidgetSize * 0.5 * scaling, 12 * scaling)
// Local preview while dragging
property real localSeekRatio: -1
// Track the last ratio we actually sent to the backend to avoid redundant seeks
property real lastSentSeekRatio: -1
// Minimum change required to issue a new seek during drag
property real seekEpsilon: 0.01
property real progressRatio: { property real progressRatio: {
if (!MediaService.currentPlayer || !MediaService.isPlaying || MediaService.trackLength <= 0) { if (!MediaService.currentPlayer || MediaService.trackLength <= 0)
return 0 return 0
} const r = MediaService.currentPosition / MediaService.trackLength
return Math.min(1, MediaService.currentPosition / MediaService.trackLength) if (isNaN(r) || !isFinite(r))
return 0
return Math.max(0, Math.min(1, r))
} }
property real effectiveRatio: (MediaService.isSeeking
&& localSeekRatio >= 0) ? Math.max(0, Math.min(1,
localSeekRatio)) : progressRatio
Rectangle { // Debounced backend seek during drag
id: progressFill Timer {
width: progressBarBackground.progressRatio * parent.width id: seekDebounce
height: parent.height interval: 75
radius: parent.radius repeat: false
color: Color.mPrimary onTriggered: {
if (MediaService.isSeeking && progressWrapper.localSeekRatio >= 0) {
Behavior on width { const next = Math.max(0, Math.min(1, progressWrapper.localSeekRatio))
NumberAnimation { if (progressWrapper.lastSentSeekRatio < 0 || Math.abs(
duration: Style.animationFast next - progressWrapper.lastSentSeekRatio) >= progressWrapper.seekEpsilon) {
MediaService.seekByRatio(next)
progressWrapper.lastSentSeekRatio = next
}
} }
} }
} }
// Interactive progress handle NSlider {
Rectangle { id: progressSlider
id: progressHandle
visible: (MediaService.currentPlayer && MediaService.trackLength > 0)
width: 16 * scaling
height: 16 * scaling
radius: width * 0.5
color: Color.mPrimary
border.color: Color.mOutline
border.width: Math.max(1 * Style.borderM * scaling)
x: Math.max(0, Math.min(parent.width - width, progressFill.width - width / 2))
anchors.verticalCenter: parent.verticalCenter
scale: progressMouseArea.containsMouse || progressMouseArea.pressed ? 1.2 : 1.0
Behavior on scale {
NumberAnimation {
duration: Style.animationFast
}
}
}
// Mouse area for seeking
MouseArea {
id: progressMouseArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true from: 0
cursorShape: Qt.PointingHandCursor to: 1
stepSize: 0
snapAlways: false
enabled: MediaService.trackLength > 0 && MediaService.canSeek enabled: MediaService.trackLength > 0 && MediaService.canSeek
cutoutColor: Color.mSurface
onClicked: function (mouse) { onMoved: {
let ratio = mouse.x / width progressWrapper.localSeekRatio = value
MediaService.seekByRatio(ratio) seekDebounce.restart()
} }
onPressedChanged: {
onPositionChanged: function (mouse) {
if (pressed) { if (pressed) {
let ratio = Math.max(0, Math.min(1, mouse.x / width)) MediaService.isSeeking = true
MediaService.seekByRatio(ratio) progressWrapper.localSeekRatio = value
MediaService.seekByRatio(value)
progressWrapper.lastSentSeekRatio = value
} else {
seekDebounce.stop()
MediaService.seekByRatio(value)
MediaService.isSeeking = false
progressWrapper.localSeekRatio = -1
progressWrapper.lastSentSeekRatio = -1
} }
} }
} }
// While not dragging, bind slider to live progress
// during drag, let the slider manage its own value
Binding {
target: progressSlider
property: "value"
value: progressWrapper.progressRatio
when: !MediaService.isSeeking
}
} }
// ------------------------- // -------------------------

View file

@ -11,6 +11,7 @@ Singleton {
property var currentPlayer: null property var currentPlayer: null
property real currentPosition: 0 property real currentPosition: 0
property bool isSeeking: false
property int selectedPlayerIndex: 0 property int selectedPlayerIndex: 0
property bool isPlaying: currentPlayer ? (currentPlayer.playbackState === MprisPlaybackState.Playing property bool isPlaying: currentPlayer ? (currentPlayer.playbackState === MprisPlaybackState.Playing
|| currentPlayer.isPlaying) : false || currentPlayer.isPlaying) : false
@ -158,11 +159,12 @@ Singleton {
Timer { Timer {
id: positionTimer id: positionTimer
interval: 1000 interval: 1000
running: currentPlayer && currentPlayer.isPlaying && currentPlayer.length > 0 running: currentPlayer && !root.isSeeking && currentPlayer.isPlaying && currentPlayer.length > 0
&& currentPlayer.playbackState === MprisPlaybackState.Playing && currentPlayer.playbackState === MprisPlaybackState.Playing
repeat: true repeat: true
onTriggered: { onTriggered: {
if (currentPlayer && currentPlayer.isPlaying && currentPlayer.playbackState === MprisPlaybackState.Playing) { if (currentPlayer && !root.isSeeking && currentPlayer.isPlaying
&& currentPlayer.playbackState === MprisPlaybackState.Playing) {
currentPosition = currentPlayer.position currentPosition = currentPlayer.position
} else { } else {
running = false running = false
@ -170,6 +172,21 @@ Singleton {
} }
} }
// Avoid overwriting currentPosition while seeking due to backend position changes
Connections {
target: currentPlayer
function onPositionChanged() {
if (!root.isSeeking && currentPlayer) {
currentPosition = currentPlayer.position
}
}
function onPlaybackStateChanged() {
if (!root.isSeeking && currentPlayer) {
currentPosition = currentPlayer.position
}
}
}
// Reset position when switching to inactive player // Reset position when switching to inactive player
onCurrentPlayerChanged: { onCurrentPlayerChanged: {
if (!currentPlayer || !currentPlayer.isPlaying || currentPlayer.playbackState !== MprisPlaybackState.Playing) { if (!currentPlayer || !currentPlayer.isPlaying || currentPlayer.playbackState !== MprisPlaybackState.Playing) {

View file

@ -1,4 +1,5 @@
pragma Singleton pragma Singleton
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
@ -46,9 +47,9 @@ Singleton {
// Use Process instead of execDetached so we can monitor it // Use Process instead of execDetached so we can monitor it
recorderProcess.exec({ recorderProcess.exec({
command: ["sh", "-c", command] "command": ["sh", "-c", command]
}) })
// Start monitoring - if process ends quickly, it was likely cancelled // Start monitoring - if process ends quickly, it was likely cancelled
pendingTimer.running = true pendingTimer.running = true
} }
@ -59,8 +60,9 @@ Singleton {
return return
} }
Quickshell.execDetached(["sh", "-c", "pkill -SIGINT -f 'gpu-screen-recorder' || pkill -SIGINT -f 'com.dec05eba.gpu_screen_recorder'"]) Quickshell.execDetached(
["sh", "-c", "pkill -SIGINT -f 'gpu-screen-recorder' || pkill -SIGINT -f 'com.dec05eba.gpu_screen_recorder'"])
isRecording = false isRecording = false
isPending = false isPending = false
pendingTimer.running = false pendingTimer.running = false
@ -73,7 +75,7 @@ Singleton {
// Process to run and monitor gpu-screen-recorder // Process to run and monitor gpu-screen-recorder
Process { Process {
id: recorderProcess id: recorderProcess
onExited: function(exitCode, exitStatus) { onExited: function (exitCode, exitStatus) {
if (isPending) { if (isPending) {
// Process ended while we were pending - likely cancelled or error // Process ended while we were pending - likely cancelled or error
isPending = false isPending = false
@ -88,7 +90,7 @@ Singleton {
Timer { Timer {
id: pendingTimer id: pendingTimer
interval: 2000 // Wait 2 seconds to see if process stays alive interval: 2000 // Wait 2 seconds to see if process stays alive
running: false running: false
repeat: false repeat: false
onTriggered: { onTriggered: {
@ -124,7 +126,8 @@ Singleton {
running: false running: false
repeat: false repeat: false
onTriggered: { onTriggered: {
Quickshell.execDetached(["sh", "-c", "pkill -9 -f 'gpu-screen-recorder' 2>/dev/null || pkill -9 -f 'com.dec05eba.gpu_screen_recorder' 2>/dev/null || true"]) Quickshell.execDetached(
["sh", "-c", "pkill -9 -f 'gpu-screen-recorder' 2>/dev/null || pkill -9 -f 'com.dec05eba.gpu_screen_recorder' 2>/dev/null || true"])
} }
} }
} }