mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
feat(desktop-widgets): add configurable audio visualizer widget and fix CAVA deregistration lifecycle
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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: ({})
|
||||
|
||||
Reference in New Issue
Block a user