Files
noctalia-shell/Modules/DesktopWidgets/Widgets/DesktopMediaPlayer.qml
T

312 lines
9.8 KiB
QML

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