diff options
Diffstat (limited to 'quickshell/mpd/shell.qml')
| -rw-r--r-- | quickshell/mpd/shell.qml | 564 |
1 files changed, 564 insertions, 0 deletions
diff --git a/quickshell/mpd/shell.qml b/quickshell/mpd/shell.qml new file mode 100644 index 0000000..04a74b8 --- /dev/null +++ b/quickshell/mpd/shell.qml @@ -0,0 +1,564 @@ +import Quickshell +import Quickshell.Io +import QtQuick +import QtQuick.Layouts +ShellRoot { + property color colBg: "#000000" + property color colFg: "#ffffff" + property color colMuted: "#313244" + property color colCyan: "#89dceb" + property color colPurple: "#cba6f7" + property color colRed: "#f38ba8" + property color colYellow: "#f9e2af" + property color colBlue: "#89b4fa" + property color colGreen: "#A3BE8C" + property string fontFamily: "Iosevka NerdFont Propo" + property int fontSize: 16 + property string mpd_title: "" + property string mpd_artist: "" + property string mpd_elapsed: "0:00" + property string mpd_duration: "0:00" + property string mpd_file: "" + property real mpd_progress: 0.0 + property bool isPlaying: false + property bool isPaused: false + property bool isActive: isPlaying || isPaused + property bool isSeeking: false + property bool cardVisible: false + property bool titleCardVisible: false + property string artPath: "/tmp/mpdrop_art.png" + property string artCache: "" + property real seekStartProgress: 0 + property double seekStartTime: 0 + property int mpdVolume: 0 + function timeToSecs(t) { + var p = t.split(":") + if (p.length === 2) return parseInt(p[0]) * 60 + parseInt(p[1]) + if (p.length === 3) return parseInt(p[0]) * 3600 + parseInt(p[1]) * 60 + parseInt(p[2]) + return 0 + } + Process { + id: volProc + command: ["sh", "-c", "echo idle"] + running: false + } + Process { + id: notifyProc + property string title: "" + property string artist: "" + property string art: "" + command : ["sh", "-c", "notify-send -i '/tmp/mpdrop_art.png' -a 'MPD' 'Now Playing' '" + mpd_title.replace(/'/g, "") + (mpd_artist !== "" ? "\n" + mpd_artist.replace(/'/g, "") : "") + "'" + ] + running: false + } + + Process { + id: artProc + property string filePath: "" + command: ["sh", "-c", + "ffmpeg -i \"/mnt/" + filePath + "\" -an -vcodec copy /tmp/mpdrop_art.png -y 2>/dev/null"] + running: false + onExited: { + artCache = "" + artCache = artPath + notifyProc.running = false + notifyProc.running = true + } + } + Process { + id: fileProc + command: ["mpc", "--format", "%file%", "current"] + stdout: StdioCollector { + onStreamFinished: { + var f = this.text.trim() + if (f !== "" && f !== mpd_file) { + mpd_file = f + artProc.filePath = f + artProc.running = false + artProc.running = true + } + } + } + } + Process { + id: idleProc + command: ["mpc", "idlewait"] + running: true + onExited: statusProc.running = true + } + Process { + id: statusProc + command: ["mpc", "status", "--format", "%title%||%artist%||%duration%"] + stdout: StdioCollector { + onStreamFinished: { + var lines = this.text.trim().split("\n") + if (lines.length >= 2) { + var meta = lines[0].split("||") + var newTitle = meta[0] || "Unknown" + var volMatch = this.text.match(/volume:\s*(\d+)%/) + if (volMatch) mpdVolume = parseInt(volMatch[1]) + if (newTitle !== mpd_title) + { + fileProc.running = true + } + mpd_title = newTitle + mpd_artist = meta[1] || "" + var rawDuration = meta[2] ? meta[2].trim() : "" + var sl = lines[1] + isPlaying = sl.indexOf("[playing]") !== -1 + isPaused = sl.indexOf("[paused]") !== -1 + var tm = sl.match(/(\d+:\d+)\/(\d+:\d+)/) + if (tm) { + mpd_elapsed = tm[1] + mpd_duration = (rawDuration !== "" && rawDuration !== "0:00") ? rawDuration: tm [2] + var total = timeToSecs(mpd_duration) + if (!isSeeking) { + mpd_progress = total > 0 ? timeToSecs(tm[1]) / total : 0 + seekStartProgress = mpd_progress + seekStartTime = Date.now() / 1000 + } + } + } else { + mpd_title = ""; mpd_artist = "" + mpd_elapsed = "0:00"; mpd_duration = "0:00" + mpd_progress = 0; isPlaying = false; isPaused = false + } + } + } + onExited: idleRestartTimer.restart() + } + Timer { + id: idleRestartTimer + interval: 50 + repeat: false + onTriggered: idleProc.running = true + } + Timer { + id: progressTimer + interval: 500 + running: isPlaying && !isSeeking && isActive + repeat: true + onTriggered: { + var now = Date.now() / 1000 + var elapsed = now - seekStartTime + var total = timeToSecs(mpd_duration) + if (total <= 0) return + + var newProgress = Math.min(1.0, seekStartProgress + (elapsed / total)) + mpd_progress = newProgress + var elapsedSecs = Math.round(newProgress * total) + var m = Math.floor(elapsedSecs / 60) + var s = elapsedSecs % 60 + mpd_elapsed = m + ":" + (s < 10 ? "0" + s : s) + } + } + Timer { + id: seekResetTimer + interval: 1100 + repeat: false + onTriggered: isSeeking = false + } + Timer { + id: syncTimer + interval: 5000 + running: isActive + repeat: true + onTriggered: statusProc.running = true + } + Process { + id: ctrlProc + property var args: ["toggle"] + command: ["mpc"].concat(args) + running: false + onExited: statusProc.running = true + } + PanelWindow { + visible: titleCardVisible && isActive + screen: Quickshell.screens[0] + exclusionMode: ExclusionMode.Ignore + anchors { bottom: true; left: true } + implicitHeight: 36 + color: "transparent" + + Rectangle { + anchors.fill: titleCardText.implicitWidth + 24 + height: 28 + anchors.bottom: parent.bottom + anchors.bottomMargin: 40 + anchors.left: parent.left + anchors.leftMargin: 40 + radius: 8 + color: colBg + border.color: colMuted + border.width: 1 + opacity: titleCardVisible ? 1 : 0 + Behavior on opacity { NumberAnimation { duration: 150 } } + + Text { + id: titleCardText + anchors.centerIn: parent + text: mpd_title + color: colPurple + font.pixelSize: fontSize + font.family: fontFamily + font.bold: true + } + } + } + PanelWindow { + visible: cardVisible && isActive + screen: Quickshell.screens[0] + exclusionMode: ExclusionMode.Ignore + anchors { bottom: true; right: true } + implicitWidth: 280 + implicitHeight: artCardContent.implicitHeight + 24 + color: "transparent" + + Rectangle { + id: artCardContent + anchors.fill: parent + anchors.margins: 12 + implicitHeight: cardCol.implicitHeight + 24 + radius: 12 + color: colBg + border.color: colMuted + border.width: 1 + opacity: cardVisible ? 1 : 0 + Behavior on opacity { NumberAnimation { duration: 200 } } + + Column { + id: cardCol + anchors.fill: parent + anchors.margins: 12 + spacing: 10 + + // Album art + Rectangle { + width: parent.width + height: artImage.status === Image.Ready + ? parent.width * (artImage.implicitHeight / artImage.implicitWidth) + : parent.width + radius: 8 + color: colMuted + clip: true + Image { + id: artImage + anchors.fill: parent + source: artCache !== "" ? "file://" + artCache : "" + fillMode: Image.PreserveAspectFit + cache: false + smooth: cardVisible + asynchronous: true + Text { + anchors.centerIn: parent + text: "" + font.pixelSize: 48 + font.family: fontFamily + color: colMuted + visible: artImage.status !== Image.Ready + } + } + } + Text { + text: mpd_title + color: colFg + font.pixelSize: 13 + font.family: fontFamily + font.bold: true + elide: Text.ElideRight + width: parent.width + } + Text { + text: mpd_artist + color: colMuted + font.pixelSize: 12 + font.family: fontFamily + elide: Text.ElideRight + width: parent.width + visible: mpd_artist !== "" + } + Rectangle { + width: parent.width + height: 3 + radius: 2 + color: colMuted + Rectangle { + width: parent.width * mpd_progress + height: parent.height + radius: 2 + color: isPlaying ? colCyan : colYellow + Behavior on width { + enabled: !isSeeking + NumberAnimation { duration: 950; easing.type: Easing.Linear } + } + } + } + RowLayout { + width: parent.width + Text { + text: mpd_elapsed + color: colFg + font.pixelSize: 11 + font.family: fontFamily + } + Item { Layout.fillWidth: true } + Text { + text: mpd_duration + color: colMuted + font.pixelSize: 11 + font.family: fontFamily + } + } + } + } + } + PanelWindow { + id: mainBar + visible: isActive + screen: Quickshell.screens[0] + exclusionMode: ExclusionMode.Auto + anchors { bottom: true; left: true; right: true } + implicitHeight: 36 + color: colBg + Rectangle { + anchors.fill: parent + color: colBg + Rectangle { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + height: 1 + color: colMuted + } + RowLayout { + anchors.fill: parent + anchors.leftMargin: 8 + anchors.rightMargin: 8 + spacing: 0 + Text { + text: isPaused ? "" : "" + color: isPaused ? colYellow : colCyan + font.pixelSize: fontSize + 3 + font.family: fontFamily + font.bold: true + + Behavior on color { ColorAnimation { duration: 200 } } + } + Item { width: 8 } + Text { + text: mpd_title || "Unknown" + color: colPurple + font.pixelSize: fontSize + font.family: fontFamily + font.bold: true + elide: Text.ElideRight + Layout.maximumWidth: 220 + HoverHandler { + onHoveredChanged: titleCardVisible = hovered + } + } + + Rectangle { + Layout.preferredWidth: 1; Layout.preferredHeight: 16 + Layout.leftMargin: 8; Layout.rightMargin: 8 + color: colMuted + visible: mpd_artist !== "" + } + Text { + text: mpd_artist + color: colCyan + font.pixelSize: fontSize + font.family: fontFamily + font.bold: true + elide: Text.ElideRight + Layout.maximumWidth: 180 + visible: mpd_artist !== "" + } + Rectangle { + Layout.preferredWidth: 1; Layout.preferredHeight: 16 + Layout.leftMargin: 8; Layout.rightMargin: 8 + color: colMuted + } + Text { + text: "" + color: prevH.containsMouse ? colCyan : colFg + font.pixelSize: fontSize + 3; font.family: fontFamily; font.bold: true + Layout.rightMargin: 10 + HoverHandler { id: prevH } + TapHandler { + onTapped: { + if (!ctrlProc.running) { + ctrlProc.args = ["prev"] + ctrlProc.running = true + } + } + } + Behavior on color { ColorAnimation { duration: 100 } } + } + Text { + text: isPlaying ? "" : "" + color: playH.containsMouse ? colPurple : colCyan + font.pixelSize: fontSize + 3; font.family: fontFamily; font.bold: true + Layout.rightMargin: 10 + HoverHandler { id: playH } + TapHandler { + onTapped: { + if (!ctrlProc.running) { + ctrlProc.args = ["toggle"] + ctrlProc.running = true + } + } + } + Behavior on color { ColorAnimation { duration: 100 } } + } + Text { + text: "" + color: nextH.containsMouse ? colCyan : colFg + font.pixelSize: fontSize + 3; font.family: fontFamily; font.bold: true + Layout.rightMargin: 10 + HoverHandler { id: nextH } + TapHandler { + onTapped: { + if (!ctrlProc.running) { + ctrlProc.args = ["next"] + ctrlProc.running = true + } + } + } + Behavior on color { ColorAnimation { duration: 100 } } + } + Text { + text: "" + color: stopH.containsMouse ? colRed : colMuted + font.pixelSize: fontSize + 3; font.family: fontFamily; font.bold: true + HoverHandler { id: stopH } + TapHandler { + onTapped: { + if (!ctrlProc.running) { + ctrlProc.args = ["stop"] + ctrlProc.running = true + } + } + } + Behavior on color { ColorAnimation { duration: 100 } } + } + Rectangle { + Layout.preferredWidth: 1; Layout.preferredHeight: 16 + Layout.leftMargin: 8; Layout.rightMargin: 8 + color: colMuted + } + Item { + implicitWidth: volIcon.implicitWidth + volText.implicitWidth + 4 + implicitHeight: parent.height + + Text { + id: volIcon + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + text: mpdVolume <=0 ? "" : mpdVolume < 50 ? "" : "" + color: colBlue + font.pixelSize: fontSize + 3 + font.family: fontFamily + font.bold: true + } + Text { + id: volText + anchors.left: volIcon.right + anchors.leftMargin: 4 + anchors.verticalCenter: parent.verticalCenter + text: mpdVolume + "%" + color: colBlue + font.pixelSize: fontSize + font.family: fontFamily + font.bold: true + } + MouseArea { + anchors.fill: parent + onWheel: (wheel) => { + console.log("wheel fired: " + wheel.angleDelta.y) + volProc.command = wheel.angleDelta.y > 0 + ? ["sh", "-c","mpc volume +5"] + : ["sh", "-c","mpc volume -5"] + volProc.running = false + volProc.running = true + } + } + } + + + Rectangle { + Layout.preferredWidth: 1; Layout.preferredHeight: 16 + Layout.leftMargin: 8; Layout.rightMargin: 8 + color: colMuted + } + Text { + text: mpd_elapsed + color: colFg + font.pixelSize: fontSize; font.family: fontFamily; font.bold: true + Layout.rightMargin: 8 + } + Rectangle { + id: progressTrack + Layout.fillWidth: true + height: 4; radius: 2 + color: colMuted + Rectangle { + id: progressFill + width: progressTrack.width * mpd_progress + height: parent.height; radius: 2 + color: isPlaying ? colCyan : colYellow + Behavior on width { + enabled: !isSeeking + NumberAnimation { duration: 950; easing.type: Easing.Linear } + } + Behavior on color { ColorAnimation { duration: 300 } } + } + MouseArea { + anchors.fill: parent + anchors.topMargin: -6 + anchors.bottomMargin: -6 + cursorShape: Qt.PointingHandCursor + onClicked: (mouse) => { + if (!ctrlProc.running) { + var pct = Math.max(0, Math.min(1, mouse.x / progressTrack.width)) + var pctInt = Math.round(pct * 100) + isSeeking = true + mpd_progress = pct + seekStartProgress = pct + seekStartTime = Date.now() / 1000 + ctrlProc.args = ["seek", pctInt + "%"] + ctrlProc.running = true + seekResetTimer.restart() + } + } + } + } + Text { + text: mpd_duration + color: colMuted + font.pixelSize: fontSize; font.family: fontFamily; font.bold: true + Layout.leftMargin: 8 + } + Rectangle { + Layout.preferredWidth: 1; Layout.preferredHeight: 16 + Layout.leftMargin: 8; Layout.rightMargin: 8 + color: colMuted + } + Text { + text: "" + color: colCyan + font.pixelSize: fontSize + 3 + font.family: fontFamily + font.bold: true + HoverHandler { + id: iconHover + onHoveredChanged: cardVisible = iconHover.hovered + } + + Behavior on color { ColorAnimation { duration: 200 } } + } + Item { width: 8 } + } + } + } +} |
