From 5299945cfb4a6f9c9cd6789f7530140526c01f16 Mon Sep 17 00:00:00 2001 From: tux Date: Mon, 24 Aug 2026 18:32:02 +0530 Subject: [PATCH] feat(bar): add music widget to bar layout --- config/BarLayout.qml | 2 +- modules/bar/MusicWidget.qml | 17 +++++++++++++++ services/Media.qml | 43 +++++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 modules/bar/MusicWidget.qml create mode 100644 services/Media.qml diff --git a/config/BarLayout.qml b/config/BarLayout.qml index 7bbab10..ee47f19 100644 --- a/config/BarLayout.qml +++ b/config/BarLayout.qml @@ -11,7 +11,7 @@ Singleton { return `${Quickshell.shellPath("modules/bar")}/${name}.qml`; } - property var left: [root.widget("LauncherButton"), root.widget("PowerProfileWidget"), root.widget("HomeWidget"),] + property var left: [root.widget("LauncherButton"), root.widget("PowerProfileWidget"), root.widget("HomeWidget"), root.widget("MusicWidget")] property var center: [root.widget("CavaVisualizer"), root.widget("Workspaces"), root.widget("CavaVisualizer"),] diff --git a/modules/bar/MusicWidget.qml b/modules/bar/MusicWidget.qml new file mode 100644 index 0000000..236d031 --- /dev/null +++ b/modules/bar/MusicWidget.qml @@ -0,0 +1,17 @@ +pragma ComponentBehavior: Bound + +import qs.services +import qs.ui + +BarButton { + id: root + + readonly property string track: Media.trackArtist !== "" ? `${Media.trackTitle} — ${Media.trackArtist}` : Media.trackTitle + readonly property bool active: track !== "" + + visible: active + hoverHighlight: false + pointerCursor: false + iconSource: Media.icon + label: `${Media.truncateText(root.track, 28)} ${Media.position} / ${Media.length}` +} diff --git a/services/Media.qml b/services/Media.qml new file mode 100644 index 0000000..b0b2eb2 --- /dev/null +++ b/services/Media.qml @@ -0,0 +1,43 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Services.Mpris + +Singleton { + id: root + + readonly property MprisPlayer player: { + const players = [...Mpris.players.values]; + return players.find(p => p.playbackState === MprisPlaybackState.Playing) ?? players[0] ?? null; + } + readonly property bool playing: root.player?.isPlaying ?? false + readonly property string trackTitle: root.player?.trackTitle ?? "" + readonly property string trackArtist: root.player?.trackArtist ?? "" + readonly property url icon: { + const entry = root.player?.desktopEntry ?? ""; + return entry !== "" ? Quickshell.iconPath(entry, false) : ""; + } + readonly property string position: root.formatTime(root.player?.position ?? 0) + readonly property string length: root.formatTime(root.player?.length ?? 0) + + function truncateText(text, maxLength) { + return text.length > maxLength ? text.slice(0, maxLength - 3) + "..." : text; + } + + function formatTime(seconds: real): string { + const total = Math.max(0, Math.floor(seconds)); + const minutes = Math.floor(total / 60); + return `${minutes}:${(total % 60).toString().padStart(2, "0")}`; + } + + // `position` does not update reactively on its own; emit the changed + // signal every second while playing so bindings stay in sync. + Timer { + running: root.playing && (root.player?.positionSupported ?? false) + interval: 1000 + repeat: true + triggeredOnStart: true + onTriggered: root.player?.positionChanged() + } +}