mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-08 12:13:31 -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:
@@ -10,12 +10,53 @@ Singleton {
|
||||
id: root
|
||||
|
||||
property string _lastArtUrl: ""
|
||||
property string _bgArtSource: ""
|
||||
property string resolvedArtUrl: ""
|
||||
property alias _bgArtSource: root.resolvedArtUrl
|
||||
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) {
|
||||
if (!url || url === "") {
|
||||
_bgArtSource = "";
|
||||
resolvedArtUrl = "";
|
||||
_lastArtUrl = "";
|
||||
loading = false;
|
||||
return;
|
||||
@@ -25,25 +66,99 @@ Singleton {
|
||||
_lastArtUrl = url;
|
||||
|
||||
if (url.startsWith("http://") || url.startsWith("https://")) {
|
||||
_bgArtSource = url;
|
||||
loading = false;
|
||||
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;
|
||||
} 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;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
resolvedArtUrl = ""; // Clear stale artwork immediately while verifying local file
|
||||
const localUrl = 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)
|
||||
return;
|
||||
_bgArtSource = exitCode === 0 ? localUrl : "";
|
||||
resolvedArtUrl = exitCode === 0 ? localUrl : "";
|
||||
loading = false;
|
||||
}, 200);
|
||||
}
|
||||
|
||||
property MprisPlayer activePlayer: MprisController.activePlayer
|
||||
|
||||
onActivePlayerChanged: {
|
||||
loadArtwork(activePlayer?.trackArtUrl ?? "");
|
||||
onActivePlayerChanged: _updateArtUrl()
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user