208 lines
7.0 KiB
QML
208 lines
7.0 KiB
QML
// MediaCavaWidget.qml - cava visualiser + media info + volume in one pill
|
|
import QtQuick
|
|
import QtQuick.Layouts
|
|
import Quickshell.Io
|
|
import Quickshell.Services.Mpris
|
|
import ".."
|
|
|
|
Popup {
|
|
id: root
|
|
popupName: "media"
|
|
|
|
property MprisPlayer player: {
|
|
var players = Mpris.players.values
|
|
for (var i = 0; i < players.length; i++)
|
|
if (players[i].identity.toLowerCase() === "spotify") return players[i]
|
|
return players.length > 0 ? players[0] : null
|
|
}
|
|
|
|
property bool isPlaying: player?.playbackState === MprisPlaybackState.Playing ?? false
|
|
property real displayPosition: 0
|
|
|
|
Timer {
|
|
interval: 1000
|
|
running: root.visible && root.player !== null
|
|
repeat: true
|
|
triggeredOnStart: true
|
|
onTriggered: root.displayPosition = root.player?.position ?? 0
|
|
}
|
|
|
|
function formatTime(secs) {
|
|
secs = Math.max(0, Math.floor(secs))
|
|
return Math.floor(secs / 60) + ":" + String(secs % 60).padStart(2, "0")
|
|
}
|
|
|
|
component CtrlBtn: Text {
|
|
property string icon: ""
|
|
property color baseColor: Theme.pillText
|
|
property color hoverColor: Theme.pillHover
|
|
signal activate()
|
|
text: icon
|
|
font { family: Theme.fontMono; pixelSize: 20 }
|
|
color: ma.containsMouse ? hoverColor : baseColor
|
|
Behavior on color { ColorAnimation { duration: 100 } }
|
|
MouseArea {
|
|
id: ma
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
onClicked: parent.activate()
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
width: 200
|
|
spacing: 8
|
|
|
|
// Album art - hidden when no art is available
|
|
Rectangle {
|
|
visible: artImage.status === Image.Ready
|
|
Layout.alignment: Qt.AlignHCenter
|
|
Layout.preferredWidth: 180
|
|
Layout.preferredHeight: artImage.status === Image.Ready ? 180 : 0
|
|
Layout.topMargin: artImage.status === Image.Ready ? 10 : 0
|
|
radius: Theme.radius
|
|
clip: true
|
|
color: Theme.pill
|
|
|
|
Image {
|
|
id: artImage
|
|
anchors.fill: parent
|
|
source: root.player?.trackArtUrl ?? ""
|
|
fillMode: Image.PreserveAspectCrop
|
|
smooth: true
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: {
|
|
if (root.player)
|
|
Exec.run(["hyprctl", "dispatch", "hl.dsp.focus({ window = 'class:" + root.player.identity + "' })"])
|
|
GlobalStates.popups.close()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Track title with marquee scroll
|
|
Item {
|
|
id: titleClip
|
|
Layout.fillWidth: true
|
|
Layout.leftMargin: 15
|
|
Layout.rightMargin: 15
|
|
Layout.topMargin: 4
|
|
implicitHeight: titleText.implicitHeight
|
|
clip: true
|
|
|
|
Text {
|
|
id: titleText
|
|
text: root.player?.trackTitle ?? "Nothing playing"
|
|
font { family: Theme.fontSans; pixelSize: Theme.fontSize + 1; bold: true }
|
|
color: titleMa.containsMouse && root.player ? Qt.lighter(Theme.pillText, 1.3) : Theme.pillText
|
|
Behavior on color { ColorAnimation { duration: 100 } }
|
|
|
|
MouseArea {
|
|
id: titleMa
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: root.player ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
onClicked: if (root.player) {
|
|
Exec.run(["hyprctl", "dispatch", "hl.dsp.focus({ window = 'class:" + root.player.identity + "' })"])
|
|
GlobalStates.popups.close()
|
|
}
|
|
}
|
|
|
|
onTextChanged: {
|
|
marquee.stop()
|
|
x = 0
|
|
Qt.callLater(() => {
|
|
if (implicitWidth > titleClip.width) marquee.start()
|
|
})
|
|
}
|
|
}
|
|
|
|
Component.onCompleted: Qt.callLater(() => {
|
|
if (titleText.implicitWidth > width) marquee.start()
|
|
})
|
|
|
|
SequentialAnimation {
|
|
id: marquee
|
|
loops: Animation.Infinite
|
|
|
|
PauseAnimation { duration: 1500 }
|
|
NumberAnimation {
|
|
target: titleText; property: "x"
|
|
from: 0
|
|
to: -(titleText.implicitWidth - titleClip.width + 10)
|
|
duration: Math.max(1, titleText.implicitWidth - titleClip.width) * 18
|
|
easing.type: Easing.Linear
|
|
}
|
|
PauseAnimation { duration: 800 }
|
|
NumberAnimation {
|
|
target: titleText; property: "x"
|
|
to: 0; duration: 400; easing.type: Easing.OutCubic
|
|
}
|
|
}
|
|
}
|
|
|
|
// Artist
|
|
Text {
|
|
Layout.fillWidth: true
|
|
Layout.leftMargin: 15
|
|
Layout.rightMargin: 15
|
|
horizontalAlignment: Text.AlignHCenter
|
|
text: root.player?.trackArtists ? Array.from(root.player.trackArtists).join(", ") : ""
|
|
font { family: Theme.fontSans; pixelSize: Theme.fontSize }
|
|
color: Theme.pillText
|
|
elide: Text.ElideRight
|
|
}
|
|
|
|
// Progress bar
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
Layout.leftMargin: 15
|
|
Layout.rightMargin: 15
|
|
height: 3
|
|
radius: 2
|
|
color: Theme.pill
|
|
|
|
Rectangle {
|
|
width: root.player && root.player.length > 0
|
|
? Math.min(1, root.displayPosition / root.player.length) * parent.width
|
|
: 0
|
|
height: parent.height
|
|
radius: parent.radius
|
|
color: Theme.color6
|
|
}
|
|
}
|
|
|
|
// Time labels
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
Layout.leftMargin: 15
|
|
Layout.rightMargin: 15
|
|
Text {
|
|
text: root.formatTime(root.displayPosition)
|
|
font { family: Theme.fontMono; pixelSize: Theme.fontSize - 1 }
|
|
color: Theme.pillText
|
|
}
|
|
Item { Layout.fillWidth: true }
|
|
Text {
|
|
text: root.formatTime(root.player?.length ?? 0)
|
|
font { family: Theme.fontMono; pixelSize: Theme.fontSize - 1 }
|
|
color: Theme.pillText
|
|
}
|
|
}
|
|
|
|
// Controls
|
|
RowLayout {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
Layout.bottomMargin: 10
|
|
spacing: 20
|
|
CtrlBtn { text: ""; onActivate: root.player?.previous() }
|
|
CtrlBtn { text: root.isPlaying ? "" : ""; onActivate: root.player?.togglePlaying() }
|
|
CtrlBtn { text: ""; onActivate: root.player?.stop() }
|
|
CtrlBtn { text: ""; onActivate: root.player?.next() }
|
|
}
|
|
}
|
|
}
|