1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-08 04:09:15 -04:00
Files
DankMaterialShell/quickshell/Modules/OSD/MediaPlaybackOSD.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

332 lines
9.5 KiB
QML

import QtQuick
import QtQuick.Effects
import qs.Common
import qs.Services
import qs.Widgets
import Quickshell.Services.Mpris
DankOSD {
id: root
readonly property bool useVertical: isVerticalLayout
readonly property var player: MprisController.activePlayer
osdWidth: useVertical ? (40 + Theme.spacingS * 2) : Math.min(280, Screen.width - Theme.spacingM * 2)
osdHeight: useVertical ? (Theme.iconSize * 2) : (40 + Theme.spacingS * 2)
autoHideInterval: 3000
enableMouseInteraction: true
property string _displayIcon: "music_note"
function updatePlaybackIcon() {
if (!player) {
_displayIcon = "music_note";
iconDebounce.stop();
return;
}
let icon = "music_note";
switch (player.playbackState) {
case MprisPlaybackState.Playing:
icon = "pause";
break;
case MprisPlaybackState.Paused:
case MprisPlaybackState.Stopped:
icon = "play_arrow";
break;
}
if (icon === _displayIcon)
return;
iconDebounce.pendingIcon = icon;
iconDebounce.restart();
}
function togglePlaying() {
if (player?.canTogglePlaying) {
player.togglePlaying();
}
}
property bool _pendingShow: false
property string _displayTitle: ""
property string _displayArtist: ""
property string _displayAlbum: ""
Timer {
id: iconDebounce
interval: 150
property string pendingIcon: "music_note"
onTriggered: root._displayIcon = pendingIcon
}
Image {
id: artPreloader
source: TrackArtService.resolvedArtUrl
visible: false
asynchronous: true
cache: true
}
onPlayerChanged: {
if (!player) {
_pendingShow = false;
hide();
}
}
Connections {
target: TrackArtService
function onLoadingChanged() {
if (TrackArtService.loading || !root._pendingShow)
return;
if (!TrackArtService.resolvedArtUrl || artPreloader.status === Image.Ready) {
root._pendingShow = false;
root.show();
}
}
}
Connections {
target: artPreloader
function onStatusChanged() {
if (!root._pendingShow || TrackArtService.loading)
return;
switch (artPreloader.status) {
case Image.Ready:
case Image.Error:
root._pendingShow = false;
root.show();
break;
}
}
}
Connections {
target: player
function handleUpdate() {
if (!root.player?.trackTitle)
return;
if (!SettingsData.osdMediaPlaybackEnabled)
return;
if (MprisController.isFirefoxYoutubeHoverPreview(player))
return;
root._displayTitle = player.trackTitle || "";
root._displayArtist = player.trackArtist || "";
root._displayAlbum = player.trackAlbum || "";
root.updatePlaybackIcon();
const resolvedArtUrl = TrackArtService.resolvedArtUrl;
if (!resolvedArtUrl || resolvedArtUrl === "") {
root.show();
return;
}
if (TrackArtService.loading) {
root._pendingShow = true;
return;
}
if (!TrackArtService.resolvedArtUrl || artPreloader.status === Image.Ready) {
root.show();
return;
}
root._pendingShow = true;
}
function onTrackArtUrlChanged() {
handleUpdate();
}
function onMetadataChanged() {
handleUpdate();
}
function onIsPlayingChanged() {
handleUpdate();
}
function onTrackChanged() {
if (!useVertical)
handleUpdate();
}
}
content: Loader {
anchors.fill: parent
sourceComponent: useVertical ? verticalContent : horizontalContent
}
Component {
id: horizontalContent
Item {
property int gap: Theme.spacingS
anchors.centerIn: parent
width: parent.width - Theme.spacingS * 2
height: 40
MouseArea {
anchors.fill: parent
onClicked: root.hide()
}
Item {
id: bgContainer
anchors.fill: parent
visible: TrackArtService.resolvedArtUrl !== ""
Image {
id: bgImage
anchors.centerIn: parent
width: Math.max(parent.width, parent.height)
height: width
source: TrackArtService.resolvedArtUrl
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: true
visible: false
}
Item {
id: blurredBg
anchors.fill: parent
visible: false
MultiEffect {
anchors.centerIn: parent
width: bgImage.width
height: bgImage.height
source: bgImage
blurEnabled: true
blurMax: 64
blur: 0.3
saturation: -0.2
brightness: -0.25
}
}
Rectangle {
id: bgMask
anchors.fill: parent
radius: Theme.cornerRadius
visible: false
layer.enabled: true
}
MultiEffect {
anchors.fill: parent
source: blurredBg
maskEnabled: true
maskSource: bgMask
maskThresholdMin: 0.5
maskSpreadAtMin: 1.0
opacity: 0.7
}
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: Theme.surface
opacity: 0.3
}
}
Rectangle {
width: Theme.iconSize
height: Theme.iconSize
radius: Theme.iconSize / 2
color: "transparent"
x: parent.gap
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: root._displayIcon
size: Theme.iconSize
color: playPauseButton.containsMouse ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: playPauseButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
togglePlaying();
root.hide();
}
}
}
Column {
x: parent.gap * 2 + Theme.iconSize
width: parent.width - Theme.iconSize - parent.gap * 3
anchors.verticalCenter: parent.verticalCenter
spacing: 3
StyledText {
id: topText
width: parent.width
text: player ? (root._displayTitle || I18n.tr("Unknown Title")) : ""
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
wrapMode: Text.NoWrap
elide: Text.ElideRight
}
StyledText {
id: bottomText
width: parent.width
text: player ? ((root._displayArtist || I18n.tr("Unknown Artist")) + (root._displayAlbum ? ` ${root._displayAlbum}` : "")) : ""
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Light
color: Theme.surfaceText
wrapMode: Text.NoWrap
elide: Text.ElideRight
}
}
}
}
Component {
id: verticalContent
Item {
property int gap: Theme.spacingS
MouseArea {
anchors.fill: parent
onClicked: root.hide()
}
Rectangle {
width: Theme.iconSize
height: Theme.iconSize
radius: Theme.iconSize / 2
color: "transparent"
anchors.centerIn: parent
y: gap
DankIcon {
anchors.centerIn: parent
name: root._displayIcon
size: Theme.iconSize
color: playPauseButtonVert.containsMouse ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: playPauseButtonVert
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
togglePlaying();
root.hide();
}
}
}
}
}
}