Possible fix for MediaCard slider
MediaCard: use proper seek binding MediaService: add seek binding autoformat
This commit is contained in:
parent
6f7528c87a
commit
563a151277
3 changed files with 92 additions and 71 deletions
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------
|
// -------------------------
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue