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.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
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue