mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-04-12 16:52:10 -04:00
Improve seek and scrub indicator/ animations in the media controls widget. (#2181)
This commit is contained in:
@@ -8,13 +8,122 @@ Item {
|
||||
id: root
|
||||
|
||||
property MprisPlayer activePlayer
|
||||
property real value: {
|
||||
if (!activePlayer || activePlayer.length <= 0) return 0
|
||||
const pos = (activePlayer.position || 0) % Math.max(1, activePlayer.length)
|
||||
const calculatedRatio = pos / activePlayer.length
|
||||
return Math.max(0, Math.min(1, calculatedRatio))
|
||||
property real seekPreviewRatio: -1
|
||||
readonly property real playerValue: {
|
||||
if (!activePlayer || activePlayer.length <= 0)
|
||||
return 0;
|
||||
const pos = (activePlayer.position || 0) % Math.max(1, activePlayer.length);
|
||||
const calculatedRatio = pos / activePlayer.length;
|
||||
return Math.max(0, Math.min(1, calculatedRatio));
|
||||
}
|
||||
property real value: seekPreviewRatio >= 0 ? seekPreviewRatio : playerValue
|
||||
property bool isSeeking: false
|
||||
property bool isDraggingSeek: false
|
||||
property real committedSeekRatio: -1
|
||||
property int previewSettleChecksRemaining: 0
|
||||
property real dragThreshold: 4
|
||||
property int holdIndicatorDelay: 180
|
||||
|
||||
function clampRatio(ratio) {
|
||||
return Math.max(0, Math.min(1, ratio));
|
||||
}
|
||||
|
||||
function ratioForPosition(position) {
|
||||
if (!activePlayer || activePlayer.length <= 0)
|
||||
return 0;
|
||||
return clampRatio(position / activePlayer.length);
|
||||
}
|
||||
|
||||
function positionForRatio(ratio) {
|
||||
if (!activePlayer || activePlayer.length <= 0)
|
||||
return 0;
|
||||
const rawPosition = clampRatio(ratio) * activePlayer.length;
|
||||
return Math.min(rawPosition, activePlayer.length * 0.99);
|
||||
}
|
||||
|
||||
function updatePreviewFromMouse(mouseX, width) {
|
||||
if (!activePlayer || activePlayer.length <= 0 || width <= 0)
|
||||
return;
|
||||
seekPreviewRatio = clampRatio(mouseX / width);
|
||||
}
|
||||
|
||||
function clearCommittedSeekPreview() {
|
||||
previewSettleTimer.stop();
|
||||
committedSeekRatio = -1;
|
||||
previewSettleChecksRemaining = 0;
|
||||
if (!isSeeking)
|
||||
seekPreviewRatio = -1;
|
||||
}
|
||||
|
||||
function beginCommittedSeekPreview(position) {
|
||||
seekPreviewRatio = ratioForPosition(position);
|
||||
committedSeekRatio = seekPreviewRatio;
|
||||
previewSettleChecksRemaining = 15;
|
||||
previewSettleTimer.restart();
|
||||
}
|
||||
|
||||
function handleSeekPressed(mouse, width, mouseArea, holdTimer) {
|
||||
isSeeking = true;
|
||||
isDraggingSeek = false;
|
||||
mouseArea.pressX = mouse.x;
|
||||
clearCommittedSeekPreview();
|
||||
holdTimer.restart();
|
||||
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
|
||||
updatePreviewFromMouse(mouse.x, width);
|
||||
mouseArea.pendingSeekPosition = positionForRatio(seekPreviewRatio);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSeekReleased(mouseArea, holdTimer) {
|
||||
holdTimer.stop();
|
||||
isSeeking = false;
|
||||
isDraggingSeek = false;
|
||||
if (mouseArea.pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
|
||||
const clamped = Math.min(mouseArea.pendingSeekPosition, activePlayer.length * 0.99);
|
||||
activePlayer.position = clamped;
|
||||
mouseArea.pendingSeekPosition = -1;
|
||||
beginCommittedSeekPreview(clamped);
|
||||
} else {
|
||||
seekPreviewRatio = -1;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSeekPositionChanged(mouse, width, mouseArea) {
|
||||
if (mouseArea.pressed && isSeeking && activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
|
||||
if (!isDraggingSeek && Math.abs(mouse.x - mouseArea.pressX) >= dragThreshold)
|
||||
isDraggingSeek = true;
|
||||
updatePreviewFromMouse(mouse.x, width);
|
||||
mouseArea.pendingSeekPosition = positionForRatio(seekPreviewRatio);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSeekCanceled(mouseArea, holdTimer) {
|
||||
holdTimer.stop();
|
||||
isSeeking = false;
|
||||
isDraggingSeek = false;
|
||||
mouseArea.pendingSeekPosition = -1;
|
||||
clearCommittedSeekPreview();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: previewSettleTimer
|
||||
interval: 80
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
if (root.isSeeking || root.committedSeekRatio < 0) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
|
||||
const previewSettled = Math.abs(root.playerValue - root.committedSeekRatio) <= 0.0015;
|
||||
if (previewSettled || root.previewSettleChecksRemaining <= 0) {
|
||||
root.clearCommittedSeekPreview();
|
||||
return;
|
||||
}
|
||||
|
||||
root.previewSettleChecksRemaining -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: 20
|
||||
|
||||
@@ -29,58 +138,35 @@ Item {
|
||||
|
||||
M3WaveProgress {
|
||||
value: root.value
|
||||
actualValue: root.playerValue
|
||||
showActualPlaybackState: root.isSeeking
|
||||
actualProgressColor: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.45)
|
||||
isPlaying: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing
|
||||
|
||||
MouseArea {
|
||||
id: waveMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: activePlayer && activePlayer.canSeek && activePlayer.length > 0
|
||||
|
||||
property real pendingSeekPosition: -1
|
||||
property real pressX: 0
|
||||
|
||||
Timer {
|
||||
id: waveSeekDebounceTimer
|
||||
interval: 150
|
||||
id: waveHoldIndicatorTimer
|
||||
interval: root.holdIndicatorDelay
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (parent.pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
|
||||
const clamped = Math.min(parent.pendingSeekPosition, activePlayer.length * 0.99)
|
||||
activePlayer.position = clamped
|
||||
parent.pendingSeekPosition = -1
|
||||
}
|
||||
if (parent.pressed && root.isSeeking)
|
||||
root.isDraggingSeek = true;
|
||||
}
|
||||
}
|
||||
|
||||
onPressed: (mouse) => {
|
||||
root.isSeeking = true
|
||||
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
|
||||
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
|
||||
pendingSeekPosition = r * activePlayer.length
|
||||
waveSeekDebounceTimer.restart()
|
||||
}
|
||||
}
|
||||
onReleased: {
|
||||
root.isSeeking = false
|
||||
waveSeekDebounceTimer.stop()
|
||||
if (pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
|
||||
const clamped = Math.min(pendingSeekPosition, activePlayer.length * 0.99)
|
||||
activePlayer.position = clamped
|
||||
pendingSeekPosition = -1
|
||||
}
|
||||
}
|
||||
onPositionChanged: (mouse) => {
|
||||
if (pressed && root.isSeeking && activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
|
||||
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
|
||||
pendingSeekPosition = r * activePlayer.length
|
||||
waveSeekDebounceTimer.restart()
|
||||
}
|
||||
}
|
||||
onClicked: (mouse) => {
|
||||
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
|
||||
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
|
||||
activePlayer.position = r * activePlayer.length
|
||||
}
|
||||
}
|
||||
onPressed: mouse => root.handleSeekPressed(mouse, parent.width, waveMouseArea, waveHoldIndicatorTimer)
|
||||
onReleased: root.handleSeekReleased(waveMouseArea, waveHoldIndicatorTimer)
|
||||
onPositionChanged: mouse => root.handleSeekPositionChanged(mouse, parent.width, waveMouseArea)
|
||||
onCanceled: root.handleSeekCanceled(waveMouseArea, waveHoldIndicatorTimer)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,6 +179,7 @@ Item {
|
||||
property color trackColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.40)
|
||||
property color fillColor: Theme.primary
|
||||
property color playheadColor: Theme.primary
|
||||
property color actualProgressColor: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.45)
|
||||
readonly property real midY: height / 2
|
||||
|
||||
Rectangle {
|
||||
@@ -110,7 +197,22 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: parent.fillColor
|
||||
radius: height / 2
|
||||
Behavior on width { NumberAnimation { duration: 80 } }
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: 80
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: root.isDraggingSeek
|
||||
width: 2
|
||||
height: Math.max(parent.lineWidth + 4, 10)
|
||||
radius: width / 2
|
||||
color: parent.actualProgressColor
|
||||
x: Math.max(0, Math.min(parent.width, parent.width * root.playerValue)) - width / 2
|
||||
y: parent.midY - height / 2
|
||||
z: 2
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
@@ -122,59 +224,37 @@ Item {
|
||||
x: Math.max(0, Math.min(parent.width, parent.width * root.value)) - width / 2
|
||||
y: parent.midY - height / 2
|
||||
z: 3
|
||||
Behavior on x { NumberAnimation { duration: 80 } }
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: 80
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: flatMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: activePlayer && activePlayer.canSeek && activePlayer.length > 0
|
||||
|
||||
property real pendingSeekPosition: -1
|
||||
property real pressX: 0
|
||||
|
||||
Timer {
|
||||
id: flatSeekDebounceTimer
|
||||
interval: 150
|
||||
id: flatHoldIndicatorTimer
|
||||
interval: root.holdIndicatorDelay
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (parent.pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
|
||||
const clamped = Math.min(parent.pendingSeekPosition, activePlayer.length * 0.99)
|
||||
activePlayer.position = clamped
|
||||
parent.pendingSeekPosition = -1
|
||||
}
|
||||
if (parent.pressed && root.isSeeking)
|
||||
root.isDraggingSeek = true;
|
||||
}
|
||||
}
|
||||
|
||||
onPressed: (mouse) => {
|
||||
root.isSeeking = true
|
||||
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
|
||||
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
|
||||
pendingSeekPosition = r * activePlayer.length
|
||||
flatSeekDebounceTimer.restart()
|
||||
}
|
||||
}
|
||||
onReleased: {
|
||||
root.isSeeking = false
|
||||
flatSeekDebounceTimer.stop()
|
||||
if (pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
|
||||
const clamped = Math.min(pendingSeekPosition, activePlayer.length * 0.99)
|
||||
activePlayer.position = clamped
|
||||
pendingSeekPosition = -1
|
||||
}
|
||||
}
|
||||
onPositionChanged: (mouse) => {
|
||||
if (pressed && root.isSeeking && activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
|
||||
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
|
||||
pendingSeekPosition = r * activePlayer.length
|
||||
flatSeekDebounceTimer.restart()
|
||||
}
|
||||
}
|
||||
onClicked: (mouse) => {
|
||||
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
|
||||
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
|
||||
activePlayer.position = r * activePlayer.length
|
||||
}
|
||||
}
|
||||
onPressed: mouse => root.handleSeekPressed(mouse, parent.width, flatMouseArea, flatHoldIndicatorTimer)
|
||||
onReleased: root.handleSeekReleased(flatMouseArea, flatHoldIndicatorTimer)
|
||||
onPositionChanged: mouse => root.handleSeekPositionChanged(mouse, parent.width, flatMouseArea)
|
||||
onCanceled: root.handleSeekCanceled(flatMouseArea, flatHoldIndicatorTimer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ Item {
|
||||
id: root
|
||||
|
||||
property real value: 0
|
||||
property real actualValue: value
|
||||
property bool showActualPlaybackState: false
|
||||
property real lineWidth: 2
|
||||
property real wavelength: 20
|
||||
property real amp: 1.6
|
||||
@@ -15,6 +17,7 @@ Item {
|
||||
property color trackColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.40)
|
||||
property color fillColor: Theme.primary
|
||||
property color playheadColor: Theme.primary
|
||||
property color actualProgressColor: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.45)
|
||||
|
||||
property real dpr: (root.window ? root.window.devicePixelRatio : 1)
|
||||
function snap(v) {
|
||||
@@ -22,7 +25,12 @@ Item {
|
||||
}
|
||||
|
||||
readonly property real playX: snap(root.width * root.value)
|
||||
readonly property real actualX: snap(root.width * root.actualValue)
|
||||
readonly property real midY: snap(height / 2)
|
||||
readonly property bool previewAhead: root.showActualPlaybackState && root.value > root.actualValue
|
||||
readonly property bool previewBehind: root.showActualPlaybackState && root.value < root.actualValue
|
||||
readonly property real previewGapStartX: Math.min(root.playX, root.actualX)
|
||||
readonly property real previewGapEndX: Math.max(root.playX, root.actualX)
|
||||
|
||||
Behavior on currentAmp {
|
||||
NumberAnimation {
|
||||
@@ -65,7 +73,9 @@ Item {
|
||||
|
||||
readonly property real startX: snap(root.lineWidth / 2)
|
||||
readonly property real aaBias: (0.25 / root.dpr)
|
||||
readonly property real endX: Math.max(startX, Math.min(root.playX - startX - aaBias, width))
|
||||
readonly property real endX: root.previewAhead ? Math.max(startX, Math.min(root.actualX - aaBias, width)) : Math.max(startX, Math.min(root.playX - startX - aaBias, width))
|
||||
readonly property real gapStartX: root.previewAhead ? Math.max(startX, Math.min(root.actualX + aaBias, width)) : Math.max(startX, Math.min(root.playX + playhead.width / 2, width))
|
||||
readonly property real gapEndX: root.previewAhead ? Math.max(gapStartX, Math.min(root.playX - playhead.width / 2 - aaBias, width)) : Math.max(gapStartX, Math.min(root.actualX - aaBias, width))
|
||||
|
||||
Rectangle {
|
||||
id: mask
|
||||
@@ -100,6 +110,37 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: actualMask
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
x: waveClip.gapStartX
|
||||
width: Math.max(0, waveClip.gapEndX - waveClip.gapStartX)
|
||||
color: "transparent"
|
||||
clip: true
|
||||
visible: (root.previewBehind || root.previewAhead) && width > 0
|
||||
|
||||
Shape {
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: root.width + 4 * root.wavelength
|
||||
antialiasing: true
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
x: waveOffsetX
|
||||
|
||||
ShapePath {
|
||||
strokeColor: root.actualProgressColor
|
||||
strokeWidth: snap(root.lineWidth)
|
||||
capStyle: ShapePath.RoundCap
|
||||
joinStyle: ShapePath.RoundJoin
|
||||
fillColor: "transparent"
|
||||
PathSvg {
|
||||
path: waveSvg.path
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: startCap
|
||||
width: snap(root.lineWidth)
|
||||
@@ -107,7 +148,7 @@ Item {
|
||||
radius: width / 2
|
||||
color: root.fillColor
|
||||
x: waveClip.startX - width / 2
|
||||
y: root.midY - height / 2 + root.currentAmp * Math.sin((waveClip.startX / root.wavelength) * 2 * Math.PI + root.phase)
|
||||
y: waveY(waveClip.startX) - height / 2
|
||||
visible: waveClip.endX > waveClip.startX
|
||||
z: 2
|
||||
}
|
||||
@@ -119,10 +160,34 @@ Item {
|
||||
radius: width / 2
|
||||
color: root.fillColor
|
||||
x: waveClip.endX - width / 2
|
||||
y: root.midY - height / 2 + root.currentAmp * Math.sin((waveClip.endX / root.wavelength) * 2 * Math.PI + root.phase)
|
||||
y: waveY(waveClip.endX) - height / 2
|
||||
visible: waveClip.endX > waveClip.startX
|
||||
z: 2
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: actualEndCap
|
||||
width: snap(root.lineWidth)
|
||||
height: snap(root.lineWidth)
|
||||
radius: width / 2
|
||||
color: root.actualProgressColor
|
||||
x: waveClip.gapEndX - width / 2
|
||||
y: waveY(waveClip.gapEndX) - height / 2
|
||||
visible: (root.previewBehind || root.previewAhead) && actualMask.width > 0
|
||||
z: 2
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: actualMarker
|
||||
width: 2
|
||||
height: Math.max(root.lineWidth + 4, 10)
|
||||
radius: width / 2
|
||||
color: root.actualProgressColor
|
||||
x: root.actualX - width / 2
|
||||
y: root.midY - height / 2
|
||||
visible: root.showActualPlaybackState
|
||||
z: 2
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
@@ -141,6 +206,10 @@ Item {
|
||||
let r = a % m;
|
||||
return r < 0 ? r + m : r;
|
||||
}
|
||||
function waveY(x, amplitude = root.currentAmp, phaseOffset = root.phase) {
|
||||
return root.midY + amplitude * Math.sin((x / root.wavelength) * 2 * Math.PI + phaseOffset);
|
||||
}
|
||||
|
||||
readonly property real waveOffsetX: -wrapMod(phase / k, wavelength)
|
||||
|
||||
FrameAnimation {
|
||||
@@ -148,8 +217,9 @@ Item {
|
||||
onTriggered: {
|
||||
if (root.isPlaying)
|
||||
root.phase += 0.03 * frameTime * 60;
|
||||
startCap.y = root.midY - startCap.height / 2 + root.currentAmp * Math.sin((waveClip.startX / root.wavelength) * 2 * Math.PI + root.phase);
|
||||
endCap.y = root.midY - endCap.height / 2 + root.currentAmp * Math.sin((waveClip.endX / root.wavelength) * 2 * Math.PI + root.phase);
|
||||
startCap.y = waveY(waveClip.startX) - startCap.height / 2;
|
||||
endCap.y = waveY(waveClip.endX) - endCap.height / 2;
|
||||
actualEndCap.y = waveY(waveClip.gapEndX) - actualEndCap.height / 2;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user