aboutsummaryrefslogtreecommitdiffstats
path: root/volume-osd/VolumeOsd.qml
diff options
context:
space:
mode:
Diffstat (limited to 'volume-osd/VolumeOsd.qml')
-rw-r--r--volume-osd/VolumeOsd.qml302
1 files changed, 0 insertions, 302 deletions
diff --git a/volume-osd/VolumeOsd.qml b/volume-osd/VolumeOsd.qml
deleted file mode 100644
index f739aec..0000000
--- a/volume-osd/VolumeOsd.qml
+++ /dev/null
@@ -1,302 +0,0 @@
-// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
-//
-// This program is free software; you can redistribute it and/or modify
-// it under the terms of the GNU General Public License version 2 as
-// published by the Free Software Foundation.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-
-import Quickshell
-import Quickshell.Wayland
-import Quickshell.Services.Pipewire
-import QtQuick
-
-Scope {
- id: root
-
- // Milliseconds the OSD stays up after the last change.
- property int timeout: 1500
-
- readonly property PwNode sink: Pipewire.defaultAudioSink
- readonly property PwNode source: Pipewire.defaultAudioSource
-
- // Which one to draw: set by whichever node changed last.
- property PwNode active: null
- property bool isInput: false
-
- // Keeping the nodes bound is what makes volume/muted actually update.
- PwObjectTracker { objects: [root.sink, root.source].filter(n => n !== null) }
-
- // A node reports its initial volume while binding, before `ready` goes
- // true, so the `ready` check alone suppresses the startup values. Nothing
- // else may be swallowed: the next signal after that is the user's first
- // keypress, and eating it costs the OSD its first appearance.
- function show(node, input) {
- if (!node?.ready || !node.audio) return;
- root.active = node;
- root.isInput = input;
- hideTimer.restart();
- }
-
- Connections {
- target: root.sink?.audio ?? null
- function onVolumeChanged() { root.show(root.sink, false); }
- 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
- }
-
- // Quickshell exits once no window is visible, and the OSD is hidden most
- // of the time, so a 1x1 transparent window holds the process open. Its
- // empty mask makes it click-through, so it cannot catch a stray click.
- PanelWindow {
- visible: true
- implicitWidth: 1
- implicitHeight: 1
- color: "transparent"
- exclusionMode: ExclusionMode.Ignore
- mask: Region {}
- WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
- }
-
- PanelWindow {
- id: win
-
- visible: root.active !== null
-
- readonly property PwNode node: root.active
- readonly property real volume: node?.audio?.volume ?? 0
- readonly property bool muted: node?.audio?.muted ?? false
-
- // Bottom centre. Move the anchor to relocate.
- anchors.bottom: true
- margins.bottom: 120
-
- // Grows to fit the track row; the volume-only size is unchanged.
- implicitWidth: 360
- implicitHeight: Player.active ? 150 : 72
- color: "transparent"
-
- exclusionMode: ExclusionMode.Ignore
- WlrLayershell.layer: WlrLayer.Overlay
- WlrLayershell.namespace: "quickshell-volume-osd"
- // 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 {
- anchors.fill: parent
- radius: 12
- // Translucent so the compositor's blur shows through. The frosting
- // itself is Hyprland's, applied by layerrule to this window's
- // namespace: see the README.
- color: Qt.alpha(Theme.base, 0.65)
- border.width: 1
- border.color: Qt.alpha(Theme.text, 0.12)
-
- // 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 {
- anchors.verticalCenter: parent.verticalCenter
- width: 30
- horizontalAlignment: Text.AlignHCenter
- font.family: Theme.fontFamily
- font.pixelSize: 24
- color: win.muted ? Theme.red : Theme.accent
- text: {
- if (root.isInput) return win.muted ? "\uf131" : "\uf130";
- if (win.muted || win.volume <= 0) return "\uf026";
- return win.volume < 0.5 ? "\uf027" : "\uf028";
- }
- }
-
- Column {
- anchors.verticalCenter: parent.verticalCenter
- width: parent.width - 30 - parent.spacing
- spacing: 8
-
- Item {
- width: parent.width
- height: label.implicitHeight
-
- Text {
- id: label
- anchors.left: parent.left
- font.family: Theme.fontFamily
- font.pixelSize: Theme.fontSize
- color: Theme.subtext
- text: root.isInput ? "Input" : "Output"
- }
-
- Text {
- anchors.right: parent.right
- font.family: Theme.fontFamily
- font.pixelSize: Theme.fontSize
- color: Theme.text
- text: win.muted ? "muted" : Math.round(win.volume * 100) + "%"
- }
- }
-
- Rectangle {
- width: parent.width
- height: 6
- radius: 3
- color: Theme.surface
-
- Rectangle {
- height: parent.height
- radius: parent.radius
- // Volume can exceed 1.0; the bar stops at full.
- width: parent.width * Math.min(win.volume, 1)
- color: win.muted ? Theme.red : Theme.accent
- opacity: win.muted ? 0.5 : 1
- Behavior on width { NumberAnimation { duration: 100 } }
- }
- }
- }
- }
- }
- }
- }
-
- 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()
- }
- }
- }
- }
-}