Files
Dotfiles/noctalia/plugins/fancy-audiovisualizer/DesktopWidget.qml
T
2026-04-19 17:07:18 +02:00

151 lines
5.8 KiB
QML

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