MediaCard: New look courtesy of @pC with some tweaking.

This commit is contained in:
ItsLemmy
2025-10-06 21:47:18 -04:00
parent d48bd9f297
commit 7989bcf93b
2 changed files with 238 additions and 140 deletions
+236 -138
View File
@@ -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
}
}
}