mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-08 12:13:31 -04:00
89f86be00a
* 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
265 lines
9.7 KiB
QML
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|