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 Nerd Font 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 string artPath: "/tmp/mpdrop_art.png" property string artCache: "" 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 } // Art Extractor Process { id: artProc property string filePath: "" command: ["sh", "-c", "ffmpeg -i \"" + Qt.musicFolder + filePath + "\" -an -vcodec copy /tm/mdrop_art.png -y 2>/dev/null"] running: false onExited: { artCache = "" artCache = artPath } } // File Path Folder 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 } } } } // Idle Watcher Process { id: idleProc command: ["mpc", "idlewait"] running: true onExited: { statusProc.running = true } } // Status Fetcher 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" if (newTitle !== mpd_title) fileProc.running = true mpd_title = newTitle mpd_artist = meta[1] || "" mpd_duration = meta[2] || "0:00" 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] var total = timeToSecs(tm[2]) if (!isSeeking) { mpd_progress = total > 0 ? timeToSecs(tm[1]) / total : 0 } } else { mpd_title = ""; mpd_artist = "" mpd_elapsed = "0:00"; mpd_duration = "0:00" mpd_progress = 0; isPlaying = false; isPaused = false } } } } onExited: idleProc.running = true } property real progressPerSecond: { var total = timeToSecs(mpd_duration) return total > 0 ? 1.0 / total : 0 } Timer { id: progressTimer interval: 1000 running: isPlaying && !isSeeking repeat: true onTriggered: { mpd_progress = Math.min(1.0, mpd_progress + progressPerSecond) var elapsed = Math.round(mpd_progress * timeToSecs(mpd_duration)) var m = Math.floor(elapsed / 60) var s = elapsed % 60 mpd_elapsed = m + ":" + (s < 10 ? "0" + s : s) } } Timer { id: seekResetTimer interval: 1100 repeat: false onTriggered: isSeeking = false } // Control process Process { id: ctrlProc property var args: ["toggle"] command: ["mpc"].concat(args) running: false onExited: statusProc.running = true } // Hover Card PanelWindow { visible: cardVisible && isActive screen: Quickshell.screens[0] exclusionMode: ExclusionMode.Ignore anchors { bottom: true; left: true } implicitWidth: 280 implicitHeight: 320 color: "transparent" Rectangle { anchors.fill: parent anchors.margin: 12 radius: 12 color: colBg border.color: colMuted border.width: 1 opacity: cardVisible ? 1 : 0 Behavior on opacity { NumberAnimation { duration: 200 } } Column { anchors.fill: parent anchors.margin: 12 spacing: 10 // Album Art Rectangle { width: parent.width height: parent.width radius: 8 color: colMuted clip: true Image { id: artImage anchors.fill: parent source: artCache !== "" ? "file://" + artCache : "" fillMode: Image.PreserveAspectCrop cache: false smooth: true //Placeholder for when no art Text { anchors.centerIn: parent text: "😼" font.pixelSize: 48 font.family: fontFamily color: colMuted visible: artImage.status !== Image.Ready } } } // Title Text { text: mpd_title color: colFg font.pixelSize: 13 font.family: fontFamily font.bold: true elide: Text.ElideRight width: parent.width } // Artist Text { text: mpd_artist color: colMuted font.pixelSize: 12 font.family: fontFamily elide: Text.ElideRight width: parent.width visible: mpd_artist !== "" } // Progress Bar 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 } } } } // Time 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 } } } } } // ── The bar ──────────────────────────────────────────────── PanelWindow { id: mainBar visible: isActive screen: Quickshell.screens[0] exclusionMode: PanelWindow anchors { bottom: true; left: true; right: true } implicitHeight: 36 color: colBg // hover Detection HoverHandler { id: barHover onHoveredChanged: cardVisible = barHover.hovered } 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 // ── Music icon ───────────────────────────────── Text { text: isPaused ? "󰎊" : "󰎈" color: isPaused ? colYellow : colCyan font.pixelSize: fontSize font.family: fontFamily font.bold: true Behavior on color { ColorAnimation { duration: 200 } } } Item { width: 8 } // ── Title ────────────────────────────────────── Text { text: mpd_title || "Unknown" color: colPurple font.pixelSize: fontSize font.family: fontFamily font.bold: true elide: Text.ElideRight Layout.maximumWidth: 220 } // ── Separator + Artist ───────────────────────── 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 !== "" } // ── Separator ────────────────────────────────── Rectangle { Layout.preferredWidth: 1; Layout.preferredHeight: 16 Layout.leftMargin: 8; Layout.rightMargin: 8 color: colMuted } // ── Prev ─────────────────────────────────────── Text { text: "󰒮" color: prevH.containsMouse ? colCyan : colFg font.pixelSize: fontSize 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 } } } // ── Play / Pause ─────────────────────────────── Text { text: isPlaying ? "󰏤" : "󰐊" color: playH.containsMouse ? colPurple : colCyan font.pixelSize: fontSize + 2 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 } } } // ── Next ─────────────────────────────────────── Text { text: "󰒭" color: nextH.containsMouse ? colCyan : colFg font.pixelSize: fontSize 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 } } } // ── Stop ─────────────────────────────────────── Text { text: "󰓛" color: stopH.containsMouse ? colRed : colMuted font.pixelSize: fontSize 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 } } } // ── Separator ────────────────────────────────── Rectangle { Layout.preferredWidth: 1; Layout.preferredHeight: 16 Layout.leftMargin: 8; Layout.rightMargin: 8 color: colMuted } // ── Elapsed ──────────────────────────────────── Text { text: mpd_elapsed color: colFg font.pixelSize: fontSize font.family: fontFamily font.bold: true Layout.rightMargin: 8 } // ── Progress bar ─────────────────────────────── Rectangle { id: progressTrack Layout.fillWidth: true height: 4 radius: 2 color: colMuted // Fill 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 } } } // Seek — child of progressTrack so parent.width works 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 ctrlProc.args = ["seek", pctInt + "%"] ctrlProc.running = true seekResetTimer.restart() } } } } // ── Duration ─────────────────────────────────── Text { text: mpd_duration color: colMuted font.pixelSize: fontSize font.family: fontFamily font.bold: true Layout.leftMargin: 8 } Item { width: 8 } } } } }