import QtQuick import Quickshell import qs.Commons import qs.Modules.DesktopWidgets import qs.Services.Media DraggableDesktopWidget { id: root property var pluginApi: null implicitWidth: Math.round(300 * widgetScale) implicitHeight: Math.round(300 * widgetScale) showBackground: false // Scaled dimensions readonly property int scaledRadiusL: Math.round(Style.radiusL * widgetScale) // Settings from plugin readonly property real sensitivity: widgetData?.sensitivity ?? pluginApi?.pluginSettings?.sensitivity ?? pluginApi?.manifest?.metadata?.defaultSettings?.sensitivity readonly property real rotationSpeed: widgetData?.rotationSpeed ?? pluginApi?.pluginSettings?.rotationSpeed ?? pluginApi?.manifest?.metadata?.defaultSettings?.rotationSpeed readonly property real barWidth: widgetData?.barWidth ?? pluginApi?.pluginSettings?.barWidth ?? pluginApi?.manifest?.metadata?.defaultSettings?.barWidth readonly property real ringOpacity: widgetData?.ringOpacity ?? pluginApi?.pluginSettings?.ringOpacity ?? pluginApi?.manifest?.metadata?.defaultSettings?.ringOpacity readonly property real bloomIntensity: widgetData?.bloomIntensity ?? pluginApi.pluginSettings?.bloomIntensity ?? pluginApi?.manifest?.metadata?.defaultSettings?.bloomIntensity readonly property int visualizationMode: widgetData?.visualizationMode ?? pluginApi?.pluginSettings?.visualizationMode ?? pluginApi?.manifest?.metadata?.defaultSettings?.visualizationMode ?? 3 readonly property real waveThickness: widgetData?.waveThickness ?? pluginApi?.pluginSettings?.waveThickness ?? pluginApi?.manifest?.metadata?.defaultSettings?.waveThickness ?? 1.0 readonly property real innerDiameter: widgetData?.innerDiameter ?? pluginApi?.pluginSettings?.innerDiameter ?? pluginApi?.manifest?.metadata?.defaultSettings?.innerDiameter ?? 0.7 readonly property bool fadeWhenIdle: widgetData?.fadeWhenIdle ?? pluginApi?.pluginSettings?.fadeWhenIdle ?? false readonly property bool useCustomColors: widgetData?.useCustomColors ?? pluginApi?.pluginSettings?.useCustomColors ?? false readonly property color customPrimaryColor: widgetData?.customPrimaryColor ?? pluginApi?.pluginSettings?.customPrimaryColor ?? "#6750A4" readonly property color customSecondaryColor: widgetData?.customSecondaryColor ?? pluginApi?.pluginSettings?.customSecondaryColor ?? "#625B71" // Animation time for shader (0 to 3600, 1 hour cycle) property real shaderTime: 0 NumberAnimation on shaderTime { loops: Animation.Infinite from: 0 to: 3600 duration: 3600000 running: !SpectrumService.isIdle } // Hidden canvas that encodes audio data as a 32x1 texture Canvas { id: audioCanvas width: 32 height: 1 visible: false onPaint: { var ctx = getContext("2d"); var values = SpectrumService.values; if (!values || values.length === 0) { ctx.fillStyle = "black"; ctx.fillRect(0, 0, 32, 1); return; } for (var i = 0; i < 32; i++) { var v = values[i] || 0; // Encode amplitude in grayscale (R=G=B=amplitude) var c = Math.floor(v * 255); ctx.fillStyle = "rgb(" + c + "," + c + "," + c + ")"; ctx.fillRect(i, 0, 1, 1); } } } // Trigger canvas repaint when audio data changes Connections { target: SpectrumService function onValuesChanged() { if (!SpectrumService.isIdle) { audioCanvas.requestPaint(); } } } // Unique instance ID for SpectrumService registration // This prevents the old widget's destruction from unregistering the new widget readonly property string spectrumInstanceId: "plugin:fancy-audiovisualizer:" + Date.now() + Math.random() // Register with SpectrumService when pluginApi becomes available onPluginApiChanged: { if (pluginApi) { SpectrumService.registerComponent(spectrumInstanceId); audioCanvas.requestPaint(); } } Component.onDestruction: { SpectrumService.unregisterComponent(spectrumInstanceId); } // Audio texture source (outside ShaderEffect to avoid 'source' property warning) ShaderEffectSource { id: audioTextureSource sourceItem: audioCanvas live: true hideSource: true } // The shader effect visualization ShaderEffect { id: visualizer anchors.fill: parent visible: pluginApi !== null opacity: (root.fadeWhenIdle && SpectrumService.isIdle) ? 0 : 1 Behavior on opacity { NumberAnimation { duration: 500; easing.type: Easing.InOutQuad } } // Audio texture - named 'source' to match ShaderEffectSource's property and avoid warning property var source: audioTextureSource // Uniforms passed to shader property real time: root.shaderTime property real itemWidth: visualizer.width property real itemHeight: visualizer.height property color primaryColor: root.useCustomColors ? root.customPrimaryColor : Color.mPrimary property color secondaryColor: root.useCustomColors ? root.customSecondaryColor : Color.mSecondary property real sensitivity: root.sensitivity property real rotationSpeed: root.rotationSpeed property real barWidth: root.barWidth property real ringOpacity: root.ringOpacity property real cornerRadius: scaledRadiusL property real bloomIntensity: root.bloomIntensity property real visualizationMode: root.visualizationMode property real waveThickness: root.waveThickness property real innerDiameter: root.innerDiameter fragmentShader: pluginApi ? Qt.resolvedUrl(pluginApi.pluginDir + "/shaders/visualizer.frag.qsb") : "" } // Fallback when shader not loaded Rectangle { anchors.fill: parent color: Color.mSurface radius: scaledRadiusL visible: !visualizer.visible || visualizer.fragmentShader === "" Text { anchors.centerIn: parent text: "Loading..." color: Color.mOnSurface font.pointSize: Math.round(Style.fontSizeM * widgetScale) } } }