diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-11 16:46:20 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-11 16:46:20 +0200 |
| commit | 4c4ed4e3ce3dd25395866cb0bb71a8187f359dbe (patch) | |
| tree | b34dc7585d31f2b8002b3d8edf4af9117e9f31f8 /volume-osd/VolumeOsd.qml | |
| parent | 4f01824832da8455a40446dc2ee949545f470ec5 (diff) | |
| download | quickshell-4c4ed4e3ce3dd25395866cb0bb71a8187f359dbe.tar.gz quickshell-4c4ed4e3ce3dd25395866cb0bb71a8187f359dbe.zip | |
feat(volume-osd): show what is playing
The OSD gains a track row above the volume bar when an MPRIS player is
running: album art, title, artist, and prev/play/next. With no player it
is the volume OSD it was, at the same size. A track change or a
play/pause shows the panel as well, so the row is not something you only
see by happening to touch the volume.
Hovering freezes the fade so the buttons can be clicked and leaving
restarts it, which keeps the passive behaviour intact for anyone not
reaching for the mouse. The panel still takes no keyboard focus.
playerctld proxies whichever player is active and republishes it under
its own bus name, so every player is enumerated twice. Player.qml drops
that name rather than deduplicating by identity, and controls the real
player, which works whether or not playerctld is running.
Album art is loaded with cache off and a per-track suffix on the URL.
Audacious extracts embedded art into one temp file in its cache and
rewrites that same path for each track, so the URL repeats while the
image changes, and a cached Image would keep showing the last cover.
Theme gains overlay, used by the art placeholder.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G7NRsGyF9jGfPYS4zPqpN7
Diffstat (limited to 'volume-osd/VolumeOsd.qml')
| -rw-r--r-- | volume-osd/VolumeOsd.qml | 135 |
1 files changed, 131 insertions, 4 deletions
diff --git a/volume-osd/VolumeOsd.qml b/volume-osd/VolumeOsd.qml index 9ee21ff..ab7eb62 100644 --- a/volume-osd/VolumeOsd.qml +++ b/volume-osd/VolumeOsd.qml @@ -47,14 +47,36 @@ Scope { function onMutedChanged() { root.show(root.sink, false); } } + // A track change shows the OSD as well, so the row is not something you + // only see when you happen to touch the volume. + Connections { + target: Player.current ?? null + function onTrackTitleChanged() { if (Player.title) root.showTrack(); } + function onPlaybackStateChanged() { root.showTrack(); } + } + Connections { target: root.source?.audio ?? null function onVolumeChanged() { root.show(root.source, true); } function onMutedChanged() { root.show(root.source, true); } } + function showTrack() { + if (!Player.active) return; + root.active = root.sink; + root.isInput = false; + hideTimer.restart(); + } + + // Hovering freezes the countdown so the transport buttons can be clicked; + // leaving starts it again. Without a hover the OSD behaves exactly as it + // did before there was anything clickable on it. + property bool hovered: false + + // Only counts down while the OSD is up and the pointer is elsewhere. Timer { id: hideTimer + running: root.active !== null && !root.hovered interval: root.timeout onTriggered: root.active = null } @@ -72,15 +94,16 @@ Scope { anchors.bottom: true margins.bottom: 120 + // Grows to fit the track row; the volume-only size is unchanged. implicitWidth: 360 - implicitHeight: 72 + implicitHeight: Player.active ? 150 : 72 color: "transparent" exclusionMode: ExclusionMode.Ignore WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.namespace: "quickshell-volume-osd" - // No keyboard focus: the OSD must never steal input from the window - // the user is typing in. + // Still no keyboard focus: the transport buttons are pointer targets, + // and the OSD must never take keys from the window being typed in. WlrLayershell.keyboardFocus: WlrKeyboardFocus.None Rectangle { @@ -93,9 +116,31 @@ Scope { border.width: 1 border.color: Qt.alpha(Theme.text, 0.12) - Row { + // Tracks the pointer over the whole panel so the fade can pause. + HoverHandler { + onHoveredChanged: root.hovered = hovered + } + + Column { anchors.fill: parent anchors.margins: 16 + spacing: 12 + + Loader { + active: Player.active + width: parent.width + sourceComponent: trackRow + } + + Rectangle { + visible: Player.active + width: parent.width + height: 1 + color: Qt.alpha(Theme.text, 0.1) + } + + Row { + width: parent.width spacing: 14 Text { @@ -157,6 +202,88 @@ Scope { } } } + } + } + } + + Component { + id: trackRow + + Row { + id: trackLine + // A Row sizes to its children, so the panel width has to be + // pushed in: the text column below subtracts from it. + width: parent ? parent.width : 0 + spacing: 12 + + // Players that extract embedded art reuse one temp path, so the + // source carries a per-track suffix and caching is off. + Rectangle { + width: 46; height: 46; radius: 6 + color: Qt.alpha(Theme.surface, 0.8) + clip: true + + Image { + anchors.fill: parent + source: Player.artUrl + cache: false + asynchronous: true + fillMode: Image.PreserveAspectCrop + visible: status === Image.Ready + } + Text { + anchors.centerIn: parent + visible: Player.artUrl === "" || parent.children[0].status !== Image.Ready + text: "" + font { family: Theme.fontFamily; pixelSize: 20 } + color: Theme.overlay + } + } + + Column { + anchors.verticalCenter: parent.verticalCenter + // Whatever the art and transport buttons leave: a fixed width + // here overflowed the panel and pushed `next` past its edge. + width: trackLine.width - 46 - transport.width - 2 * trackLine.spacing + spacing: 3 + + Text { + width: parent.width + elide: Text.ElideRight + text: Player.title || "Nothing playing" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 1; bold: true } + color: Theme.text + } + Text { + width: parent.width + elide: Text.ElideRight + visible: Player.artist !== "" + text: Player.artist + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } + color: Theme.subtext + } + } + + Row { + id: transport + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + TransportButton { + glyph: "" + enabled: Player.current?.canGoPrevious ?? false + onClicked: Player.current?.previous() + } + TransportButton { + glyph: Player.playing ? "" : "" + enabled: Player.current?.canTogglePlaying ?? false + onClicked: Player.current?.togglePlaying() + } + TransportButton { + glyph: "" + enabled: Player.current?.canGoNext ?? false + onClicked: Player.current?.next() + } + } } } } |
