diff --git a/Modules/SidePanel/Cards/MediaCard.qml b/Modules/SidePanel/Cards/MediaCard.qml index bdf8c7c..abc2bbe 100644 --- a/Modules/SidePanel/Cards/MediaCard.qml +++ b/Modules/SidePanel/Cards/MediaCard.qml @@ -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 - } - return Math.min(1, MediaService.currentPosition / MediaService.trackLength) + const r = 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 { - id: progressFill - width: progressBarBackground.progressRatio * parent.width - height: parent.height - radius: parent.radius - color: Color.mPrimary - - Behavior on width { - NumberAnimation { - duration: Style.animationFast + // 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 + } } } } - // 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 + } } // ------------------------- diff --git a/Services/MediaService.qml b/Services/MediaService.qml index 558dc93..a37e85b 100644 --- a/Services/MediaService.qml +++ b/Services/MediaService.qml @@ -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) { diff --git a/Services/ScreenRecorderService.qml b/Services/ScreenRecorderService.qml index ad4f323..08d6503 100644 --- a/Services/ScreenRecorderService.qml +++ b/Services/ScreenRecorderService.qml @@ -1,4 +1,5 @@ pragma Singleton + import QtQuick import Quickshell import Quickshell.Io @@ -46,9 +47,9 @@ 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 pendingTimer.running = true } @@ -59,8 +60,9 @@ 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 pendingTimer.running = false @@ -73,7 +75,7 @@ Singleton { // Process to run and monitor gpu-screen-recorder Process { id: recorderProcess - onExited: function(exitCode, exitStatus) { + onExited: function (exitCode, exitStatus) { if (isPending) { // Process ended while we were pending - likely cancelled or error isPending = false @@ -88,7 +90,7 @@ Singleton { Timer { 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 repeat: false onTriggered: { @@ -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"]) } } -} \ No newline at end of file +}