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.Widgets
// Media player area (placeholder until MediaPlayer service is wired)
NBox {
id: root
Layout.fillWidth: 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 {
anchors.fill: parent
Layout.fillHeight: true
@ -223,78 +216,86 @@ NBox {
}
// -------------------------
// Progress bar
Rectangle {
id: progressBarBackground
// Progress slider (uses shared NSlider behavior like BarTab)
Item {
id: progressWrapper
visible: (MediaService.currentPlayer && MediaService.trackLength > 0)
width: parent.width
height: 4 * scaling
radius: Style.radiusS * scaling
color: Color.mSurface
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: {
if (!MediaService.currentPlayer || !MediaService.isPlaying || MediaService.trackLength <= 0) {
if (!MediaService.currentPlayer || MediaService.trackLength <= 0)
return 0
const r = MediaService.currentPosition / MediaService.trackLength
if (isNaN(r) || !isFinite(r))
return 0
return Math.max(0, Math.min(1, r))
}
return Math.min(1, MediaService.currentPosition / MediaService.trackLength)
property real effectiveRatio: (MediaService.isSeeking
&& localSeekRatio >= 0) ? Math.max(0, Math.min(1,
localSeekRatio)) : progressRatio
// Debounced backend seek during drag
Timer {
id: seekDebounce
interval: 75
repeat: false
onTriggered: {
if (MediaService.isSeeking && progressWrapper.localSeekRatio >= 0) {
const next = Math.max(0, Math.min(1, progressWrapper.localSeekRatio))
if (progressWrapper.lastSentSeekRatio < 0 || Math.abs(
next - progressWrapper.lastSentSeekRatio) >= progressWrapper.seekEpsilon) {
MediaService.seekByRatio(next)
progressWrapper.lastSentSeekRatio = next
}
Rectangle {
id: progressFill
width: progressBarBackground.progressRatio * parent.width
height: parent.height
radius: parent.radius
color: Color.mPrimary
Behavior on width {
NumberAnimation {
duration: Style.animationFast
}
}
}
// Interactive progress handle
Rectangle {
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
NSlider {
id: progressSlider
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
from: 0
to: 1
stepSize: 0
snapAlways: false
enabled: MediaService.trackLength > 0 && MediaService.canSeek
cutoutColor: Color.mSurface
onClicked: function (mouse) {
let ratio = mouse.x / width
MediaService.seekByRatio(ratio)
onMoved: {
progressWrapper.localSeekRatio = value
seekDebounce.restart()
}
onPositionChanged: function (mouse) {
onPressedChanged: {
if (pressed) {
let ratio = Math.max(0, Math.min(1, mouse.x / width))
MediaService.seekByRatio(ratio)
MediaService.isSeeking = true
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 real currentPosition: 0
property bool isSeeking: false
property int selectedPlayerIndex: 0
property bool isPlaying: currentPlayer ? (currentPlayer.playbackState === MprisPlaybackState.Playing
|| currentPlayer.isPlaying) : false
@ -158,11 +159,12 @@ Singleton {
Timer {
id: positionTimer
interval: 1000
running: currentPlayer && currentPlayer.isPlaying && currentPlayer.length > 0
running: currentPlayer && !root.isSeeking && currentPlayer.isPlaying && currentPlayer.length > 0
&& currentPlayer.playbackState === MprisPlaybackState.Playing
repeat: true
onTriggered: {
if (currentPlayer && currentPlayer.isPlaying && currentPlayer.playbackState === MprisPlaybackState.Playing) {
if (currentPlayer && !root.isSeeking && currentPlayer.isPlaying
&& currentPlayer.playbackState === MprisPlaybackState.Playing) {
currentPosition = currentPlayer.position
} else {
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
onCurrentPlayerChanged: {
if (!currentPlayer || !currentPlayer.isPlaying || currentPlayer.playbackState !== MprisPlaybackState.Playing) {

View file

@ -1,4 +1,5 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
@ -46,7 +47,7 @@ Singleton {
// Use Process instead of execDetached so we can monitor it
recorderProcess.exec({
command: ["sh", "-c", command]
"command": ["sh", "-c", command]
})
// Start monitoring - if process ends quickly, it was likely cancelled
@ -59,7 +60,8 @@ Singleton {
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
isPending = false
@ -124,7 +126,8 @@ Singleton {
running: false
repeat: false
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"])
}
}
}