import QtQuick import QtQuick.Effects import QtQuick.Layouts 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: 200 // Widget settings - check widgetData exists before accessing properties readonly property string hideMode: (widgetData && widgetData.hideMode !== undefined) ? widgetData.hideMode : "visible" readonly property bool showButtons: (widgetData && widgetData.showButtons !== undefined) ? widgetData.showButtons : true readonly property bool showAlbumArt: (widgetData && widgetData.showAlbumArt !== undefined) ? widgetData.showAlbumArt : true readonly property bool showVisualizer: (widgetData && widgetData.showVisualizer !== undefined) ? widgetData.showVisualizer : true readonly property string visualizerType: (widgetData && widgetData.visualizerType && widgetData.visualizerType !== "") ? widgetData.visualizerType : "linear" readonly property bool roundedCorners: (widgetData && widgetData.roundedCorners !== undefined) ? widgetData.roundedCorners : true readonly property bool hasPlayer: MediaService.currentPlayer !== null readonly property bool isPlaying: MediaService.isPlaying readonly property bool hasActiveTrack: hasPlayer && (MediaService.trackTitle || MediaService.trackArtist) // State // Hide when idle when playback is not active readonly property bool shouldHideIdle: (hideMode === "idle") && !isPlaying readonly property bool shouldHideEmpty: !hasPlayer && hideMode === "hidden" readonly property bool isHidden: (shouldHideIdle || shouldHideEmpty) && !DesktopWidgetRegistry.editMode visible: !isHidden // SpectrumService registration for visualizer readonly property string spectrumComponentId: "desktopmediaplayer:" + (root.screen ? root.screen.name : "unknown") readonly property bool needsSpectrum: root.shouldShowVisualizer && !root.isHidden onNeedsSpectrumChanged: { if (root.needsSpectrum) { SpectrumService.registerComponent(root.spectrumComponentId); } else { SpectrumService.unregisterComponent(root.spectrumComponentId); } } Component.onCompleted: { if (root.needsSpectrum) { SpectrumService.registerComponent(root.spectrumComponentId); } } Component.onDestruction: { SpectrumService.unregisterComponent(root.spectrumComponentId); } readonly property bool showPrev: hasPlayer && MediaService.canGoPrevious readonly property bool showNext: hasPlayer && MediaService.canGoNext readonly property int visibleButtonCount: root.showButtons ? (1 + (showPrev ? 1 : 0) + (showNext ? 1 : 0)) : 0 implicitWidth: Math.round(400 * widgetScale) implicitHeight: Math.round(64 * widgetScale + Style.margin2M * widgetScale) width: implicitWidth height: implicitHeight // Visualizer visibility mode readonly property bool shouldShowVisualizer: { if (!root.showVisualizer) return false; if (root.visualizerType === "" || root.visualizerType === "none") return false; return true; } // Visualizer overlay (visibility controlled by visualizerVisibility setting) // Completely disabled during scaling to avoid expensive canvas redraws Loader { anchors.fill: parent anchors.leftMargin: Math.round(Style.marginXS * widgetScale) anchors.rightMargin: Math.round(Style.marginXS * widgetScale) anchors.topMargin: Math.round(Style.marginXS * widgetScale) anchors.bottomMargin: 0 z: 0 clip: true active: needsSpectrum layer.enabled: true layer.smooth: true layer.effect: MultiEffect { maskEnabled: root.roundedCorners maskSource: ShaderEffectSource { sourceItem: Rectangle { width: root.width - Math.round(Style.marginXS * widgetScale) * 2 height: root.height - Math.round(Style.marginXS * widgetScale) radius: root.roundedCorners ? Math.round(Math.max(0, (Style.radiusL - Style.marginXS) * widgetScale)) : 0 color: "white" } } } 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: SpectrumService.values fillColor: Color.mPrimary opacity: 0.5 mirrored: Settings.data.audio.spectrumMirrored } } Component { id: mirroredComponent NMirroredSpectrum { anchors.fill: parent values: SpectrumService.values fillColor: Color.mPrimary opacity: 0.5 mirrored: Settings.data.audio.spectrumMirrored } } Component { id: waveComponent NWaveSpectrum { anchors.fill: parent values: SpectrumService.values fillColor: Color.mPrimary opacity: 0.5 mirrored: Settings.data.audio.spectrumMirrored } } } // Drop shadow for text and controls readability over visualizer // Disabled during scaling to avoid expensive recomputations NDropShadow { visible: !root.isScaling anchors.fill: contentLayout source: contentLayout z: 1 autoPaddingEnabled: true shadowBlur: 1.0 shadowOpacity: 0.9 shadowHorizontalOffset: 0 shadowVerticalOffset: 0 } RowLayout { id: contentLayout states: [ State { when: root.showButtons AnchorChanges { target: contentLayout anchors.horizontalCenter: undefined anchors.verticalCenter: undefined anchors.top: parent.top anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right } }, State { when: !root.showButtons AnchorChanges { target: contentLayout anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter anchors.top: undefined anchors.bottom: undefined anchors.left: undefined anchors.right: undefined } } ] anchors.margins: Math.round(Style.marginM * widgetScale) spacing: Math.round(Style.marginS * widgetScale) z: 2 Item { visible: root.showAlbumArt Layout.preferredWidth: Math.round(48 * widgetScale) Layout.preferredHeight: Math.round(48 * widgetScale) Layout.alignment: Qt.AlignVCenter NImageRounded { visible: hasPlayer anchors.fill: parent radius: Math.round(Style.radiusM * widgetScale) imagePath: MediaService.trackArtUrl imageFillMode: Image.PreserveAspectCrop fallbackIcon: isPlaying ? "media-pause" : "media-play" fallbackIconSize: Math.round(20 * widgetScale) borderWidth: 0 } NIcon { visible: !hasPlayer anchors.centerIn: parent icon: "disc" pointSize: Math.round(24 * widgetScale) color: Color.mOnSurfaceVariant } } ColumnLayout { visible: root.showAlbumArt Layout.fillWidth: true Layout.alignment: root.showButtons ? Qt.AlignVCenter : Qt.AlignCenter spacing: 0 NText { Layout.fillWidth: true text: hasPlayer ? (MediaService.trackTitle || "Unknown Track") : "No media playing" pointSize: Math.round(Style.fontSizeS * widgetScale) font.weight: Style.fontWeightSemiBold color: Color.mOnSurface elide: Text.ElideRight maximumLineCount: 1 } NText { visible: hasPlayer && MediaService.trackArtist Layout.fillWidth: true text: MediaService.trackArtist || "" pointSize: Math.round(Style.fontSizeXS * widgetScale) font.weight: Style.fontWeightRegular color: Color.mSecondary elide: Text.ElideRight maximumLineCount: 1 } } RowLayout { id: controlsRow spacing: Math.round(Style.marginXS * widgetScale) z: 10 visible: root.showButtons Layout.alignment: root.showAlbumArt ? Qt.AlignVCenter : Qt.AlignCenter NIconButton { opacity: showPrev ? 1 : 0 Behavior on opacity { NumberAnimation { duration: Style.animationSlow easing.type: Easing.InOutQuad } } baseSize: Math.round(32 * widgetScale) icon: "media-prev" enabled: hasPlayer && MediaService.canGoPrevious colorBg: Color.mSurfaceVariant colorFg: enabled ? Color.mPrimary : Color.mOnSurfaceVariant customRadius: Math.round(Style.radiusS * widgetScale) onClicked: { if (enabled) MediaService.previous(); } } NIconButton { baseSize: Math.round(36 * widgetScale) icon: isPlaying ? "media-pause" : "media-play" enabled: hasPlayer && (MediaService.canPlay || MediaService.canPause) colorBg: Color.mPrimary colorFg: Color.mOnPrimary colorBgHover: Qt.lighter(Color.mPrimary, 1.1) colorFgHover: Color.mOnPrimary customRadius: Math.round(Style.radiusS * widgetScale) onClicked: { if (enabled) { MediaService.playPause(); } } } NIconButton { opacity: showNext ? 1 : 0 Behavior on opacity { NumberAnimation { duration: Style.animationSlow easing.type: Easing.InOutQuad } } baseSize: Math.round(32 * widgetScale) icon: "media-next" enabled: hasPlayer && MediaService.canGoNext colorBg: Color.mSurfaceVariant colorFg: enabled ? Color.mPrimary : Color.mOnSurfaceVariant customRadius: Math.round(Style.radiusS * widgetScale) onClicked: { if (enabled) MediaService.next(); } } } } }