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
@@ -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
}
}
}