diff options
| -rw-r--r-- | desktop/island/Island.qml | 15 | ||||
| -rw-r--r-- | desktop/island/MediaPill.qml | 157 | ||||
| -rw-r--r-- | desktop/island/README.md | 13 |
3 files changed, 183 insertions, 2 deletions
diff --git a/desktop/island/Island.qml b/desktop/island/Island.qml index 8532652..3dbd36d 100644 --- a/desktop/island/Island.qml +++ b/desktop/island/Island.qml @@ -12,8 +12,10 @@ import Quickshell import Quickshell.Wayland import Quickshell.Hyprland +import Quickshell.Services.Mpris import QtQuick import ".." +import "../modules/sound" import "Logic.js" as Logic // The island: a sliver at the top centre of DP-1 that the two pills slide @@ -43,7 +45,7 @@ PanelWindow { // --- reveal and hide ------------------------------------------------- - readonly property bool hovered: sliverHit.containsMouse || clock.hovered || status.hovered + readonly property bool hovered: sliverHit.containsMouse || clock.hovered || status.hovered || media.hovered property bool shown: false // A tray menu is a separate popup: moving onto it un-hovers the pills, @@ -147,6 +149,7 @@ PanelWindow { item: sliverHit Region { item: clock; intersection: Intersection.Combine } Region { item: status; intersection: Intersection.Combine } + Region { item: media.visible ? media : null; intersection: Intersection.Combine } } // The visible sliver: neutral at rest, the capture colours while @@ -213,4 +216,14 @@ PanelWindow { onOpenSound: win.panel = win.panel === "sound" ? "" : "sound" onOpenDrawer: { win.panel = ""; win.drawer.show("sound"); } } + + // While something plays or is paused; it sits left of the clock pill and + // follows it as the calendar widens it. + MediaPill { + id: media + visible: Player.playing || Player.current?.playbackState === MprisPlaybackState.Paused + x: win.cx - width / 2 - (win.gap * 1.5 + clock.width + width / 2) * win.reveal + y: -height + (win.top + height) * win.reveal + opacity: Math.min(1, win.reveal * 2) + } } diff --git a/desktop/island/MediaPill.qml b/desktop/island/MediaPill.qml new file mode 100644 index 0000000..fa4a6ba --- /dev/null +++ b/desktop/island/MediaPill.qml @@ -0,0 +1,157 @@ +// 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.Widgets +import QtQuick +import ".." +import "../modules/sound" + +// Album art and a scrolling `artist · album · title`, for the player the +// sound module's Player picks. Hovering swaps the text for a progress bar, +// which seeks on click when the player allows it. +Capsule { + id: pill + + readonly property var player: Player.current + readonly property string line: + [Player.artist, player?.trackAlbum ?? "", Player.title].filter(s => s).join(" · ") + + readonly property bool canSeek: + !!player && player.canSeek && player.positionSupported && player.lengthSupported && player.length > 0 + + restWidth: art.width + textBox.width + 34 + + // MPRIS does not push position; positionChanged() makes it re-read. + Timer { + interval: 1000 + repeat: true + running: pill.hovered && Player.playing + triggeredOnStart: true + onTriggered: pill.player?.positionChanged() + } + + ClippingRectangle { + id: art + x: 8 + y: (pill.restHeight - height) / 2 + width: 22; height: 22; radius: 4 + color: Qt.alpha(Theme.surface, 0.8) + + Image { + anchors.fill: parent + source: Player.artUrl + cache: false + asynchronous: true + fillMode: Image.PreserveAspectCrop + } + } + + // Marquee: two copies side by side, scrolled by one copy's width, so the + // loop point is invisible. Only scrolls when the line does not fit. + Item { + id: textBox + x: art.x + art.width + 10 + y: (pill.restHeight - height) / 2 + // At least 100, so a short title still leaves room for the bar. + width: Math.max(100, Math.min(220, first.implicitWidth)) + height: first.implicitHeight + clip: true + opacity: pill.hovered ? 0 : 1 + Behavior on opacity { NumberAnimation { duration: 120 } } + + readonly property bool scroll: first.implicitWidth > 220 + + Row { + id: marquee + spacing: 40 + Text { + id: first + text: pill.line + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 1 } + color: Theme.text + } + Text { + visible: textBox.scroll + text: pill.line + font: first.font + color: first.color + } + } + + NumberAnimation { + id: scroller + target: marquee + property: "x" + from: 0 + to: -(first.implicitWidth + marquee.spacing) + duration: (first.implicitWidth + marquee.spacing) * 30 + loops: Animation.Infinite + running: textBox.scroll && pill.visible + onRunningChanged: if (!running) marquee.x = 0 + } + Connections { + target: first + function onTextChanged() { marquee.x = 0; if (scroller.running) scroller.restart(); } + } + } + + // Wheel up for the previous track, down for the next, one per notch + // (120) so a touchpad does not skip several. Takes no buttons, so the + // seek bar's clicks still reach it. + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton + property real acc: 0 + onWheel: wheel => { + acc += wheel.angleDelta.y; + if (Math.abs(acc) >= 120) { + const p = pill.player; + if (acc > 0 && p?.canGoPrevious) p.previous(); + else if (acc < 0 && p?.canGoNext) p.next(); + acc = 0; + } + wheel.accepted = true; + } + } + + // Progress bar, over the text while hovered. + Item { + x: textBox.x + width: textBox.width + height: 12 + anchors.verticalCenter: parent.verticalCenter + opacity: 1 - textBox.opacity + visible: opacity > 0 + + Rectangle { + anchors.verticalCenter: parent.verticalCenter + width: parent.width + height: 4 + radius: 2 + color: Qt.alpha(Theme.text, 0.15) + + Rectangle { + width: pill.player && pill.player.length > 0 + ? parent.width * Math.min(1, pill.player.position / pill.player.length) : 0 + height: parent.height + radius: 2 + color: Theme.accent + } + } + + MouseArea { + anchors.fill: parent + enabled: pill.canSeek + cursorShape: Qt.PointingHandCursor + onClicked: mouse => pill.player.position = pill.player.length * mouse.x / width + } + } +} diff --git a/desktop/island/README.md b/desktop/island/README.md index c202e61..1511279 100644 --- a/desktop/island/README.md +++ b/desktop/island/README.md @@ -1,7 +1,8 @@ # island A thin neutral bar at the top centre of `DP-1`. Point at it and two pills -slide out of it: a clock pill on the left and a status pill on the right. It +slide out of it: a clock pill on the left and a status pill on the right, +with a media pill left of the clock while something plays or is paused. It replaced waybar, both bars. It is not a separate shell: `desktop/shell.qml` instantiates it next to the drawer, so it starts with `qs -p ./desktop` and shares the drawer's IPC and keepalive window. @@ -60,6 +61,16 @@ used to open from here is the drawer's Performance page now `activelayout` event. Scroll switches layout with `hyprctl switchxkblayout current next|prev`. +## Media pill + +Left of the clock pill, while something is playing or paused. It shows the album art and `artist · album · title`, scrolling when the +line is wider than 220px. The player is the sound module's `Player`, so the +browser proxy duplicates are already filtered out. Hovering swaps the text for a +progress bar; MPRIS does not push position, so the pill re-reads it every +second while hovered. A click on the bar seeks, only when the player reports +`canSeek` and a known length. The wheel over the pill goes to the previous track (up) +or the next (down), one per notch. + ## Controls panel (removed) There was one, opened by SUPER alone. Each of its seven tiles opened a drawer |
