1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-08 12:13:31 -04:00
Files
DankMaterialShell/quickshell/Widgets/DankSeekbar.qml
T
Huỳnh Thiện Lộc 89f86be00a feat: unify media controls dropdown interactions, hover behavior and cycle controls (#2470)
* feat: unify media controls dropdown interactions, hover behavior and cycle controls

- Implement hover-to-show and hover-to-hide for all media control dropdowns.
- Make clicking the Output Devices and Media Players buttons cycle through items when expanded.
- Always display the 'speaker' icon for Output Devices to maintain visual consistency.
- Bind dropdown player properties dynamically to fix list stale rendering states.

* fix(DankDash): use trackArtist property for artist label in MediaPlayerTab

* fix(DankDash): simplify active player label for consistency with output devices

* feat(DankDash): display volume levels for audio output devices in dropdown

* fix(DankDash): display Unknown Artist when artist is empty in player list

* feat(DankDash): add keyboard shortcuts for seeking, track cycling and playback control in Media popout

* feat(DankDash): change Up/Down arrow keys to adjust volume in Media popout

* feat(DankDash): auto-open volume dropdown overlay when using Up/Down shortcuts

* feat(DankDash): add Key M shortcut to toggle mute in Media popout

* fix(mpris): clamp minimum seek position to 0.1s to prevent browser player reset

* fix(mpris): cache stable length to prevent browser transient reset issues

* fix(mpris): persist activePlayerStableLength in MprisController singleton

* fix(mpris): resolve browser player album art with raw metadata and YouTube url fallbacks

* fix(mpris): resolve browser player album art with local caching and 16:9 youtube fallbacks

* style(mpris): trim trailing whitespace in TrackArtService

* fix(mpris): address code review feedback on remote caching, stale artwork, and hover state

* fix: secure curl commands and prevent premature dropdown overlays closing on button re-hover
2026-05-26 13:44:51 -04:00

265 lines
9.7 KiB
QML

import QtQuick
import Quickshell.Services.Mpris
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property MprisPlayer activePlayer
readonly property real stableLength: MprisController.activePlayerStableLength
property real seekPreviewRatio: -1
readonly property real playerValue: {
if (!activePlayer || stableLength <= 0)
return 0;
const pos = (activePlayer.position || 0) % Math.max(1, stableLength);
const calculatedRatio = pos / stableLength;
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 || stableLength <= 0)
return 0;
return clampRatio(position / stableLength);
}
function positionForRatio(ratio) {
if (!activePlayer || stableLength <= 0)
return 0;
const rawPosition = clampRatio(ratio) * stableLength;
return Math.min(rawPosition, stableLength * 0.99);
}
function updatePreviewFromMouse(mouseX, width) {
if (!activePlayer || stableLength <= 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 && stableLength > 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 && stableLength > 0) {
const clamped = Math.min(mouseArea.pendingSeekPosition, stableLength * 0.99);
activePlayer.position = Math.max(0.1, clamped);
mouseArea.pendingSeekPosition = -1;
beginCommittedSeekPreview(clamped);
} else {
seekPreviewRatio = -1;
}
}
function handleSeekPositionChanged(mouse, width, mouseArea) {
if (mouseArea.pressed && isSeeking && activePlayer && stableLength > 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
Loader {
anchors.fill: parent
visible: activePlayer && stableLength > 0
sourceComponent: SettingsData.waveProgressEnabled ? waveProgressComponent : flatProgressComponent
z: 1
Component {
id: waveProgressComponent
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 && stableLength > 0
property real pendingSeekPosition: -1
property real pressX: 0
Timer {
id: waveHoldIndicatorTimer
interval: root.holdIndicatorDelay
repeat: false
onTriggered: {
if (parent.pressed && root.isSeeking)
root.isDraggingSeek = true;
}
}
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)
}
}
}
Component {
id: flatProgressComponent
Item {
property real lineWidth: 3
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 {
width: parent.width
height: parent.lineWidth
anchors.verticalCenter: parent.verticalCenter
color: parent.trackColor
radius: height / 2
}
Rectangle {
width: Math.max(0, Math.min(parent.width, parent.width * root.value))
height: parent.lineWidth
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
color: parent.fillColor
radius: height / 2
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 {
id: playhead
width: 3
height: Math.max(parent.lineWidth + 8, 14)
radius: width / 2
color: parent.playheadColor
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
}
}
}
MouseArea {
id: flatMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: activePlayer && activePlayer.canSeek && stableLength > 0
property real pendingSeekPosition: -1
property real pressX: 0
Timer {
id: flatHoldIndicatorTimer
interval: root.holdIndicatorDelay
repeat: false
onTriggered: {
if (parent.pressed && root.isSeeking)
root.isDraggingSeek = true;
}
}
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)
}
}
}
}
}