feat(desktop-widgets): add configurable audio visualizer widget and fix CAVA deregistration lifecycle

This commit is contained in:
Lysec
2026-03-07 01:05:11 +01:00
parent 6f81d7d562
commit 5533d35527
6 changed files with 296 additions and 7 deletions
+2 -1
View File
@@ -43,7 +43,8 @@
"color-name-label": "Fill color",
"hide-when-idle-description": "When enabled, the visualizer is hidden unless a player is actively playing.",
"hide-when-idle-label": "Hide when no media is playing",
"width-description": "Custom component width."
"width-description": "Custom component width.",
"height-description": "Custom component width."
},
"battery": {
"device-default": "Default (Display Device)",
@@ -0,0 +1,116 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import qs.Commons
import qs.Modules.DesktopWidgets
import qs.Services.Media
import qs.Services.UI
import qs.Widgets
import qs.Widgets.AudioSpectrum
DraggableDesktopWidget {
id: root
defaultY: 280
readonly property var widgetMetadata: DesktopWidgetRegistry.widgetMetadata["AudioVisualizer"]
readonly property int visualizerWidth: (widgetData && widgetData.width !== undefined) ? widgetData.width : (widgetMetadata?.width ?? 320)
readonly property int visualizerHeight: (widgetData && widgetData.height !== undefined) ? widgetData.height : (widgetMetadata?.height ?? 72)
readonly property string visualizerType: (widgetData && widgetData.visualizerType !== undefined) ? widgetData.visualizerType : (widgetMetadata?.visualizerType ?? "linear")
readonly property bool hideWhenIdle: (widgetData && widgetData.hideWhenIdle !== undefined) ? widgetData.hideWhenIdle : (widgetMetadata?.hideWhenIdle ?? false)
readonly property string colorName: (widgetData && widgetData.colorName !== undefined) ? widgetData.colorName : (widgetMetadata?.colorName ?? "primary")
readonly property color fillColor: Color.resolveColorKey(colorName)
readonly property bool shouldShow: visualizerType !== "" && visualizerType !== "none" && (!hideWhenIdle || MediaService.isPlaying)
readonly property bool isHidden: !shouldShow
readonly property bool shouldRegisterCava: shouldShow
// Keep widget visible in edit mode so users can move/configure it
visible: !root.isHidden || DesktopWidgetRegistry.editMode
readonly property string cavaComponentId: "desktop:audiovisualizer:" + (root.screen ? root.screen.name : "unknown") + ":" + root.widgetIndex
onShouldRegisterCavaChanged: {
if (root.shouldRegisterCava) {
CavaService.registerComponent(root.cavaComponentId);
} else {
CavaService.unregisterComponent(root.cavaComponentId);
}
}
Component.onCompleted: {
if (root.shouldRegisterCava) {
CavaService.registerComponent(root.cavaComponentId);
}
}
Component.onDestruction: {
CavaService.unregisterComponent(root.cavaComponentId);
}
implicitWidth: Math.round(visualizerWidth * widgetScale)
implicitHeight: Math.round(visualizerHeight * widgetScale)
width: implicitWidth
height: implicitHeight
Rectangle {
id: visualizerMask
anchors.fill: parent
color: "transparent"
radius: root.roundedCorners ? Math.min(Math.round(Style.radiusL * root.widgetScale), Style.radiusL, width / 2, height / 2) : 0
clip: true
Loader {
id: visualizerLoader
anchors.fill: parent
anchors.margins: root.showBackground ? Math.round(Style.marginXS * root.widgetScale) : 0
active: root.shouldShow
asynchronous: true
sourceComponent: {
switch (root.visualizerType) {
case "linear":
return linearComponent;
case "mirrored":
return mirroredComponent;
case "wave":
return waveComponent;
default:
return null;
}
}
}
}
Component {
id: linearComponent
NLinearSpectrum {
anchors.fill: parent
values: CavaService.values
fillColor: root.fillColor
showMinimumSignal: true
}
}
Component {
id: mirroredComponent
NMirroredSpectrum {
anchors.fill: parent
values: CavaService.values
fillColor: root.fillColor
showMinimumSignal: true
}
}
Component {
id: waveComponent
NWaveSpectrum {
anchors.fill: parent
values: CavaService.values
fillColor: root.fillColor
showMinimumSignal: true
}
}
}
@@ -34,9 +34,10 @@ DraggableDesktopWidget {
// CavaService registration for visualizer
readonly property string cavaComponentId: "desktopmediaplayer:" + (root.screen ? root.screen.name : "unknown")
readonly property bool needsCava: root.shouldShowVisualizer && !root.isHidden
onShouldShowVisualizerChanged: {
if (root.shouldShowVisualizer) {
onNeedsCavaChanged: {
if (root.needsCava) {
CavaService.registerComponent(root.cavaComponentId);
} else {
CavaService.unregisterComponent(root.cavaComponentId);
@@ -44,7 +45,7 @@ DraggableDesktopWidget {
}
Component.onCompleted: {
if (root.shouldShowVisualizer) {
if (root.needsCava) {
CavaService.registerComponent(root.cavaComponentId);
}
}
@@ -81,7 +82,7 @@ DraggableDesktopWidget {
anchors.bottomMargin: 0
z: 0
clip: true
active: shouldShowVisualizer
active: needsCava
layer.enabled: true
layer.smooth: true
layer.effect: MultiEffect {
@@ -0,0 +1,154 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginM
property var widgetData: null
property var widgetMetadata: null
signal settingsChanged(var settings)
property int valueWidth: widgetData.width !== undefined ? widgetData.width : widgetMetadata.width
property int valueHeight: widgetData.height !== undefined ? widgetData.height : widgetMetadata.height
property string valueVisualizerType: widgetData.visualizerType !== undefined ? widgetData.visualizerType : widgetMetadata.visualizerType
property string valueColorName: widgetData.colorName !== undefined ? widgetData.colorName : widgetMetadata.colorName
property bool valueHideWhenIdle: widgetData.hideWhenIdle !== undefined ? widgetData.hideWhenIdle : widgetMetadata.hideWhenIdle
property bool valueShowBackground: widgetData.showBackground !== undefined ? widgetData.showBackground : widgetMetadata.showBackground
property bool valueRoundedCorners: widgetData.roundedCorners !== undefined ? widgetData.roundedCorners : widgetMetadata.roundedCorners
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
settings.width = valueWidth;
settings.height = valueHeight;
settings.visualizerType = valueVisualizerType;
settings.colorName = valueColorName;
settings.hideWhenIdle = valueHideWhenIdle;
settings.showBackground = valueShowBackground;
settings.roundedCorners = valueRoundedCorners;
settingsChanged(settings);
}
NTextInput {
id: widthInput
Layout.fillWidth: true
label: I18n.tr("common.width")
description: I18n.tr("bar.audio-visualizer.width-description")
text: String(valueWidth)
placeholderText: I18n.tr("placeholders.enter-width-pixels")
inputMethodHints: Qt.ImhDigitsOnly
onEditingFinished: {
const parsed = parseInt(text);
if (!isNaN(parsed) && parsed > 0) {
valueWidth = parsed;
saveSettings();
} else {
text = String(valueWidth);
}
}
defaultValue: String(widgetMetadata.width)
}
NTextInput {
id: heightInput
Layout.fillWidth: true
label: I18n.tr("common.height")
description: I18n.tr("bar.audio-visualizer.height-description")
text: String(valueHeight)
placeholderText: I18n.tr("placeholders.enter-width-pixels")
inputMethodHints: Qt.ImhDigitsOnly
onEditingFinished: {
const parsed = parseInt(text);
if (!isNaN(parsed) && parsed > 0) {
valueHeight = parsed;
saveSettings();
} else {
text = String(valueHeight);
}
}
defaultValue: String(widgetMetadata.height)
}
NComboBox {
Layout.fillWidth: true
label: I18n.tr("panels.audio.visualizer-type-label")
description: I18n.tr("panels.desktop-widgets.media-player-visualizer-type-description")
model: [
{
"key": "linear",
"name": I18n.tr("options.visualizer-types.linear")
},
{
"key": "mirrored",
"name": I18n.tr("options.visualizer-types.mirrored")
},
{
"key": "wave",
"name": I18n.tr("options.visualizer-types.wave")
}
]
currentKey: valueVisualizerType
onSelected: key => {
valueVisualizerType = key;
saveSettings();
}
defaultValue: widgetMetadata.visualizerType
}
NColorChoice {
Layout.fillWidth: true
label: I18n.tr("bar.audio-visualizer.color-name-label")
description: I18n.tr("bar.audio-visualizer.color-name-description")
currentKey: valueColorName
onSelected: key => {
valueColorName = key;
saveSettings();
}
defaultValue: widgetMetadata.colorName
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("bar.audio-visualizer.hide-when-idle-label")
description: I18n.tr("bar.audio-visualizer.hide-when-idle-description")
checked: valueHideWhenIdle
onToggled: checked => {
valueHideWhenIdle = checked;
saveSettings();
}
defaultValue: widgetMetadata.hideWhenIdle
}
NDivider {
Layout.fillWidth: true
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.desktop-widgets.clock-show-background-label")
description: I18n.tr("panels.desktop-widgets.media-player-show-background-description")
checked: valueShowBackground
onToggled: checked => {
valueShowBackground = checked;
saveSettings();
}
defaultValue: widgetMetadata.showBackground
}
NToggle {
Layout.fillWidth: true
visible: valueShowBackground
label: I18n.tr("panels.desktop-widgets.clock-rounded-corners-label")
description: I18n.tr("panels.desktop-widgets.media-player-rounded-corners-description")
checked: valueRoundedCorners
onToggled: checked => {
valueRoundedCorners = checked;
saveSettings();
}
defaultValue: widgetMetadata.roundedCorners
}
}
@@ -266,6 +266,9 @@ ColumnLayout {
} else if (widgetId === "MediaPlayer") {
newWidget.x = 100;
newWidget.y = 200;
} else if (widgetId === "AudioVisualizer") {
newWidget.x = 120;
newWidget.y = 280;
} else if (widgetId === "Weather") {
newWidget.x = 100;
newWidget.y = 300;
+16 -2
View File
@@ -28,6 +28,9 @@ Singleton {
property Component systemStatComponent: Component {
DesktopSystemStat {}
}
property Component audioVisualizerComponent: Component {
DesktopAudioVisualizer {}
}
// Widget registry object mapping widget names to components
// Created in Component.onCompleted to ensure Components are ready
@@ -40,6 +43,7 @@ Singleton {
widgetsObj["MediaPlayer"] = mediaPlayerComponent;
widgetsObj["Weather"] = weatherComponent;
widgetsObj["SystemStat"] = systemStatComponent;
widgetsObj["AudioVisualizer"] = audioVisualizerComponent;
widgets = widgetsObj;
Logger.i("DesktopWidgetRegistry", "Service started");
@@ -49,7 +53,8 @@ Singleton {
"Clock": "WidgetSettings/ClockSettings.qml",
"MediaPlayer": "WidgetSettings/MediaPlayerSettings.qml",
"Weather": "WidgetSettings/WeatherSettings.qml",
"SystemStat": "WidgetSettings/SystemStatSettings.qml"
"SystemStat": "WidgetSettings/SystemStatSettings.qml",
"AudioVisualizer": "WidgetSettings/AudioVisualizerSettings.qml"
})
property var widgetMetadata: ({
@@ -81,10 +86,19 @@ Singleton {
"statType": "CPU",
"diskPath": "/",
"layout": "bottom"
},
"AudioVisualizer": {
"showBackground": true,
"roundedCorners": true,
"width": 320,
"height": 72,
"visualizerType": "linear",
"hideWhenIdle": false,
"colorName": "primary"
}
})
property var cpuIntensiveWidgets: ["SystemStat"]
property var cpuIntensiveWidgets: ["SystemStat", "AudioVisualizer"]
// Plugin widget storage (mirroring BarWidgetRegistry pattern)
property var pluginWidgets: ({})