mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-24 21:42:51 -05:00
media player: replace color quantizer with album art
This commit is contained in:
@@ -51,17 +51,9 @@ Item {
|
|||||||
readonly property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported && !__isChromeBrowser
|
readonly property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported && !__isChromeBrowser
|
||||||
readonly property real currentVolume: usePlayerVolume ? activePlayer.volume : (AudioService.sink?.audio?.volume ?? 0)
|
readonly property real currentVolume: usePlayerVolume ? activePlayer.volume : (AudioService.sink?.audio?.volume ?? 0)
|
||||||
|
|
||||||
// Palette that stays stable across track switches until new colors are ready
|
|
||||||
property color dom: Qt.rgba(Theme.surface.r, Theme.surface.g, Theme.surface.b, 1.0)
|
|
||||||
property color acc: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.25)
|
|
||||||
property color _nextDom: dom
|
|
||||||
property color _nextAcc: acc
|
|
||||||
|
|
||||||
// Track-switch hold (prevents banner flicker only during switches)
|
|
||||||
property bool isSwitching: false
|
property bool isSwitching: false
|
||||||
property bool paletteReady: false
|
|
||||||
property string _lastArtUrl: ""
|
property string _lastArtUrl: ""
|
||||||
property url _cqSource: ""
|
property string _bgArtSource: ""
|
||||||
|
|
||||||
// Derived "no players" state: always correct, no timers.
|
// Derived "no players" state: always correct, no timers.
|
||||||
readonly property int _playerCount: allPlayers ? allPlayers.length : 0
|
readonly property int _playerCount: allPlayers ? allPlayers.length : 0
|
||||||
@@ -69,7 +61,6 @@ Item {
|
|||||||
readonly property bool _trulyIdle: activePlayer && activePlayer.playbackState === MprisPlaybackState.Stopped && !activePlayer.trackTitle && !activePlayer.trackArtist
|
readonly property bool _trulyIdle: activePlayer && activePlayer.playbackState === MprisPlaybackState.Stopped && !activePlayer.trackTitle && !activePlayer.trackArtist
|
||||||
readonly property bool showNoPlayerNow: (!_switchHold) && (_noneAvailable || _trulyIdle)
|
readonly property bool showNoPlayerNow: (!_switchHold) && (_noneAvailable || _trulyIdle)
|
||||||
|
|
||||||
// Short hold only during track switches (not when players disappear)
|
|
||||||
property bool _switchHold: false
|
property bool _switchHold: false
|
||||||
Timer {
|
Timer {
|
||||||
id: _switchHoldTimer
|
id: _switchHoldTimer
|
||||||
@@ -86,11 +77,9 @@ Item {
|
|||||||
}
|
}
|
||||||
isSwitching = true;
|
isSwitching = true;
|
||||||
_switchHold = true;
|
_switchHold = true;
|
||||||
paletteReady = false;
|
|
||||||
_switchHoldTimer.restart();
|
_switchHoldTimer.restart();
|
||||||
if (activePlayer.trackArtUrl) {
|
if (activePlayer.trackArtUrl)
|
||||||
loadArtwork(activePlayer.trackArtUrl);
|
loadArtwork(activePlayer.trackArtUrl);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
property string activeTrackArtFile: ""
|
property string activeTrackArtFile: ""
|
||||||
@@ -108,13 +97,13 @@ Item {
|
|||||||
imageDownloader.command = ["curl", "-L", "-s", "--user-agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36", "-o", filename, url];
|
imageDownloader.command = ["curl", "-L", "-s", "--user-agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36", "-o", filename, url];
|
||||||
imageDownloader.targetFile = filename;
|
imageDownloader.targetFile = filename;
|
||||||
imageDownloader.running = true;
|
imageDownloader.running = true;
|
||||||
} else {
|
return;
|
||||||
_preloadImage.source = url;
|
|
||||||
}
|
}
|
||||||
|
_bgArtSource = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
function maybeFinishSwitch() {
|
function maybeFinishSwitch() {
|
||||||
if (activePlayer && activePlayer.trackTitle !== "" && paletteReady) {
|
if (activePlayer && activePlayer.trackTitle !== "") {
|
||||||
isSwitching = false;
|
isSwitching = false;
|
||||||
_switchHold = false;
|
_switchHold = false;
|
||||||
}
|
}
|
||||||
@@ -219,9 +208,8 @@ Item {
|
|||||||
property string targetFile: ""
|
property string targetFile: ""
|
||||||
|
|
||||||
onExited: exitCode => {
|
onExited: exitCode => {
|
||||||
if (exitCode === 0 && targetFile) {
|
if (exitCode === 0 && targetFile)
|
||||||
_preloadImage.source = "file://" + targetFile;
|
_bgArtSource = "file://" + targetFile;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,121 +218,70 @@ Item {
|
|||||||
running: false
|
running: false
|
||||||
}
|
}
|
||||||
|
|
||||||
Image {
|
|
||||||
id: _preloadImage
|
|
||||||
source: ""
|
|
||||||
asynchronous: true
|
|
||||||
cache: true
|
|
||||||
visible: false
|
|
||||||
onStatusChanged: {
|
|
||||||
if (status === Image.Ready) {
|
|
||||||
_cqSource = source;
|
|
||||||
colorQuantizer.source = _cqSource;
|
|
||||||
} else if (status === Image.Error) {
|
|
||||||
_cqSource = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ColorQuantizer {
|
|
||||||
id: colorQuantizer
|
|
||||||
source: _cqSource !== "" ? _cqSource : undefined
|
|
||||||
depth: 8
|
|
||||||
rescaleSize: 32
|
|
||||||
onColorsChanged: {
|
|
||||||
if (!colors || colors.length === 0)
|
|
||||||
return;
|
|
||||||
function enhanceColor(color) {
|
|
||||||
const satBoost = 1.4;
|
|
||||||
const valueBoost = 1.2;
|
|
||||||
return Qt.hsva(color.hsvHue, Math.min(1, color.hsvSaturation * satBoost), Math.min(1, color.hsvValue * valueBoost), color.a);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getExtremeColor(startIdx, direction = 1) {
|
|
||||||
let bestColor = colors[startIdx];
|
|
||||||
let bestScore = 0;
|
|
||||||
|
|
||||||
for (let i = startIdx; i >= 0 && i < colors.length; i += direction) {
|
|
||||||
const c = colors[i];
|
|
||||||
const saturation = c.hsvSaturation;
|
|
||||||
const brightness = c.hsvValue;
|
|
||||||
const contrast = Math.abs(brightness - 0.5) * 2;
|
|
||||||
const score = saturation * 0.7 + contrast * 0.3;
|
|
||||||
|
|
||||||
if (score > bestScore) {
|
|
||||||
bestScore = score;
|
|
||||||
bestColor = c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return enhanceColor(bestColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
_pendingDom = getExtremeColor(Math.floor(colors.length * 0.2), 1);
|
|
||||||
_pendingAcc = getExtremeColor(Math.floor(colors.length * 0.8), -1);
|
|
||||||
paletteApplyDelay.restart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
property color _pendingDom: dom
|
|
||||||
property color _pendingAcc: acc
|
|
||||||
Timer {
|
|
||||||
id: paletteApplyDelay
|
|
||||||
interval: 90
|
|
||||||
repeat: false
|
|
||||||
onTriggered: {
|
|
||||||
const dist = (c1, c2) => {
|
|
||||||
const dr = c1.r - c2.r, dg = c1.g - c2.g, db = c1.b - c2.b;
|
|
||||||
return Math.sqrt(dr * dr + dg * dg + db * db);
|
|
||||||
};
|
|
||||||
const domChanged = dist(_pendingDom, dom) > 0.02;
|
|
||||||
const accChanged = dist(_pendingAcc, acc) > 0.02;
|
|
||||||
if (domChanged || accChanged) {
|
|
||||||
dom = _pendingDom;
|
|
||||||
acc = _pendingAcc;
|
|
||||||
}
|
|
||||||
paletteReady = true;
|
|
||||||
maybeFinishSwitch();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
property bool isSeeking: false
|
property bool isSeeking: false
|
||||||
|
|
||||||
Rectangle {
|
Item {
|
||||||
|
id: bgContainer
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
radius: Theme.cornerRadius
|
visible: _bgArtSource !== ""
|
||||||
opacity: 1.0
|
|
||||||
gradient: Gradient {
|
|
||||||
GradientStop {
|
|
||||||
position: 0.0
|
|
||||||
color: Qt.rgba(dom.r, dom.g, dom.b, paletteReady ? 0.38 : 0.06)
|
|
||||||
}
|
|
||||||
GradientStop {
|
|
||||||
position: 0.3
|
|
||||||
color: Qt.rgba(acc.r, acc.g, acc.b, paletteReady ? 0.28 : 0.05)
|
|
||||||
}
|
|
||||||
GradientStop {
|
|
||||||
position: 1.0
|
|
||||||
color: Qt.rgba(Theme.surface.r, Theme.surface.g, Theme.surface.b, paletteReady ? 0.92 : 0.985)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 160
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on dom {
|
Image {
|
||||||
ColorAnimation {
|
id: bgImage
|
||||||
duration: 220
|
anchors.centerIn: parent
|
||||||
easing.type: Easing.InOutQuad
|
width: Math.max(parent.width, parent.height) * 1.1
|
||||||
|
height: width
|
||||||
|
source: _bgArtSource
|
||||||
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
asynchronous: true
|
||||||
|
cache: true
|
||||||
|
visible: false
|
||||||
|
onStatusChanged: {
|
||||||
|
if (status === Image.Ready)
|
||||||
|
maybeFinishSwitch();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Behavior on acc {
|
Item {
|
||||||
ColorAnimation {
|
id: blurredBg
|
||||||
duration: 220
|
anchors.fill: parent
|
||||||
easing.type: Easing.InOutQuad
|
visible: false
|
||||||
|
|
||||||
|
MultiEffect {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: bgImage.width
|
||||||
|
height: bgImage.height
|
||||||
|
source: bgImage
|
||||||
|
blurEnabled: true
|
||||||
|
blurMax: 64
|
||||||
|
blur: 0.8
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,12 +17,19 @@ Item {
|
|||||||
property color playheadColor: Theme.primary
|
property color playheadColor: Theme.primary
|
||||||
|
|
||||||
property real dpr: (root.window ? root.window.devicePixelRatio : 1)
|
property real dpr: (root.window ? root.window.devicePixelRatio : 1)
|
||||||
function snap(v) { return Math.round(v * dpr) / dpr }
|
function snap(v) {
|
||||||
|
return Math.round(v * dpr) / dpr;
|
||||||
|
}
|
||||||
|
|
||||||
readonly property real playX: snap(root.width * root.value)
|
readonly property real playX: snap(root.width * root.value)
|
||||||
readonly property real midY: snap(height / 2)
|
readonly property real midY: snap(height / 2)
|
||||||
|
|
||||||
Behavior on currentAmp { NumberAnimation { duration: 300; easing.type: Easing.OutCubic } }
|
Behavior on currentAmp {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 300
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
onIsPlayingChanged: currentAmp = isPlaying ? amp : 0
|
onIsPlayingChanged: currentAmp = isPlaying ? amp : 0
|
||||||
|
|
||||||
Shape {
|
Shape {
|
||||||
@@ -38,8 +45,16 @@ Item {
|
|||||||
capStyle: ShapePath.RoundCap
|
capStyle: ShapePath.RoundCap
|
||||||
joinStyle: ShapePath.RoundJoin
|
joinStyle: ShapePath.RoundJoin
|
||||||
fillColor: "transparent"
|
fillColor: "transparent"
|
||||||
PathMove { id: flatStart; x: 0; y: root.midY }
|
PathMove {
|
||||||
PathLine { id: flatEnd; x: root.width; y: root.midY }
|
id: flatStart
|
||||||
|
x: Math.min(root.width, snap(root.playX + playhead.width / 2))
|
||||||
|
y: root.midY
|
||||||
|
}
|
||||||
|
PathLine {
|
||||||
|
id: flatEnd
|
||||||
|
x: root.width
|
||||||
|
y: root.midY
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,7 +63,7 @@ Item {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
readonly property real startX: snap(root.lineWidth/2)
|
readonly property real startX: snap(root.lineWidth / 2)
|
||||||
readonly property real aaBias: (0.25 / root.dpr)
|
readonly property real aaBias: (0.25 / root.dpr)
|
||||||
readonly property real endX: Math.max(startX, Math.min(root.playX - startX - aaBias, width))
|
readonly property real endX: Math.max(startX, Math.min(root.playX - startX - aaBias, width))
|
||||||
|
|
||||||
@@ -77,7 +92,10 @@ Item {
|
|||||||
capStyle: ShapePath.RoundCap
|
capStyle: ShapePath.RoundCap
|
||||||
joinStyle: ShapePath.RoundJoin
|
joinStyle: ShapePath.RoundJoin
|
||||||
fillColor: "transparent"
|
fillColor: "transparent"
|
||||||
PathSvg { id: waveSvg; path: "" }
|
PathSvg {
|
||||||
|
id: waveSvg
|
||||||
|
path: ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,8 +106,8 @@ Item {
|
|||||||
height: snap(root.lineWidth)
|
height: snap(root.lineWidth)
|
||||||
radius: width / 2
|
radius: width / 2
|
||||||
color: root.fillColor
|
color: root.fillColor
|
||||||
x: waveClip.startX - width/2
|
x: waveClip.startX - width / 2
|
||||||
y: root.midY - height/2 + root.currentAmp * Math.sin((waveClip.startX / root.wavelength) * 2 * Math.PI + root.phase)
|
y: root.midY - height / 2 + root.currentAmp * Math.sin((waveClip.startX / root.wavelength) * 2 * Math.PI + root.phase)
|
||||||
visible: waveClip.endX > waveClip.startX
|
visible: waveClip.endX > waveClip.startX
|
||||||
z: 2
|
z: 2
|
||||||
}
|
}
|
||||||
@@ -100,8 +118,8 @@ Item {
|
|||||||
height: snap(root.lineWidth)
|
height: snap(root.lineWidth)
|
||||||
radius: width / 2
|
radius: width / 2
|
||||||
color: root.fillColor
|
color: root.fillColor
|
||||||
x: waveClip.endX - width/2
|
x: waveClip.endX - width / 2
|
||||||
y: root.midY - height/2 + root.currentAmp * Math.sin((waveClip.endX / root.wavelength) * 2 * Math.PI + root.phase)
|
y: root.midY - height / 2 + root.currentAmp * Math.sin((waveClip.endX / root.wavelength) * 2 * Math.PI + root.phase)
|
||||||
visible: waveClip.endX > waveClip.startX
|
visible: waveClip.endX > waveClip.startX
|
||||||
z: 2
|
z: 2
|
||||||
}
|
}
|
||||||
@@ -119,48 +137,68 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
property real k: (2 * Math.PI) / Math.max(1e-6, wavelength)
|
property real k: (2 * Math.PI) / Math.max(1e-6, wavelength)
|
||||||
function wrapMod(a, m) { let r = a % m; return r < 0 ? r + m : r }
|
function wrapMod(a, m) {
|
||||||
|
let r = a % m;
|
||||||
|
return r < 0 ? r + m : r;
|
||||||
|
}
|
||||||
readonly property real waveOffsetX: -wrapMod(phase / k, wavelength)
|
readonly property real waveOffsetX: -wrapMod(phase / k, wavelength)
|
||||||
|
|
||||||
FrameAnimation {
|
FrameAnimation {
|
||||||
running: root.visible && (root.isPlaying || root.currentAmp > 0)
|
running: root.visible && (root.isPlaying || root.currentAmp > 0)
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
if (root.isPlaying) root.phase += 0.03 * frameTime * 60
|
if (root.isPlaying)
|
||||||
startCap.y = root.midY - startCap.height/2 + root.currentAmp * Math.sin((waveClip.startX / root.wavelength) * 2 * Math.PI + root.phase)
|
root.phase += 0.03 * frameTime * 60;
|
||||||
endCap.y = root.midY - endCap.height/2 + root.currentAmp * Math.sin((waveClip.endX / root.wavelength) * 2 * Math.PI + root.phase)
|
startCap.y = root.midY - startCap.height / 2 + root.currentAmp * Math.sin((waveClip.startX / root.wavelength) * 2 * Math.PI + root.phase);
|
||||||
|
endCap.y = root.midY - endCap.height / 2 + root.currentAmp * Math.sin((waveClip.endX / root.wavelength) * 2 * Math.PI + root.phase);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildStaticWave() {
|
function buildStaticWave() {
|
||||||
const start = waveClip.startX - 2 * root.wavelength
|
const start = waveClip.startX - 2 * root.wavelength;
|
||||||
const end = width + 2 * root.wavelength
|
const end = width + 2 * root.wavelength;
|
||||||
if (end <= start) { waveSvg.path = ""; return }
|
if (end <= start) {
|
||||||
|
waveSvg.path = "";
|
||||||
const kLocal = k
|
return;
|
||||||
const halfPeriod = root.wavelength / 2
|
|
||||||
function y0(x) { return root.midY + root.currentAmp * Math.sin(kLocal * x) }
|
|
||||||
function dy0(x) { return root.currentAmp * Math.cos(kLocal * x) * kLocal }
|
|
||||||
|
|
||||||
let x0 = start
|
|
||||||
let d = `M ${x0} ${y0(x0)}`
|
|
||||||
while (x0 < end) {
|
|
||||||
const x1 = Math.min(x0 + halfPeriod, end)
|
|
||||||
const dx = x1 - x0
|
|
||||||
const yA = y0(x0), yB = y0(x1)
|
|
||||||
const dyA = dy0(x0), dyB = dy0(x1)
|
|
||||||
const c1x = x0 + dx/3
|
|
||||||
const c1y = yA + (dyA * dx)/3
|
|
||||||
const c2x = x1 - dx/3
|
|
||||||
const c2y = yB - (dyB * dx)/3
|
|
||||||
d += ` C ${c1x} ${c1y} ${c2x} ${c2y} ${x1} ${yB}`
|
|
||||||
x0 = x1
|
|
||||||
}
|
}
|
||||||
waveSvg.path = d
|
|
||||||
|
const kLocal = k;
|
||||||
|
const halfPeriod = root.wavelength / 2;
|
||||||
|
function y0(x) {
|
||||||
|
return root.midY + root.currentAmp * Math.sin(kLocal * x);
|
||||||
|
}
|
||||||
|
function dy0(x) {
|
||||||
|
return root.currentAmp * Math.cos(kLocal * x) * kLocal;
|
||||||
|
}
|
||||||
|
|
||||||
|
let x0 = start;
|
||||||
|
let d = `M ${x0} ${y0(x0)}`;
|
||||||
|
while (x0 < end) {
|
||||||
|
const x1 = Math.min(x0 + halfPeriod, end);
|
||||||
|
const dx = x1 - x0;
|
||||||
|
const yA = y0(x0), yB = y0(x1);
|
||||||
|
const dyA = dy0(x0), dyB = dy0(x1);
|
||||||
|
const c1x = x0 + dx / 3;
|
||||||
|
const c1y = yA + (dyA * dx) / 3;
|
||||||
|
const c2x = x1 - dx / 3;
|
||||||
|
const c2y = yB - (dyB * dx) / 3;
|
||||||
|
d += ` C ${c1x} ${c1y} ${c2x} ${c2y} ${x1} ${yB}`;
|
||||||
|
x0 = x1;
|
||||||
|
}
|
||||||
|
waveSvg.path = d;
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: { currentAmp = isPlaying ? amp : 0; buildStaticWave() }
|
Component.onCompleted: {
|
||||||
onWidthChanged: { flatStart.x = 0; flatEnd.x = width; buildStaticWave() }
|
currentAmp = isPlaying ? amp : 0;
|
||||||
|
buildStaticWave();
|
||||||
|
}
|
||||||
|
onWidthChanged: {
|
||||||
|
flatEnd.x = width;
|
||||||
|
buildStaticWave();
|
||||||
|
}
|
||||||
onHeightChanged: buildStaticWave()
|
onHeightChanged: buildStaticWave()
|
||||||
onCurrentAmpChanged: buildStaticWave()
|
onCurrentAmpChanged: buildStaticWave()
|
||||||
onWavelengthChanged: { k = (2 * Math.PI) / Math.max(1e-6, wavelength); buildStaticWave() }
|
onWavelengthChanged: {
|
||||||
|
k = (2 * Math.PI) / Math.max(1e-6, wavelength);
|
||||||
|
buildStaticWave();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user