Skip to content

Commit d28b942

Browse files
committed
feat(media): add toggle for album art accent colours (#2831)
Adds mediaUseAlbumArtAccent setting (default: off) to Settings. When enabled, MediaAccentService extracts accent from album art via ColorQuantizer. When disabled, uses Theme.primary. Toggle is in Settings > Media Player.
1 parent b619208 commit d28b942

4 files changed

Lines changed: 93 additions & 3 deletions

File tree

quickshell/Common/SettingsData.qml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,7 @@ Singleton {
455455
property bool scrollTitleEnabled: true
456456
property bool mediaAdaptiveWidthEnabled: true
457457
property bool audioVisualizerEnabled: true
458+
property bool mediaUseAlbumArtAccent: false
458459
property string audioScrollMode: "volume"
459460
property int audioWheelScrollAmount: 5
460461
property bool audioDeviceScrollVolumeEnabled: false

quickshell/Common/settings/SettingsSpec.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ var SPEC = {
185185
scrollTitleEnabled: { def: true },
186186
mediaAdaptiveWidthEnabled: { def: true },
187187
audioVisualizerEnabled: { def: true },
188+
mediaUseAlbumArtAccent: { def: false },
188189
audioScrollMode: { def: "volume" },
189190
audioWheelScrollAmount: { def: 5 },
190191
audioDeviceScrollVolumeEnabled: { def: false },

quickshell/Modules/Settings/MediaPlayerTab.qml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,13 @@ Item {
6565
onToggled: checked => SettingsData.set("mediaAdaptiveWidthEnabled", checked)
6666
}
6767

68+
SettingsToggleRow {
69+
text: I18n.tr("Use album art accent")
70+
description: I18n.tr("Use colours extracted from album art instead of system theme colours")
71+
checked: SettingsData.mediaUseAlbumArtAccent
72+
onToggled: checked => SettingsData.set("mediaUseAlbumArtAccent", checked)
73+
}
74+
6875
SettingsDropdownRow {
6976
property var scrollOptsInternal: ["volume", "song", "nothing"]
7077
property var scrollOptsDisplay: [I18n.tr("Change Volume", "media scroll wheel option"), I18n.tr("Change Song", "media scroll wheel option"), I18n.tr("Nothing", "media scroll wheel option")]

quickshell/Services/MediaAccentService.qml

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,95 @@ import qs.Services
99
Singleton {
1010
id: root
1111

12-
readonly property bool hasAccent: true
13-
readonly property color accent: Theme.primary
12+
readonly property bool hasAccent: SettingsData.mediaUseAlbumArtAccent ? _accent !== null : true
13+
readonly property color accent: SettingsData.mediaUseAlbumArtAccent && _accent !== null ? _accent : Theme.primary
1414

15-
readonly property color onAccent: Theme.onPrimary
15+
readonly property color onAccent: SettingsData.mediaUseAlbumArtAccent && _accent !== null ? (() => {
16+
const lum = 0.2126 * _accent.r + 0.7152 * _accent.g + 0.0722 * _accent.b;
17+
return lum > 0.6 ? Qt.rgba(0, 0, 0, 1) : Qt.rgba(1, 1, 1, 1);
18+
})() : Theme.onPrimary
1619

1720
readonly property color accentHover: Theme.withAlpha(accent, 0.12)
1821
readonly property color accentPressed: Theme.withAlpha(accent, Theme.transparentBlurLayers ? 0.24 : 0.16)
1922

2023
readonly property color accentTrack: Theme.withAlpha(accent, 0.28)
2124
readonly property color accentSubtle: Theme.withAlpha(accent, 0.55)
25+
26+
readonly property string artUrl: {
27+
const resolved = TrackArtService.resolvedArtUrl;
28+
if (resolved !== "")
29+
return resolved;
30+
const p = MprisController.activePlayer;
31+
if (!p)
32+
return "";
33+
if (p.trackArtUrl)
34+
return p.trackArtUrl;
35+
const m = p.metadata;
36+
return m && m["mpris:artUrl"] ? m["mpris:artUrl"].toString() : "";
37+
}
38+
39+
property var _accent: null
40+
41+
ColorQuantizer {
42+
id: quantizer
43+
source: root.artUrl
44+
depth: 4
45+
rescaleSize: 64
46+
onColorsChanged: {
47+
if (!colors || colors.length === 0)
48+
return;
49+
root._accent = root._pickAccent(colors);
50+
}
51+
}
52+
53+
function _pickAccent(colors) {
54+
if (!colors || colors.length === 0)
55+
return null;
56+
57+
let best = null;
58+
let bestScore = -1;
59+
for (let i = 0; i < colors.length; i++) {
60+
const c = colors[i];
61+
const s = c.hsvSaturation;
62+
const v = c.hsvValue;
63+
if (v < 0.22 || v > 0.96 || s < 0.22)
64+
continue;
65+
const score = s * (1 - Math.abs(v - 0.68));
66+
if (score > bestScore) {
67+
bestScore = score;
68+
best = c;
69+
}
70+
}
71+
72+
if (best)
73+
return _normalize(best);
74+
75+
return _pickNeutral(colors);
76+
}
77+
78+
function _pickNeutral(colors) {
79+
let best = null;
80+
let bestScore = -1;
81+
for (let i = 0; i < colors.length; i++) {
82+
const c = colors[i];
83+
const v = c.hsvValue;
84+
const score = (1 - Math.abs(v - 0.6)) + c.hsvSaturation * 0.5;
85+
if (score > bestScore) {
86+
bestScore = score;
87+
best = c;
88+
}
89+
}
90+
91+
const hue = best.hsvHue < 0 ? 0 : best.hsvHue;
92+
const s = Math.min(best.hsvSaturation, 0.18);
93+
const v = Math.min(Math.max(best.hsvValue, 0.6), 0.82);
94+
return Qt.hsva(hue, s, v, 1);
95+
}
96+
97+
function _normalize(c) {
98+
const hue = c.hsvHue < 0 ? 0 : c.hsvHue;
99+
const s = Math.min(1, c.hsvSaturation * 1.05);
100+
const v = Math.max(c.hsvValue, 0.62);
101+
return Qt.hsva(hue, s, v, 1);
102+
}
22103
}

0 commit comments

Comments
 (0)