aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--desktop/island/Island.qml15
-rw-r--r--desktop/island/MediaPill.qml157
-rw-r--r--desktop/island/README.md13
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