1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-07 19:59:14 -04:00

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
This commit is contained in:
Huỳnh Thiện Lộc
2026-05-27 00:44:51 +07:00
committed by GitHub
parent 12a744e985
commit 89f86be00a
9 changed files with 411 additions and 109 deletions
+16 -8
View File
@@ -25,14 +25,14 @@ DankPopout {
property int __dropdownType: 0 property int __dropdownType: 0
property point __dropdownAnchor: Qt.point(0, 0) property point __dropdownAnchor: Qt.point(0, 0)
property bool __dropdownRightEdge: false property bool __dropdownRightEdge: false
property var __dropdownPlayer: null property var __dropdownPlayer: MprisController.activePlayer
property var __dropdownPlayers: [] property var __dropdownPlayers: MprisController.availablePlayers
function __showVolumeDropdown(pos, rightEdge, player, players) { function __showVolumeDropdown(pos, rightEdge, player, players) {
__dropdownAnchor = pos; __dropdownAnchor = pos;
__dropdownRightEdge = rightEdge; __dropdownRightEdge = rightEdge;
__dropdownPlayer = player; __dropdownPlayer = Qt.binding(() => MprisController.activePlayer);
__dropdownPlayers = players; __dropdownPlayers = Qt.binding(() => MprisController.availablePlayers);
__dropdownType = 1; __dropdownType = 1;
} }
@@ -45,8 +45,8 @@ DankPopout {
function __showPlayersDropdown(pos, rightEdge, player, players) { function __showPlayersDropdown(pos, rightEdge, player, players) {
__dropdownAnchor = pos; __dropdownAnchor = pos;
__dropdownRightEdge = rightEdge; __dropdownRightEdge = rightEdge;
__dropdownPlayer = player; __dropdownPlayer = Qt.binding(() => MprisController.activePlayer);
__dropdownPlayers = players; __dropdownPlayers = Qt.binding(() => MprisController.availablePlayers);
__dropdownType = 3; __dropdownType = 3;
} }
@@ -69,7 +69,7 @@ DankPopout {
id: __volumeCloseTimer id: __volumeCloseTimer
interval: 400 interval: 400
onTriggered: { onTriggered: {
if (__dropdownType === 1) { if (__dropdownType !== 0) {
__hideDropdowns(); __hideDropdowns();
} }
} }
@@ -230,6 +230,13 @@ DankPopout {
return; return;
} }
if (root.currentTabIndex === 1 && mediaLoader.item?.handleKeyEvent) {
if (mediaLoader.item.handleKeyEvent(event)) {
event.accepted = true;
return;
}
}
if (root.currentTabIndex === 2 && wallpaperLoader.item?.handleKeyEvent) { if (root.currentTabIndex === 2 && wallpaperLoader.item?.handleKeyEvent) {
if (wallpaperLoader.item.handleKeyEvent(event)) { if (wallpaperLoader.item.handleKeyEvent(event)) {
event.accepted = true; event.accepted = true;
@@ -394,7 +401,8 @@ DankPopout {
root.__showPlayersDropdown(pos, rightEdge, player, players); root.__showPlayersDropdown(pos, rightEdge, player, players);
} }
onHideDropdowns: root.__hideDropdowns() onHideDropdowns: root.__hideDropdowns()
onVolumeButtonExited: root.__startCloseTimer() onDropdownButtonExited: root.__startCloseTimer()
onDropdownButtonEntered: root.__stopCloseTimer()
} }
} }
} }
@@ -42,16 +42,22 @@ Item {
signal panelEntered signal panelEntered
signal panelExited signal panelExited
property int __volumeHoverCount: 0 property int __panelHoverCount: 0
function volumeAreaEntered() { onDropdownTypeChanged: {
__volumeHoverCount++; if (dropdownType === 0) {
__panelHoverCount = 0;
}
}
function panelAreaEntered() {
__panelHoverCount++;
panelEntered(); panelEntered();
} }
function volumeAreaExited() { function panelAreaExited() {
__volumeHoverCount = Math.max(0, __volumeHoverCount - 1); __panelHoverCount = Math.max(0, __panelHoverCount - 1);
if (__volumeHoverCount === 0) if (__panelHoverCount === 0)
panelExited(); panelExited();
} }
@@ -131,8 +137,8 @@ Item {
anchors.fill: parent anchors.fill: parent
anchors.margins: -12 anchors.margins: -12
hoverEnabled: true hoverEnabled: true
onEntered: volumeAreaEntered() onEntered: panelAreaEntered()
onExited: volumeAreaExited() onExited: panelAreaExited()
} }
Item { Item {
@@ -190,8 +196,8 @@ Item {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
preventStealing: true preventStealing: true
onEntered: volumeAreaEntered() onEntered: panelAreaEntered()
onExited: volumeAreaExited() onExited: panelAreaExited()
onPressed: mouse => updateVolume(mouse) onPressed: mouse => updateVolume(mouse)
onPositionChanged: mouse => { onPositionChanged: mouse => {
if (pressed) if (pressed)
@@ -269,6 +275,14 @@ Item {
shadowEnabled: Theme.elevationEnabled && !BlurService.enabled shadowEnabled: Theme.elevationEnabled && !BlurService.enabled
} }
MouseArea {
anchors.fill: parent
anchors.margins: -12
hoverEnabled: true
onEntered: panelAreaEntered()
onExited: panelAreaExited()
}
Column { Column {
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.spacingM anchors.margins: Theme.spacingM
@@ -349,7 +363,13 @@ Item {
} }
StyledText { StyledText {
text: modelData === AudioService.sink ? "Active" : "Available" text: {
if (!modelData?.audio)
return modelData === AudioService.sink ? I18n.tr("Active") : I18n.tr("Available");
if (modelData.audio.muted)
return I18n.tr("Muted", "audio status");
return Math.round(modelData.audio.volume * 100) + "%";
}
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
elide: Text.ElideRight elide: Text.ElideRight
@@ -369,6 +389,8 @@ Item {
root.deviceSelected(modelData); root.deviceSelected(modelData);
} }
} }
onEntered: panelAreaEntered()
onExited: panelAreaExited()
} }
} }
} }
@@ -425,6 +447,14 @@ Item {
shadowEnabled: Theme.elevationEnabled && !BlurService.enabled shadowEnabled: Theme.elevationEnabled && !BlurService.enabled
} }
MouseArea {
anchors.fill: parent
anchors.margins: -12
hoverEnabled: true
onEntered: panelAreaEntered()
onExited: panelAreaExited()
}
Column { Column {
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.spacingM anchors.margins: Theme.spacingM
@@ -498,15 +528,7 @@ Item {
} }
StyledText { StyledText {
text: { text: modelData?.trackArtist || I18n.tr("Unknown Artist")
if (!modelData)
return "";
const artist = modelData.trackArtist || "";
const isActive = modelData === activePlayer;
if (artist.length > 0)
return artist + (isActive ? " (Active)" : "");
return isActive ? "Active" : "Available";
}
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
elide: Text.ElideRight elide: Text.ElideRight
@@ -526,6 +548,8 @@ Item {
root.playerSelected(modelData); root.playerSelected(modelData);
} }
} }
onEntered: panelAreaEntered()
onExited: panelAreaExited()
} }
} }
} }
+170 -42
View File
@@ -13,6 +13,7 @@ Item {
LayoutMirroring.childrenInherit: true LayoutMirroring.childrenInherit: true
property MprisPlayer activePlayer: MprisController.activePlayer property MprisPlayer activePlayer: MprisController.activePlayer
readonly property real stableLength: MprisController.activePlayerStableLength
property var allPlayers: MprisController.availablePlayers property var allPlayers: MprisController.availablePlayers
property var targetScreen: null property var targetScreen: null
property real popoutX: 0 property real popoutX: 0
@@ -27,7 +28,8 @@ Item {
signal showAudioDevicesDropdown(point pos, var screen, bool rightEdge) signal showAudioDevicesDropdown(point pos, var screen, bool rightEdge)
signal showPlayersDropdown(point pos, var screen, bool rightEdge, var player, var players) signal showPlayersDropdown(point pos, var screen, bool rightEdge, var player, var players)
signal hideDropdowns signal hideDropdowns
signal volumeButtonExited signal dropdownButtonExited
signal dropdownButtonEntered
property bool volumeExpanded: false property bool volumeExpanded: false
property bool devicesExpanded: false property bool devicesExpanded: false
@@ -39,9 +41,7 @@ Item {
playersExpanded = false; playersExpanded = false;
} }
DankTooltipV2 {
id: sharedTooltip
}
readonly property bool isRightEdge: { readonly property bool isRightEdge: {
if (barPosition === SettingsData.Position.Right) if (barPosition === SettingsData.Position.Right)
@@ -85,7 +85,6 @@ Item {
isSwitching = true; isSwitching = true;
_switchHold = true; _switchHold = true;
_switchHoldTimer.restart(); _switchHoldTimer.restart();
TrackArtService.loadArtwork(activePlayer.trackArtUrl);
} }
function maybeFinishSwitch() { function maybeFinishSwitch() {
@@ -96,11 +95,11 @@ Item {
} }
readonly property real ratio: { readonly property real ratio: {
if (!activePlayer || !activePlayer.length || activePlayer.length <= 0) { if (!activePlayer || stableLength <= 0) {
return 0; return 0;
} }
const pos = (activePlayer.position || 0) % Math.max(1, activePlayer.length); const pos = (activePlayer.position || 0) % Math.max(1, stableLength);
const calculatedRatio = pos / activePlayer.length; const calculatedRatio = pos / stableLength;
return Math.max(0, Math.min(1, calculatedRatio)); return Math.max(0, Math.min(1, calculatedRatio));
} }
@@ -109,13 +108,11 @@ Item {
Connections { Connections {
target: activePlayer target: activePlayer
ignoreUnknownSignals: true
function onTrackTitleChanged() { function onTrackTitleChanged() {
_switchHoldTimer.restart(); _switchHoldTimer.restart();
maybeFinishSwitch(); maybeFinishSwitch();
} }
function onTrackArtUrlChanged() {
TrackArtService.loadArtwork(activePlayer.trackArtUrl);
}
} }
Connections { Connections {
@@ -186,6 +183,102 @@ Item {
} }
} }
function triggerVolumeDropdown() {
if (!volumeAvailable)
return;
if (volumeExpanded)
return;
hideDropdowns();
volumeExpanded = true;
const buttonsOnRight = !isRightEdge;
const btnY = volumeButton.y + volumeButton.height / 2;
const screenX = buttonsOnRight ? (popoutX + popoutWidth) : popoutX;
const screenY = popoutY + contentOffsetY + btnY;
showVolumeDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight, activePlayer, allPlayers);
}
function toggleMute() {
if (!volumeAvailable)
return;
SessionData.suppressOSDTemporarily();
if (currentVolume > 0) {
volumeButton.previousVolume = currentVolume;
if (usePlayerVolume) {
activePlayer.volume = 0;
} else if (AudioService.sink?.audio) {
AudioService.sink.audio.volume = 0;
}
} else {
const restoreVolume = volumeButton.previousVolume > 0 ? volumeButton.previousVolume : 0.5;
if (usePlayerVolume) {
activePlayer.volume = restoreVolume;
} else if (AudioService.sink?.audio) {
AudioService.sink.audio.volume = restoreVolume;
}
}
}
function handleKeyEvent(event) {
if (!activePlayer)
return false;
// 1. Number keys 0-9 to seek to 0%-90%
if (event.key >= Qt.Key_0 && event.key <= Qt.Key_9) {
if (activePlayer.canSeek && stableLength > 0) {
const ratio = (event.key - Qt.Key_0) * 0.1;
const targetPosition = ratio * stableLength;
activePlayer.position = Math.max(0.1, Math.min(targetPosition, stableLength * 0.99));
return true;
}
}
// 2. Left / Right arrows to seek backward / forward 5s
if (event.key === Qt.Key_Left) {
if (activePlayer.canSeek) {
activePlayer.position = Math.max(0.1, activePlayer.position - 5);
return true;
}
}
if (event.key === Qt.Key_Right) {
if (activePlayer.canSeek && stableLength > 0) {
activePlayer.position = Math.max(0.1, Math.min(stableLength - 1, activePlayer.position + 5));
return true;
}
}
// 3. Up / Down arrows to adjust volume
if (event.key === Qt.Key_Up) {
adjustVolume(5);
triggerVolumeDropdown();
dropdownButtonExited();
return true;
}
if (event.key === Qt.Key_Down) {
adjustVolume(-5);
triggerVolumeDropdown();
dropdownButtonExited();
return true;
}
// 4. Spacebar to play/pause
if (event.key === Qt.Key_Space) {
if (activePlayer.canTogglePlaying) {
activePlayer.togglePlaying();
return true;
}
}
// 5. M key to toggle mute
if (event.key === Qt.Key_M) {
toggleMute();
triggerVolumeDropdown();
dropdownButtonExited();
return true;
}
return false;
}
property bool isSeeking: false property bool isSeeking: false
Timer { Timer {
@@ -198,14 +291,14 @@ Item {
Item { Item {
id: bgContainer id: bgContainer
anchors.fill: parent anchors.fill: parent
visible: TrackArtService._bgArtSource !== "" visible: TrackArtService.resolvedArtUrl !== ""
Image { Image {
id: bgImage id: bgImage
anchors.centerIn: parent anchors.centerIn: parent
width: Math.max(parent.width, parent.height) * 1.1 width: Math.max(parent.width, parent.height) * 1.1
height: width height: width
source: TrackArtService._bgArtSource source: TrackArtService.resolvedArtUrl
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
asynchronous: true asynchronous: true
cache: true cache: true
@@ -331,7 +424,7 @@ Item {
} }
StyledText { StyledText {
text: activePlayer?.trackTitle || I18n.tr("Unknown Artist") text: activePlayer?.trackArtist || I18n.tr("Unknown Artist")
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8) color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
width: parent.width width: parent.width
@@ -389,7 +482,7 @@ Item {
if (!activePlayer) if (!activePlayer)
return "0:00"; return "0:00";
const rawPos = Math.max(0, activePlayer.position || 0); const rawPos = Math.max(0, activePlayer.position || 0);
const pos = activePlayer.length ? rawPos % Math.max(1, activePlayer.length) : rawPos; const pos = stableLength ? rawPos % Math.max(1, stableLength) : rawPos;
const minutes = Math.floor(pos / 60); const minutes = Math.floor(pos / 60);
const seconds = Math.floor(pos % 60); const seconds = Math.floor(pos % 60);
const timeStr = minutes + ":" + (seconds < 10 ? "0" : "") + seconds; const timeStr = minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
@@ -403,9 +496,9 @@ Item {
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: { text: {
if (!activePlayer || !activePlayer.length) if (!activePlayer || stableLength <= 0)
return "0:00"; return "--:--";
const dur = Math.max(0, activePlayer.length || 0); const dur = stableLength;
const minutes = Math.floor(dur / 60); const minutes = Math.floor(dur / 60);
const seconds = Math.floor(dur % 60); const seconds = Math.floor(dur % 60);
return minutes + ":" + (seconds < 10 ? "0" : "") + seconds; return minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
@@ -647,7 +740,17 @@ Item {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
if (playersExpanded) { if (playersExpanded) {
hideDropdowns(); if (allPlayers && allPlayers.length > 1) {
let currentIndex = -1;
for (let i = 0; i < allPlayers.length; i++) {
if (allPlayers[i] === activePlayer) {
currentIndex = i;
break;
}
}
const nextIndex = (currentIndex + 1) % allPlayers.length;
MprisController.setActivePlayer(allPlayers[nextIndex]);
}
return; return;
} }
hideDropdowns(); hideDropdowns();
@@ -658,8 +761,22 @@ Item {
const screenY = popoutY + contentOffsetY + btnY; const screenY = popoutY + contentOffsetY + btnY;
showPlayersDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight, activePlayer, allPlayers); showPlayersDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight, activePlayer, allPlayers);
} }
onEntered: sharedTooltip.show(I18n.tr("Media Players"), playerSelectorButton, 0, 0, isRightEdge ? "right" : "left") onEntered: {
onExited: sharedTooltip.hide() dropdownButtonEntered();
if (playersExpanded)
return;
hideDropdowns();
playersExpanded = true;
const buttonsOnRight = !isRightEdge;
const btnY = playerSelectorButton.y + playerSelectorButton.height / 2;
const screenX = buttonsOnRight ? (popoutX + popoutWidth) : popoutX;
const screenY = popoutY + contentOffsetY + btnY;
showPlayersDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight, activePlayer, allPlayers);
}
onExited: {
if (playersExpanded)
dropdownButtonExited();
}
} }
} }
@@ -691,6 +808,7 @@ Item {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onEntered: { onEntered: {
dropdownButtonEntered();
if (volumeExpanded) if (volumeExpanded)
return; return;
hideDropdowns(); hideDropdowns();
@@ -703,25 +821,10 @@ Item {
} }
onExited: { onExited: {
if (volumeExpanded) if (volumeExpanded)
volumeButtonExited(); dropdownButtonExited();
} }
onClicked: { onClicked: {
SessionData.suppressOSDTemporarily(); toggleMute();
if (currentVolume > 0) {
volumeButton.previousVolume = currentVolume;
if (usePlayerVolume) {
activePlayer.volume = 0;
} else if (AudioService.sink?.audio) {
AudioService.sink.audio.volume = 0;
}
} else {
const restoreVolume = volumeButton.previousVolume > 0 ? volumeButton.previousVolume : 0.5;
if (usePlayerVolume) {
activePlayer.volume = restoreVolume;
} else if (AudioService.sink?.audio) {
AudioService.sink.audio.volume = restoreVolume;
}
}
} }
onWheel: wheelEvent => { onWheel: wheelEvent => {
SessionData.suppressOSDTemporarily(); SessionData.suppressOSDTemporarily();
@@ -754,7 +857,7 @@ Item {
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: devicesExpanded ? "expand_less" : "speaker" name: "speaker"
size: 18 size: 18
color: Theme.surfaceText color: Theme.surfaceText
} }
@@ -766,7 +869,18 @@ Item {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
if (devicesExpanded) { if (devicesExpanded) {
hideDropdowns(); const sinks = AudioService.getAvailableSinks();
if (sinks && sinks.length > 1) {
let currentIndex = -1;
for (let i = 0; i < sinks.length; i++) {
if (sinks[i]?.name === AudioService.sink?.name) {
currentIndex = i;
break;
}
}
const nextIndex = (currentIndex + 1) % sinks.length;
AudioService.setSink(sinks[nextIndex]);
}
return; return;
} }
hideDropdowns(); hideDropdowns();
@@ -777,8 +891,22 @@ Item {
const screenY = popoutY + contentOffsetY + btnY; const screenY = popoutY + contentOffsetY + btnY;
showAudioDevicesDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight); showAudioDevicesDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight);
} }
onEntered: sharedTooltip.show(I18n.tr("Output Device"), audioDevicesButton, 0, 0, isRightEdge ? "right" : "left") onEntered: {
onExited: sharedTooltip.hide() dropdownButtonEntered();
if (devicesExpanded)
return;
hideDropdowns();
devicesExpanded = true;
const buttonsOnRight = !isRightEdge;
const btnY = audioDevicesButton.y + audioDevicesButton.height / 2;
const screenX = buttonsOnRight ? (popoutX + popoutWidth) : popoutX;
const screenY = popoutY + contentOffsetY + btnY;
showAudioDevicesDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight);
}
onExited: {
if (devicesExpanded)
dropdownButtonExited();
}
} }
} }
} }
@@ -15,10 +15,11 @@ Card {
property real displayPosition: currentPosition property real displayPosition: currentPosition
readonly property real ratio: { readonly property real ratio: {
if (!activePlayer || activePlayer.length <= 0) const len = MprisController.activePlayerStableLength;
if (!activePlayer || !activePlayer.lengthSupported || len <= 0)
return 0; return 0;
const pos = displayPosition % Math.max(1, activePlayer.length); const pos = displayPosition % Math.max(1, len);
const calculatedRatio = pos / activePlayer.length; const calculatedRatio = pos / len;
return Math.max(0, Math.min(1, calculatedRatio)); return Math.max(0, Math.min(1, calculatedRatio));
} }
+11 -8
View File
@@ -60,7 +60,7 @@ DankOSD {
Image { Image {
id: artPreloader id: artPreloader
source: TrackArtService._bgArtSource source: TrackArtService.resolvedArtUrl
visible: false visible: false
asynchronous: true asynchronous: true
cache: true cache: true
@@ -78,7 +78,7 @@ DankOSD {
function onLoadingChanged() { function onLoadingChanged() {
if (TrackArtService.loading || !root._pendingShow) if (TrackArtService.loading || !root._pendingShow)
return; return;
if (!TrackArtService._bgArtSource || artPreloader.status === Image.Ready) { if (!TrackArtService.resolvedArtUrl || artPreloader.status === Image.Ready) {
root._pendingShow = false; root._pendingShow = false;
root.show(); root.show();
} }
@@ -116,9 +116,9 @@ DankOSD {
root._displayAlbum = player.trackAlbum || ""; root._displayAlbum = player.trackAlbum || "";
root.updatePlaybackIcon(); root.updatePlaybackIcon();
TrackArtService.loadArtwork(player.trackArtUrl); const resolvedArtUrl = TrackArtService.resolvedArtUrl;
if (!player.trackArtUrl || player.trackArtUrl === "") { if (!resolvedArtUrl || resolvedArtUrl === "") {
root.show(); root.show();
return; return;
} }
@@ -126,7 +126,7 @@ DankOSD {
root._pendingShow = true; root._pendingShow = true;
return; return;
} }
if (!TrackArtService._bgArtSource || artPreloader.status === Image.Ready) { if (!TrackArtService.resolvedArtUrl || artPreloader.status === Image.Ready) {
root.show(); root.show();
return; return;
} }
@@ -134,7 +134,10 @@ DankOSD {
} }
function onTrackArtUrlChanged() { function onTrackArtUrlChanged() {
TrackArtService.loadArtwork(player.trackArtUrl); handleUpdate();
}
function onMetadataChanged() {
handleUpdate();
} }
function onIsPlayingChanged() { function onIsPlayingChanged() {
handleUpdate(); handleUpdate();
@@ -168,14 +171,14 @@ DankOSD {
Item { Item {
id: bgContainer id: bgContainer
anchors.fill: parent anchors.fill: parent
visible: TrackArtService._bgArtSource !== "" visible: TrackArtService.resolvedArtUrl !== ""
Image { Image {
id: bgImage id: bgImage
anchors.centerIn: parent anchors.centerIn: parent
width: Math.max(parent.width, parent.height) width: Math.max(parent.width, parent.height)
height: width height: width
source: TrackArtService._bgArtSource source: TrackArtService.resolvedArtUrl
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
asynchronous: true asynchronous: true
cache: true cache: true
+18 -1
View File
@@ -11,6 +11,23 @@ Singleton {
readonly property list<MprisPlayer> availablePlayers: Mpris.players.values readonly property list<MprisPlayer> availablePlayers: Mpris.players.values
property MprisPlayer activePlayer: null property MprisPlayer activePlayer: null
property real activePlayerStableLength: 0
Connections {
target: root.activePlayer
function onTrackTitleChanged() {
root.activePlayerStableLength = (root.activePlayer && root.activePlayer.lengthSupported && root.activePlayer.length > 1) ? root.activePlayer.length : 0;
}
function onLengthChanged() {
if (root.activePlayer && root.activePlayer.lengthSupported && root.activePlayer.length > 1) {
root.activePlayerStableLength = root.activePlayer.length;
}
}
}
onActivePlayerChanged: {
activePlayerStableLength = (activePlayer && activePlayer.lengthSupported && activePlayer.length > 1) ? activePlayer.length : 0;
}
onAvailablePlayersChanged: _resolveActivePlayer() onAvailablePlayersChanged: _resolveActivePlayer()
Component.onCompleted: _resolveActivePlayer() Component.onCompleted: _resolveActivePlayer()
@@ -81,7 +98,7 @@ Singleton {
if (!activePlayer) if (!activePlayer)
return; return;
if (activePlayer.position > 8 && activePlayer.canSeek) if (activePlayer.position > 8 && activePlayer.canSeek)
activePlayer.position = 0; activePlayer.position = 0.1;
else if (activePlayer.canGoPrevious) else if (activePlayer.canGoPrevious)
activePlayer.previous(); activePlayer.previous();
} }
+122 -7
View File
@@ -10,12 +10,53 @@ Singleton {
id: root id: root
property string _lastArtUrl: "" property string _lastArtUrl: ""
property string _bgArtSource: "" property string resolvedArtUrl: ""
property alias _bgArtSource: root.resolvedArtUrl
property bool loading: false property bool loading: false
function djb2Hash(str) {
if (!str) return "";
let hash = 5381;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) + hash) + str.charCodeAt(i);
hash = hash & 0x7FFFFFFF;
}
return hash.toString(16).padStart(8, '0');
}
function getArtworkUrl(player) {
if (!player) return "";
// 1. If native trackArtUrl is present and valid
let artUrl = player.trackArtUrl || "";
if (artUrl !== "") {
return artUrl;
}
// 2. Fallback to raw metadata mpris:artUrl if present
if (player.metadata && player.metadata["mpris:artUrl"]) {
artUrl = player.metadata["mpris:artUrl"].toString();
if (artUrl !== "") return artUrl;
}
// 3. Fallback for YouTube from xesam:url
if (player.metadata && player.metadata["xesam:url"]) {
const url = player.metadata["xesam:url"].toString();
if (url.includes("youtube.com") || url.includes("youtu.be")) {
const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/;
const match = url.match(regExp);
if (match && match[2].length === 11) {
return "https://img.youtube.com/vi/" + match[2] + "/hqdefault.jpg";
}
}
}
return "";
}
function loadArtwork(url) { function loadArtwork(url) {
if (!url || url === "") { if (!url || url === "") {
_bgArtSource = ""; resolvedArtUrl = "";
_lastArtUrl = ""; _lastArtUrl = "";
loading = false; loading = false;
return; return;
@@ -25,25 +66,99 @@ Singleton {
_lastArtUrl = url; _lastArtUrl = url;
if (url.startsWith("http://") || url.startsWith("https://")) { if (url.startsWith("http://") || url.startsWith("https://")) {
_bgArtSource = url; loading = true;
resolvedArtUrl = ""; // Clear stale artwork immediately while loading
const targetUrl = url;
const hash = djb2Hash(url);
const cacheDir = Paths.strip(Paths.imagecache);
const filePath = cacheDir + "/remote_" + hash;
const localFileUrl = "file://" + filePath;
// 1. First, check if the file already exists locally
Proc.runCommand(null, ["test", "-f", filePath], (output, exitCode) => {
if (_lastArtUrl !== targetUrl)
return;
if (exitCode === 0) {
resolvedArtUrl = localFileUrl;
loading = false; loading = false;
} else {
const dlCmd = "mkdir -p \"$(dirname \"$1\")\" && curl -f -s -L -o \"$1\" \"$2\" && mv \"$1\" \"$3\" || { rm -f \"$1\"; exit 1; }";
// 2. Check if this is a YouTube URL to do high quality 16:9 fallback
if (targetUrl.includes("img.youtube.com/vi/")) {
const videoId = targetUrl.split("/vi/")[1].split("/")[0];
const maxresUrl = "https://img.youtube.com/vi/" + videoId + "/maxresdefault.jpg";
const mqUrl = "https://img.youtube.com/vi/" + videoId + "/mqdefault.jpg";
const tmpPath = filePath + ".tmp";
Proc.runCommand(null, ["sh", "-c", dlCmd, "sh", tmpPath, maxresUrl, filePath], (maxOutput, maxExitCode) => {
if (_lastArtUrl !== targetUrl)
return;
if (maxExitCode === 0) {
resolvedArtUrl = localFileUrl;
loading = false;
} else {
Proc.runCommand(null, ["sh", "-c", dlCmd, "sh", tmpPath, mqUrl, filePath], (mqOutput, mqExitCode) => {
if (_lastArtUrl !== targetUrl)
return;
if (mqExitCode === 0) {
resolvedArtUrl = localFileUrl;
} else {
resolvedArtUrl = targetUrl; // Ultimate fallback
}
loading = false;
}, 50, 15000);
}
}, 50, 15000);
} else {
// Standard curl download for other remote URLs (e.g. SoundCloud)
const tmpPath = filePath + ".tmp";
Proc.runCommand(null, ["sh", "-c", dlCmd, "sh", tmpPath, targetUrl, filePath], (dlOutput, dlExitCode) => {
if (_lastArtUrl !== targetUrl)
return;
if (dlExitCode === 0) {
resolvedArtUrl = localFileUrl;
} else {
resolvedArtUrl = targetUrl; // Fallback to raw URL
}
loading = false;
}, 50, 15000);
}
}
}, 50, 5000);
return; return;
} }
loading = true; loading = true;
resolvedArtUrl = ""; // Clear stale artwork immediately while verifying local file
const localUrl = url; const localUrl = url;
const filePath = url.startsWith("file://") ? url.substring(7) : url; const filePath = url.startsWith("file://") ? url.substring(7) : url;
Proc.runCommand("trackart", ["test", "-f", filePath], (output, exitCode) => { Proc.runCommand(null, ["test", "-f", filePath], (output, exitCode) => {
if (_lastArtUrl !== localUrl) if (_lastArtUrl !== localUrl)
return; return;
_bgArtSource = exitCode === 0 ? localUrl : ""; resolvedArtUrl = exitCode === 0 ? localUrl : "";
loading = false; loading = false;
}, 200); }, 200);
} }
property MprisPlayer activePlayer: MprisController.activePlayer property MprisPlayer activePlayer: MprisController.activePlayer
onActivePlayerChanged: { onActivePlayerChanged: _updateArtUrl()
loadArtwork(activePlayer?.trackArtUrl ?? "");
Connections {
target: root.activePlayer
ignoreUnknownSignals: true
function onTrackTitleChanged() { root._updateArtUrl(); }
function onTrackArtUrlChanged() { root._updateArtUrl(); }
function onMetadataChanged() { root._updateArtUrl(); }
}
function _updateArtUrl() {
const url = getArtworkUrl(activePlayer);
loadArtwork(url);
} }
} }
+6 -2
View File
@@ -8,15 +8,19 @@ Item {
id: root id: root
property MprisPlayer activePlayer property MprisPlayer activePlayer
property string artUrl: (activePlayer?.trackArtUrl) || "" property string artUrl: TrackArtService.resolvedArtUrl
property string lastValidArtUrl: "" property string lastValidArtUrl: ""
property alias albumArtStatus: albumArt.imageStatus property alias albumArtStatus: albumArt.imageStatus
property real albumSize: Math.min(width, height) * 0.88 property real albumSize: Math.min(width, height) * 0.88
property bool showAnimation: true property bool showAnimation: true
property real animationScale: 1.0 property real animationScale: 1.0
onActivePlayerChanged: {
lastValidArtUrl = "";
}
onArtUrlChanged: { onArtUrlChanged: {
if (artUrl && albumArt.status !== Image.Error) { if (artUrl && albumArtStatus !== Image.Error) {
lastValidArtUrl = artUrl; lastValidArtUrl = artUrl;
} }
} }
+19 -17
View File
@@ -8,12 +8,14 @@ Item {
id: root id: root
property MprisPlayer activePlayer property MprisPlayer activePlayer
readonly property real stableLength: MprisController.activePlayerStableLength
property real seekPreviewRatio: -1 property real seekPreviewRatio: -1
readonly property real playerValue: { readonly property real playerValue: {
if (!activePlayer || activePlayer.length <= 0) if (!activePlayer || stableLength <= 0)
return 0; return 0;
const pos = (activePlayer.position || 0) % Math.max(1, activePlayer.length); const pos = (activePlayer.position || 0) % Math.max(1, stableLength);
const calculatedRatio = pos / activePlayer.length; const calculatedRatio = pos / stableLength;
return Math.max(0, Math.min(1, calculatedRatio)); return Math.max(0, Math.min(1, calculatedRatio));
} }
property real value: seekPreviewRatio >= 0 ? seekPreviewRatio : playerValue property real value: seekPreviewRatio >= 0 ? seekPreviewRatio : playerValue
@@ -29,20 +31,20 @@ Item {
} }
function ratioForPosition(position) { function ratioForPosition(position) {
if (!activePlayer || activePlayer.length <= 0) if (!activePlayer || stableLength <= 0)
return 0; return 0;
return clampRatio(position / activePlayer.length); return clampRatio(position / stableLength);
} }
function positionForRatio(ratio) { function positionForRatio(ratio) {
if (!activePlayer || activePlayer.length <= 0) if (!activePlayer || stableLength <= 0)
return 0; return 0;
const rawPosition = clampRatio(ratio) * activePlayer.length; const rawPosition = clampRatio(ratio) * stableLength;
return Math.min(rawPosition, activePlayer.length * 0.99); return Math.min(rawPosition, stableLength * 0.99);
} }
function updatePreviewFromMouse(mouseX, width) { function updatePreviewFromMouse(mouseX, width) {
if (!activePlayer || activePlayer.length <= 0 || width <= 0) if (!activePlayer || stableLength <= 0 || width <= 0)
return; return;
seekPreviewRatio = clampRatio(mouseX / width); seekPreviewRatio = clampRatio(mouseX / width);
} }
@@ -68,7 +70,7 @@ Item {
mouseArea.pressX = mouse.x; mouseArea.pressX = mouse.x;
clearCommittedSeekPreview(); clearCommittedSeekPreview();
holdTimer.restart(); holdTimer.restart();
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) { if (activePlayer && stableLength > 0 && activePlayer.canSeek) {
updatePreviewFromMouse(mouse.x, width); updatePreviewFromMouse(mouse.x, width);
mouseArea.pendingSeekPosition = positionForRatio(seekPreviewRatio); mouseArea.pendingSeekPosition = positionForRatio(seekPreviewRatio);
} }
@@ -78,9 +80,9 @@ Item {
holdTimer.stop(); holdTimer.stop();
isSeeking = false; isSeeking = false;
isDraggingSeek = false; isDraggingSeek = false;
if (mouseArea.pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) { if (mouseArea.pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && stableLength > 0) {
const clamped = Math.min(mouseArea.pendingSeekPosition, activePlayer.length * 0.99); const clamped = Math.min(mouseArea.pendingSeekPosition, stableLength * 0.99);
activePlayer.position = clamped; activePlayer.position = Math.max(0.1, clamped);
mouseArea.pendingSeekPosition = -1; mouseArea.pendingSeekPosition = -1;
beginCommittedSeekPreview(clamped); beginCommittedSeekPreview(clamped);
} else { } else {
@@ -89,7 +91,7 @@ Item {
} }
function handleSeekPositionChanged(mouse, width, mouseArea) { function handleSeekPositionChanged(mouse, width, mouseArea) {
if (mouseArea.pressed && isSeeking && activePlayer && activePlayer.length > 0 && activePlayer.canSeek) { if (mouseArea.pressed && isSeeking && activePlayer && stableLength > 0 && activePlayer.canSeek) {
if (!isDraggingSeek && Math.abs(mouse.x - mouseArea.pressX) >= dragThreshold) if (!isDraggingSeek && Math.abs(mouse.x - mouseArea.pressX) >= dragThreshold)
isDraggingSeek = true; isDraggingSeek = true;
updatePreviewFromMouse(mouse.x, width); updatePreviewFromMouse(mouse.x, width);
@@ -129,7 +131,7 @@ Item {
Loader { Loader {
anchors.fill: parent anchors.fill: parent
visible: activePlayer && activePlayer.length > 0 visible: activePlayer && stableLength > 0
sourceComponent: SettingsData.waveProgressEnabled ? waveProgressComponent : flatProgressComponent sourceComponent: SettingsData.waveProgressEnabled ? waveProgressComponent : flatProgressComponent
z: 1 z: 1
@@ -148,7 +150,7 @@ Item {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
enabled: activePlayer && activePlayer.canSeek && activePlayer.length > 0 enabled: activePlayer && activePlayer.canSeek && stableLength > 0
property real pendingSeekPosition: -1 property real pendingSeekPosition: -1
property real pressX: 0 property real pressX: 0
@@ -236,7 +238,7 @@ Item {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
enabled: activePlayer && activePlayer.canSeek && activePlayer.length > 0 enabled: activePlayer && activePlayer.canSeek && stableLength > 0
property real pendingSeekPosition: -1 property real pendingSeekPosition: -1
property real pressX: 0 property real pressX: 0