mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-01 19:18:28 -04:00
media: improve album art handling
This commit is contained in:
@@ -294,7 +294,19 @@ Item {
|
|||||||
id: bgContainer
|
id: bgContainer
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
readonly property string curArt: TrackArtService.resolvedArtUrl
|
// Fall back to the live mpris url so the background is never blank.
|
||||||
|
readonly property string curArt: {
|
||||||
|
const resolved = TrackArtService.resolvedArtUrl;
|
||||||
|
if (resolved !== "")
|
||||||
|
return resolved;
|
||||||
|
const p = root.activePlayer;
|
||||||
|
if (!p)
|
||||||
|
return "";
|
||||||
|
if (p.trackArtUrl)
|
||||||
|
return p.trackArtUrl;
|
||||||
|
const m = p.metadata;
|
||||||
|
return m && m["mpris:artUrl"] ? m["mpris:artUrl"].toString() : "";
|
||||||
|
}
|
||||||
// Two layers crossfade: new art loads into the hidden one and fades in once decoded.
|
// Two layers crossfade: new art loads into the hidden one and fades in once decoded.
|
||||||
property bool _showA: true
|
property bool _showA: true
|
||||||
visible: layerA.ready || layerB.ready
|
visible: layerA.ready || layerB.ready
|
||||||
@@ -305,18 +317,29 @@ Item {
|
|||||||
function syncArt() {
|
function syncArt() {
|
||||||
if (curArt === "")
|
if (curArt === "")
|
||||||
return;
|
return;
|
||||||
const frontArt = _showA ? layerA.art : layerB.art;
|
const front = _showA ? layerA : layerB;
|
||||||
const backArt = _showA ? layerB.art : layerA.art;
|
const back = _showA ? layerB : layerA;
|
||||||
if (frontArt == curArt)
|
if (front.art == curArt)
|
||||||
return;
|
return;
|
||||||
if (backArt == curArt) {
|
if (back.art == curArt) {
|
||||||
_showA = !_showA;
|
// Already decoded in the hidden layer; flip once ready (else promote() does).
|
||||||
|
if (back.ready)
|
||||||
|
_showA = !_showA;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (_showA)
|
back.art = curArt;
|
||||||
layerB.art = curArt;
|
}
|
||||||
else
|
|
||||||
layerA.art = curArt;
|
// Flip to the hidden layer only when it holds the current art, ignoring stale
|
||||||
|
// Ready re-emits (e.g. popout re-expose) that would otherwise ping-pong _showA.
|
||||||
|
function promote(layer) {
|
||||||
|
const back = _showA ? layerB : layerA;
|
||||||
|
if (layer !== back)
|
||||||
|
return;
|
||||||
|
if (layer.art != curArt)
|
||||||
|
return;
|
||||||
|
_showA = (layer === layerA);
|
||||||
|
root.maybeFinishSwitch();
|
||||||
}
|
}
|
||||||
|
|
||||||
component BgBlurLayer: ClippingRectangle {
|
component BgBlurLayer: ClippingRectangle {
|
||||||
@@ -370,23 +393,13 @@ Item {
|
|||||||
BgBlurLayer {
|
BgBlurLayer {
|
||||||
id: layerA
|
id: layerA
|
||||||
front: bgContainer._showA
|
front: bgContainer._showA
|
||||||
onLoaded: {
|
onLoaded: bgContainer.promote(layerA)
|
||||||
if (!bgContainer._showA) {
|
|
||||||
bgContainer._showA = true;
|
|
||||||
root.maybeFinishSwitch();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BgBlurLayer {
|
BgBlurLayer {
|
||||||
id: layerB
|
id: layerB
|
||||||
front: !bgContainer._showA
|
front: !bgContainer._showA
|
||||||
onLoaded: {
|
onLoaded: bgContainer.promote(layerB)
|
||||||
if (bgContainer._showA) {
|
|
||||||
bgContainer._showA = false;
|
|
||||||
root.maybeFinishSwitch();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
|||||||
@@ -26,13 +26,9 @@ Singleton {
|
|||||||
readonly property color accentTrack: Theme.withAlpha(accent, 0.28)
|
readonly property color accentTrack: Theme.withAlpha(accent, 0.28)
|
||||||
readonly property color accentSubtle: Theme.withAlpha(accent, 0.55)
|
readonly property color accentSubtle: Theme.withAlpha(accent, 0.55)
|
||||||
|
|
||||||
// Plain-named alias: underscore-prefixed props with onChanged handlers crash config load.
|
|
||||||
readonly property string artUrl: TrackArtService.resolvedArtUrl
|
readonly property string artUrl: TrackArtService.resolvedArtUrl
|
||||||
onArtUrlChanged: {
|
|
||||||
if (artUrl === "")
|
|
||||||
_accent = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Hold the last accent across the brief artUrl blank between tracks; never reset to primary.
|
||||||
property var _accent: null
|
property var _accent: null
|
||||||
|
|
||||||
ColorQuantizer {
|
ColorQuantizer {
|
||||||
@@ -40,7 +36,11 @@ Singleton {
|
|||||||
source: root.artUrl
|
source: root.artUrl
|
||||||
depth: 4
|
depth: 4
|
||||||
rescaleSize: 64
|
rescaleSize: 64
|
||||||
onColorsChanged: root._accent = root._pickAccent(colors)
|
onColorsChanged: {
|
||||||
|
const a = root._pickAccent(colors);
|
||||||
|
if (a !== null)
|
||||||
|
root._accent = a;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _pickAccent(colors) {
|
function _pickAccent(colors) {
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ Singleton {
|
|||||||
property string resolvedArtUrl: ""
|
property string resolvedArtUrl: ""
|
||||||
property alias _bgArtSource: root.resolvedArtUrl
|
property alias _bgArtSource: root.resolvedArtUrl
|
||||||
property bool loading: false
|
property bool loading: false
|
||||||
|
// sha1s of placeholder art to reject (Chrome's own logo, shown before real cover).
|
||||||
|
readonly property var _artHashDenylist: ["764a730860c5b8a7bbee690ee5a443672ae37dc8"]
|
||||||
|
|
||||||
function djb2Hash(str) {
|
function djb2Hash(str) {
|
||||||
if (!str) return "";
|
if (!str) return "";
|
||||||
@@ -54,6 +56,11 @@ Singleton {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _commit(u) {
|
||||||
|
resolvedArtUrl = u;
|
||||||
|
_committedArtKey = u !== "" ? _pendingArtKey : "";
|
||||||
|
}
|
||||||
|
|
||||||
function loadArtwork(url) {
|
function loadArtwork(url) {
|
||||||
if (!url || url === "") {
|
if (!url || url === "") {
|
||||||
// Keep stale art; only blank once the empty url debounce settles.
|
// Keep stale art; only blank once the empty url debounce settles.
|
||||||
@@ -81,7 +88,7 @@ Singleton {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
if (exitCode === 0) {
|
if (exitCode === 0) {
|
||||||
resolvedArtUrl = localFileUrl;
|
_commit(localFileUrl);
|
||||||
loading = false;
|
loading = false;
|
||||||
} else {
|
} else {
|
||||||
const dlCmd = "mkdir -p \"$(dirname \"$1\")\" && curl -f -s -L -o \"$1\" \"$2\" && mv \"$1\" \"$3\" || { rm -f \"$1\"; exit 1; }";
|
const dlCmd = "mkdir -p \"$(dirname \"$1\")\" && curl -f -s -L -o \"$1\" \"$2\" && mv \"$1\" \"$3\" || { rm -f \"$1\"; exit 1; }";
|
||||||
@@ -98,18 +105,14 @@ Singleton {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
if (maxExitCode === 0) {
|
if (maxExitCode === 0) {
|
||||||
resolvedArtUrl = localFileUrl;
|
_commit(localFileUrl);
|
||||||
loading = false;
|
loading = false;
|
||||||
} else {
|
} else {
|
||||||
Proc.runCommand(null, ["sh", "-c", dlCmd, "sh", tmpPath, mqUrl, filePath], (mqOutput, mqExitCode) => {
|
Proc.runCommand(null, ["sh", "-c", dlCmd, "sh", tmpPath, mqUrl, filePath], (mqOutput, mqExitCode) => {
|
||||||
if (_lastArtUrl !== targetUrl)
|
if (_lastArtUrl !== targetUrl)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (mqExitCode === 0) {
|
_commit(mqExitCode === 0 ? localFileUrl : targetUrl);
|
||||||
resolvedArtUrl = localFileUrl;
|
|
||||||
} else {
|
|
||||||
resolvedArtUrl = targetUrl; // Ultimate fallback
|
|
||||||
}
|
|
||||||
loading = false;
|
loading = false;
|
||||||
}, 50, 15000);
|
}, 50, 15000);
|
||||||
}
|
}
|
||||||
@@ -121,11 +124,7 @@ Singleton {
|
|||||||
if (_lastArtUrl !== targetUrl)
|
if (_lastArtUrl !== targetUrl)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (dlExitCode === 0) {
|
_commit(dlExitCode === 0 ? localFileUrl : targetUrl);
|
||||||
resolvedArtUrl = localFileUrl;
|
|
||||||
} else {
|
|
||||||
resolvedArtUrl = targetUrl; // Fallback to raw URL
|
|
||||||
}
|
|
||||||
loading = false;
|
loading = false;
|
||||||
}, 50, 15000);
|
}, 50, 15000);
|
||||||
}
|
}
|
||||||
@@ -137,11 +136,22 @@ Singleton {
|
|||||||
loading = true;
|
loading = true;
|
||||||
const localUrl = url;
|
const localUrl = url;
|
||||||
const filePath = url.startsWith("file://") ? url.substring(7) : url;
|
const filePath = url.startsWith("file://") ? url.substring(7) : url;
|
||||||
// Cover file often lands after the metadata update, so poll briefly.
|
// Cover lands after metadata, so poll; hash only to reject placeholder art.
|
||||||
Proc.runCommand(null, ["sh", "-c", "for i in $(seq 20); do [ -f \"$1\" ] && exit 0; sleep 0.15; done; exit 1", "sh", filePath], (output, exitCode) => {
|
const script = "f=\"$1\"; for i in $(seq 20); do [ -f \"$f\" ] && break; sleep 0.15; done; [ -f \"$f\" ] || exit 1; sha1sum \"$f\" | cut -c1-40";
|
||||||
|
Proc.runCommand(null, ["sh", "-c", script, "sh", filePath], (output, exitCode) => {
|
||||||
if (_lastArtUrl !== localUrl)
|
if (_lastArtUrl !== localUrl)
|
||||||
return;
|
return;
|
||||||
resolvedArtUrl = exitCode === 0 ? localUrl : "";
|
if (exitCode !== 0) {
|
||||||
|
// Keep current art rather than blanking (avoids an accent/art flash).
|
||||||
|
loading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Placeholder (Chrome logo): skip without committing so the real cover still resolves.
|
||||||
|
if (_artHashDenylist.indexOf((output || "").trim()) !== -1) {
|
||||||
|
loading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_commit(localUrl);
|
||||||
loading = false;
|
loading = false;
|
||||||
}, 50, 5000);
|
}, 50, 5000);
|
||||||
}
|
}
|
||||||
@@ -151,13 +161,14 @@ Singleton {
|
|||||||
interval: 800
|
interval: 800
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
if (root._lastArtUrl === "")
|
if (root._lastArtUrl === "")
|
||||||
root.resolvedArtUrl = "";
|
root._commit("");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
property MprisPlayer activePlayer: MprisController.activePlayer
|
property MprisPlayer activePlayer: MprisController.activePlayer
|
||||||
|
|
||||||
property string _resolvedTrackKey: ""
|
property string _committedArtKey: ""
|
||||||
|
property string _pendingArtKey: ""
|
||||||
|
|
||||||
onActivePlayerChanged: _updateArtUrl()
|
onActivePlayerChanged: _updateArtUrl()
|
||||||
|
|
||||||
@@ -173,17 +184,21 @@ Singleton {
|
|||||||
const p = activePlayer;
|
const p = activePlayer;
|
||||||
if (!p)
|
if (!p)
|
||||||
return "";
|
return "";
|
||||||
|
// Prefer the stable track id; title/artist/album fill in progressively (Chrome).
|
||||||
|
const tid = p.metadata && p.metadata["mpris:trackid"] ? p.metadata["mpris:trackid"].toString() : "";
|
||||||
|
if (tid !== "")
|
||||||
|
return tid;
|
||||||
return (p.trackTitle || "") + "" + (p.trackArtist || "") + "" + (p.trackAlbum || "");
|
return (p.trackTitle || "") + "" + (p.trackArtist || "") + "" + (p.trackAlbum || "");
|
||||||
}
|
}
|
||||||
|
|
||||||
function _updateArtUrl() {
|
function _updateArtUrl() {
|
||||||
const url = getArtworkUrl(activePlayer);
|
|
||||||
const key = _trackKey();
|
const key = _trackKey();
|
||||||
// Ignore metadata jitter once resolved, but only when the url still matches
|
// Skip once real art is committed for this track (dedup Chrome's multi-size
|
||||||
// (trackArtUrl can update before the title, and skipping then wedges old art).
|
// re-publish). The lock is set in _commit(), never optimistically, so a rejected
|
||||||
if (key !== "" && key === _resolvedTrackKey && url === _lastArtUrl && resolvedArtUrl !== "")
|
// placeholder or a short-circuited duplicate url can't wedge the real cover out.
|
||||||
|
if (key !== "" && key === _committedArtKey)
|
||||||
return;
|
return;
|
||||||
_resolvedTrackKey = key;
|
_pendingArtKey = key;
|
||||||
loadArtwork(url);
|
loadArtwork(getArtworkUrl(activePlayer));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,22 @@ Item {
|
|||||||
property MprisPlayer activePlayer
|
property MprisPlayer activePlayer
|
||||||
property string artUrl: TrackArtService.resolvedArtUrl
|
property string artUrl: TrackArtService.resolvedArtUrl
|
||||||
property string lastValidArtUrl: ""
|
property string lastValidArtUrl: ""
|
||||||
property alias albumArtStatus: albumArt.imageStatus
|
// Live mpris url — always valid for the current track; fallback so art is never blank.
|
||||||
|
readonly property string rawArtUrl: {
|
||||||
|
const p = activePlayer;
|
||||||
|
if (!p)
|
||||||
|
return "";
|
||||||
|
if (p.trackArtUrl)
|
||||||
|
return p.trackArtUrl;
|
||||||
|
const m = p.metadata;
|
||||||
|
return m && m["mpris:artUrl"] ? m["mpris:artUrl"].toString() : "";
|
||||||
|
}
|
||||||
|
readonly property string curArt: artUrl || lastValidArtUrl || rawArtUrl
|
||||||
|
property string _prevArt: ""
|
||||||
|
property bool _fadePending: false
|
||||||
|
property string _srcOverride: "" // forces the live url when the resolved one fails
|
||||||
|
readonly property string _mainSrc: _srcOverride !== "" ? _srcOverride : curArt
|
||||||
|
readonly property int albumArtStatus: mainArt.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
|
||||||
@@ -48,10 +63,49 @@ Item {
|
|||||||
lastValidArtUrl = "";
|
lastValidArtUrl = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
onArtUrlChanged: {
|
onCurArtChanged: {
|
||||||
if (artUrl && albumArtStatus !== Image.Error) {
|
_srcOverride = "";
|
||||||
lastValidArtUrl = artUrl;
|
// Keep the outgoing art covering mainArt until the new art decodes, then fade —
|
||||||
|
// hides mainArt's placeholder base so no primary circle flashes mid-load.
|
||||||
|
if (_prevArt !== "" && _prevArt !== curArt) {
|
||||||
|
fadeArt.imageSource = _prevArt;
|
||||||
|
fadeArt.opacity = 1;
|
||||||
|
_fadePending = true;
|
||||||
|
fadeSafety.restart();
|
||||||
|
Qt.callLater(_maybeStartFade); // catch cached (synchronous) loads
|
||||||
}
|
}
|
||||||
|
_prevArt = curArt;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _maybeStartFade() {
|
||||||
|
if (!_fadePending)
|
||||||
|
return;
|
||||||
|
if (mainArt.imageStatus !== Image.Ready && mainArt.imageStatus !== Image.Error)
|
||||||
|
return;
|
||||||
|
_fadePending = false;
|
||||||
|
fadeSafety.stop();
|
||||||
|
fadeOut.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: fadeSafety
|
||||||
|
interval: 1200
|
||||||
|
onTriggered: {
|
||||||
|
if (root._fadePending) {
|
||||||
|
root._fadePending = false;
|
||||||
|
fadeOut.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NumberAnimation {
|
||||||
|
id: fadeOut
|
||||||
|
target: fadeArt
|
||||||
|
property: "opacity"
|
||||||
|
from: 1
|
||||||
|
to: 0
|
||||||
|
duration: 300
|
||||||
|
easing.type: Easing.InOutQuad
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateBands() {
|
function updateBands() {
|
||||||
@@ -166,21 +220,36 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DankCircularImage {
|
DankCircularImage {
|
||||||
id: albumArt
|
id: mainArt
|
||||||
width: albumSize
|
width: albumSize
|
||||||
height: albumSize
|
height: albumSize
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
z: 1
|
z: 1
|
||||||
|
imageSource: root._mainSrc
|
||||||
imageSource: artUrl || lastValidArtUrl || ""
|
|
||||||
fallbackIcon: "album"
|
fallbackIcon: "album"
|
||||||
border.color: MediaAccentService.accent
|
border.color: MediaAccentService.accent
|
||||||
border.width: 2
|
border.width: 2
|
||||||
|
|
||||||
onImageSourceChanged: {
|
onImageStatusChanged: {
|
||||||
if (imageSource && imageStatus !== Image.Error) {
|
if (imageStatus === Image.Ready && imageSource !== "")
|
||||||
lastValidArtUrl = imageSource;
|
root.lastValidArtUrl = imageSource;
|
||||||
}
|
else if (imageStatus === Image.Error && root._srcOverride === "" && root.rawArtUrl !== "" && root.rawArtUrl !== imageSource)
|
||||||
|
root._srcOverride = root.rawArtUrl; // resolved url dead → use live mpris url
|
||||||
|
root._maybeStartFade();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Outgoing art, shown on top only while fading out over the new mainArt.
|
||||||
|
DankCircularImage {
|
||||||
|
id: fadeArt
|
||||||
|
width: albumSize
|
||||||
|
height: albumSize
|
||||||
|
anchors.centerIn: parent
|
||||||
|
z: 2
|
||||||
|
fallbackIcon: ""
|
||||||
|
border.color: MediaAccentService.accent
|
||||||
|
border.width: 2
|
||||||
|
opacity: 0
|
||||||
|
visible: opacity > 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user