From 7989bcf93bc9d06a81f83842dd32f2890fdb4f23 Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Mon, 6 Oct 2025 21:47:18 -0400 Subject: [PATCH] MediaCard: New look courtesy of @pC with some tweaking. --- Modules/ControlCenter/Cards/MediaCard.qml | 374 ++++++++++++++-------- Modules/Settings/Tabs/DisplayTab.qml | 4 +- 2 files changed, 238 insertions(+), 140 deletions(-) diff --git a/Modules/ControlCenter/Cards/MediaCard.qml b/Modules/ControlCenter/Cards/MediaCard.qml index 1d31fa44..04b337f9 100644 --- a/Modules/ControlCenter/Cards/MediaCard.qml +++ b/Modules/ControlCenter/Cards/MediaCard.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Qt5Compat.GraphicalEffects import Quickshell import qs.Modules.Audio import qs.Commons @@ -10,6 +11,112 @@ import qs.Widgets NBox { id: root + // Background artwork that covers everything + Item { + anchors.fill: parent + clip: true + + Image { + id: bgArtImage + anchors.fill: parent + source: MediaService.trackArtUrl + fillMode: Image.PreserveAspectCrop + smooth: true + visible: false + } + + OpacityMask { + anchors.fill: parent + source: bgArtImage + maskSource: Rectangle { + width: root.width + height: root.height + radius: Style.radiusM * scaling + } + visible: MediaService.trackArtUrl !== "" + } + + // Dark overlay for readability + Rectangle { + anchors.fill: parent + color: Color.mSurfaceVariant + opacity: 0.85 + radius: Style.radiusM * scaling + } + + // Border + Rectangle { + anchors.fill: parent + color: Color.transparent + border.color: Color.mOutline + border.width: 1 + radius: Style.radiusM * scaling + } + } + + // Background visualizer on top of the artwork + Item { + id: visualizerContainer + anchors.fill: parent + visible: false + + Loader { + anchors.fill: parent + active: Settings.data.audio.visualizerType !== "" && Settings.data.audio.visualizerType !== "none" + + sourceComponent: { + switch (Settings.data.audio.visualizerType) { + case "linear": + return linearComponent + case "mirrored": + return mirroredComponent + case "wave": + return waveComponent + default: + return null + } + } + + Component { + id: linearComponent + LinearSpectrum { + anchors.fill: parent + values: CavaService.values + fillColor: Color.mPrimary + } + } + + Component { + id: mirroredComponent + MirroredSpectrum { + anchors.fill: parent + values: CavaService.values + fillColor: Color.mPrimary + } + } + + Component { + id: waveComponent + WaveSpectrum { + anchors.fill: parent + values: CavaService.values + fillColor: Color.mPrimary + } + } + } + } + + OpacityMask { + anchors.fill: parent + opacity: 0.35 + source: visualizerContainer + maskSource: Rectangle { + width: root.width + height: root.height + radius: Style.radiusM * scaling + } + } + ColumnLayout { anchors.fill: parent anchors.margins: Style.marginL * scaling @@ -26,18 +133,103 @@ NBox { Layout.fillHeight: true } - NIcon { - icon: "disc" - pointSize: Style.fontSizeXXXL * 3 * scaling - color: Color.mPrimary - Layout.alignment: Qt.AlignHCenter + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + ColumnLayout { + anchors.centerIn: parent + spacing: Style.marginL * scaling + + Item { + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: Style.fontSizeXXXL * 4 * scaling + Layout.preferredHeight: Style.fontSizeXXXL * 4 * scaling + + // Pulsating audio circles (background) + Repeater { + model: 3 + Rectangle { + anchors.centerIn: parent + width: parent.width * (1.0+ index * 0.2) + height: width + radius: width / 2 + color: "transparent" + border.color: Color.mPrimary + border.width: 2 + opacity: 0 + + SequentialAnimation on opacity { + running: true + loops: Animation.Infinite + PauseAnimation { + duration: index * 600 + } + NumberAnimation { + from: 1.0 + to: 0 + duration: 2000 + easing.type: Easing.OutQuad + } + } + + SequentialAnimation on scale { + running: true + loops: Animation.Infinite + PauseAnimation { + duration: index * 600 + } + NumberAnimation { + from: 0.5 + to: 1.2 + duration: 2000 + easing.type: Easing.OutQuad + } + } + } + } + + // Spinning disc + NIcon { + anchors.centerIn: parent + icon: "disc" + pointSize: Style.fontSizeXXXL * 3 * scaling + color: Color.mPrimary + + RotationAnimator on rotation { + from: 0 + to: 360 + duration: 8000 + loops: Animation.Infinite + running: true + } + } + } + + // Descriptive text + ColumnLayout { + Layout.alignment: Qt.AlignHCenter + spacing: Style.marginXS * scaling + + NText { + Layout.alignment: Qt.AlignHCenter + text: "No music playing" + font.weight: Style.fontWeightBold + pointSize: Style.fontSizeL * scaling + color: Color.mOnSurface + } + + NText { + Layout.alignment: Qt.AlignHCenter + text: "Start playing something to see it here" + pointSize: Style.fontSizeS * scaling + color: Color.mOnSurfaceVariant + opacity: 0.7 + } + } + } } - // NText { - // text: I18n.tr("system.no-media-player-detected") - // color: Color.mOnSurfaceVariant - // Layout.alignment: Qt.AlignHCenter - // } Item { Layout.fillWidth: true Layout.fillHeight: true @@ -50,14 +242,8 @@ NBox { visible: MediaService.currentPlayer && MediaService.canPlay spacing: Style.marginM * scaling - Layout.alignment: Qt.AlignHCenter - Item { - Layout.fillWidth: true - Layout.fillHeight: true - } - - // Player selector using NContextMenu + // Player selector Rectangle { id: playerSelectorButton Layout.fillWidth: true @@ -93,7 +279,6 @@ NBox { cursorShape: Qt.PointingHandCursor onClicked: { - // Create menu items from available players var menuItems = [] var players = MediaService.getAvailablePlayers() for (var i = 0; i < players.length; i++) { @@ -125,89 +310,56 @@ NBox { } } - RowLayout { - spacing: Style.marginM * scaling + // Spacer to push content down + Item { + Layout.fillHeight: true + } - // ------------------------- - // Rounded thumbnail image - Rectangle { + // Metadata at the bottom left + ColumnLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft + spacing: Style.marginXS * scaling - width: 90 * scaling - height: 90 * scaling - radius: width * 0.5 - color: trackArt.visible ? Color.mPrimary : Color.transparent - - // Can't use fallback icon here, as we have a big disc behind - NImageCircled { - id: trackArt - visible: MediaService.trackArtUrl !== "" - anchors.fill: parent - anchors.margins: Style.marginXS * scaling - imagePath: MediaService.trackArtUrl - borderColor: Color.mOutline - borderWidth: Math.max(1, Style.borderS * scaling) - } - - // Fallback icon when no album art available - NIcon { - icon: "disc" - color: Color.mPrimary - pointSize: Style.fontSizeXXXL * 3 * scaling - visible: !trackArt.visible - anchors.centerIn: parent - } + NText { + visible: MediaService.trackTitle !== "" + text: MediaService.trackTitle + pointSize: Style.fontSizeXL * scaling + font.weight: Style.fontWeightBold + elide: Text.ElideRight + wrapMode: Text.Wrap + maximumLineCount: 2 + Layout.fillWidth: true } - // ------------------------- - // Track metadata - ColumnLayout { + NText { + visible: MediaService.trackArtist !== "" + text: MediaService.trackArtist + color: Color.mPrimary + pointSize: Style.fontSizeL * scaling + elide: Text.ElideRight Layout.fillWidth: true - spacing: Style.marginXS * scaling + } - NText { - visible: MediaService.trackTitle !== "" - text: MediaService.trackTitle - pointSize: Style.fontSizeM * scaling - font.weight: Style.fontWeightBold - elide: Text.ElideRight - wrapMode: Text.Wrap - maximumLineCount: 2 - Layout.fillWidth: true - } - - NText { - visible: MediaService.trackArtist !== "" - text: MediaService.trackArtist - color: Color.mPrimary - pointSize: Style.fontSizeXS * scaling - elide: Text.ElideRight - Layout.fillWidth: true - } - - NText { - visible: MediaService.trackAlbum !== "" - text: MediaService.trackAlbum - color: Color.mOnSurface - pointSize: Style.fontSizeXS * scaling - elide: Text.ElideRight - Layout.fillWidth: true - } + NText { + visible: MediaService.trackAlbum !== "" + text: MediaService.trackAlbum + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeM * scaling + elide: Text.ElideRight + Layout.fillWidth: true } } - // ------------------------- - // Progress slider (uses shared NSlider behavior like BarTab) + // Progress slider Item { id: progressWrapper visible: (MediaService.currentPlayer && MediaService.trackLength > 0) Layout.fillWidth: true height: Style.baseWidgetSize * 0.5 * scaling - // Local preview while dragging property real localSeekRatio: -1 - // Track the last ratio we actually sent to the backend to avoid redundant seeks property real lastSentSeekRatio: -1 - // Minimum change required to issue a new seek during drag property real seekEpsilon: 0.01 property real progressRatio: { if (!MediaService.currentPlayer || MediaService.trackLength <= 0) @@ -219,7 +371,6 @@ NBox { } property real effectiveRatio: (MediaService.isSeeking && localSeekRatio >= 0) ? Math.max(0, Math.min(1, localSeekRatio)) : progressRatio - // Debounced backend seek during drag Timer { id: seekDebounce interval: 75 @@ -265,8 +416,6 @@ NBox { } } - // While not dragging, bind slider to live progress - // during drag, let the slider manage its own value Binding { target: progressSlider property: "value" @@ -275,81 +424,30 @@ NBox { } } - // ------------------------- // Media controls RowLayout { spacing: Style.marginM * scaling Layout.fillWidth: true Layout.alignment: Qt.AlignHCenter - // Previous button NIconButton { icon: "media-prev" - tooltipText: I18n.tr("tooltips.previous-media") visible: MediaService.canGoPrevious onClicked: MediaService.canGoPrevious ? MediaService.previous() : {} } - // Play/Pause button NIconButton { icon: MediaService.isPlaying ? "media-pause" : "media-play" - tooltipText: MediaService.isPlaying ? I18n.tr("tooltips.pause") : I18n.tr("tooltips.play") visible: (MediaService.canPlay || MediaService.canPause) onClicked: (MediaService.canPlay || MediaService.canPause) ? MediaService.playPause() : {} } - // Next button NIconButton { icon: "media-next" - tooltipText: I18n.tr("tooltips.next-media") visible: MediaService.canGoNext onClicked: MediaService.canGoNext ? MediaService.next() : {} } } } - - Loader { - active: Settings.data.audio.visualizerType == "linear" - Layout.alignment: Qt.AlignHCenter - - sourceComponent: LinearSpectrum { - width: 300 * scaling - height: 80 * scaling - values: CavaService.values - fillColor: Color.mPrimary - Layout.alignment: Qt.AlignHCenter - } - } - - Loader { - active: Settings.data.audio.visualizerType == "mirrored" - Layout.alignment: Qt.AlignHCenter - - sourceComponent: MirroredSpectrum { - width: 300 * scaling - height: 80 * scaling - values: CavaService.values - fillColor: Color.mPrimary - Layout.alignment: Qt.AlignHCenter - } - } - - Loader { - active: Settings.data.audio.visualizerType == "wave" - Layout.alignment: Qt.AlignHCenter - - sourceComponent: WaveSpectrum { - width: 300 * scaling - height: 80 * scaling - values: CavaService.values - fillColor: Color.mPrimary - Layout.alignment: Qt.AlignHCenter - } - } - - Item { - Layout.fillWidth: true - Layout.fillHeight: true - } } } diff --git a/Modules/Settings/Tabs/DisplayTab.qml b/Modules/Settings/Tabs/DisplayTab.qml index 0afbb86c..491dacea 100644 --- a/Modules/Settings/Tabs/DisplayTab.qml +++ b/Modules/Settings/Tabs/DisplayTab.qml @@ -124,8 +124,8 @@ ColumnLayout { NText { text: I18n.tr("system.scaling-percentage", { - "percentage": Math.round(scaleSlider.value * 100) - }) + "percentage": Math.round(scaleSlider.value * 100) + }) Layout.preferredWidth: 55 * scaling horizontalAlignment: Text.AlignRight Layout.alignment: Qt.AlignVCenter