From 853795cfc346511f16794cb07e34135de66bd65b Mon Sep 17 00:00:00 2001 From: Jaxon Wright Date: Thu, 3 Sep 2026 23:04:02 -0400 Subject: [PATCH 01/10] Add squared waveform and slider theming Square the waveform bars and provide a plugin-local WavebarSlider with squared track, fill, tick, and knob styling so the progress and volume sliders match, without changing other Omarchy panels that use the shared rounded PanelSlider. --- WavebarSlider.qml | 150 ++++++++++++++++++++++++++++++++++++++++++++++ Waveform.qml | 2 +- 2 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 WavebarSlider.qml diff --git a/WavebarSlider.qml b/WavebarSlider.qml new file mode 100644 index 0000000..714e335 --- /dev/null +++ b/WavebarSlider.qml @@ -0,0 +1,150 @@ +import QtQuick +import qs.Commons +import qs.Ui + +Item { + id: root + + property QtObject bar: null + property real value: 0 + property real minimum: 0 + property real maximum: 1 + property real step: 0.05 + property bool integer: false + property color trackColor: bar ? Style.selectedFillFor(bar.foreground, Color.accent) : "#333" + property color fillColor: bar ? bar.foreground : Color.foreground + property color knobColor: bar ? bar.foreground : Color.foreground + property bool dragging: false + property real trackHeight: Math.max(4, Math.round(Style.spacing.controlHeight * 0.11)) + property real knobSize: Math.max(14, Math.round(Style.spacing.controlHeight * 0.38)) + property real liveValue: value + + // macOS-style notches. When > 1, that many evenly-spaced tick marks are cut + // into the track (drawn in the panel background color, so only the part + // crossing the track shows). Purely visual — snapping is the caller's job via + // `integer`/`step` or an index-based value. Default 0 leaves the track plain. + property int tickCount: 0 + property color tickColor: bar ? bar.background : Color.background + + onValueChanged: if (!dragging) liveValue = value + + signal moved(real value) + signal released(real value) + + // Right-click is a secondary action on the whole track — audio uses it to + // mute the channel the slider belongs to. Dragging stays left-button only. + signal rightClicked() + + implicitWidth: Style.space(200) + implicitHeight: Math.max(Style.space(22), knobSize + Style.spacing.md) + + readonly property real range: Math.max(0.0001, maximum - minimum) + readonly property real progress: Math.max(0, Math.min(1, (liveValue - minimum) / range)) + readonly property bool _hot: mouseArea.containsMouse || root.dragging + + Rectangle { + id: track + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.right: parent.right + height: root.trackHeight + radius: 0 + color: root.trackColor + } + + Rectangle { + id: fill + anchors.verticalCenter: track.verticalCenter + anchors.left: track.left + height: track.height + radius: track.radius + color: root.fillColor + width: track.width * root.progress + + Behavior on width { + enabled: !root.dragging + NumberAnimation { duration: 140; easing.type: Easing.OutCubic } + } + } + + Repeater { + model: root.tickCount > 1 ? root.tickCount : 0 + Rectangle { + required property int index + width: Math.max(1, Style.space(2)) + height: root.trackHeight + Style.space(4) + radius: 0 + color: root.tickColor + anchors.verticalCenter: track.verticalCenter + x: Math.max(0, Math.min(track.width - width, + track.width * (index / (root.tickCount - 1)) - width / 2)) + } + } + + BorderSurface { + id: knob + width: root.knobSize + height: root.knobSize + radius: 0 + color: root.knobColor + borderSpec: Border.flat(root.bar ? root.bar.background : "#101315", Math.max(1, Style.space(2))) + anchors.verticalCenter: track.verticalCenter + x: Math.max(0, Math.min(track.width - width, track.width * root.progress - width / 2)) + scale: root._hot ? 1.15 : 1.0 + + Behavior on x { + enabled: !root.dragging + NumberAnimation { duration: 140; easing.type: Easing.OutCubic } + } + + Behavior on scale { + NumberAnimation { duration: 110; easing.type: Easing.OutCubic } + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton + + function valueFromX(x) { + var clamped = Math.max(0, Math.min(track.width, x)) + var raw = root.minimum + (clamped / track.width) * root.range + if (root.integer) raw = Math.round(raw) + return Math.max(root.minimum, Math.min(root.maximum, raw)) + } + + onPressed: function(mouse) { + if (mouse.button !== Qt.LeftButton) return + root.dragging = true + var next = valueFromX(mouse.x) + root.liveValue = next + root.moved(next) + } + onClicked: function(mouse) { + if (mouse.button === Qt.RightButton) root.rightClicked() + } + onPositionChanged: function(mouse) { + if (!root.dragging) return + var next = valueFromX(mouse.x) + root.liveValue = next + root.moved(next) + } + onReleased: function(mouse) { + if (mouse.button !== Qt.LeftButton) return + root.dragging = false + root.released(root.liveValue) + root.liveValue = root.value + } + onWheel: function(wheel) { + var delta = wheel.angleDelta.y > 0 ? root.step : -root.step + var next = Math.max(root.minimum, Math.min(root.maximum, root.liveValue + delta)) + if (root.integer) next = Math.round(next) + root.liveValue = next + root.moved(next) + root.released(next) + } + } +} diff --git a/Waveform.qml b/Waveform.qml index c020589..c32afa1 100644 --- a/Waveform.qml +++ b/Waveform.qml @@ -35,7 +35,7 @@ Item { width: Math.max(1, slotWidth - root.gap) height: Math.max(root.minimumBarHeight, Math.round(root.height * (0.08 + level * 0.92))) y: Math.round((root.height - height) / 2) - radius: width / 2 + radius: 0 color: root.foreground opacity: root.live ? 0.95 : (root.active ? 0.48 : 0.28) From 7015b6cadc47a9a40b5f5bd6490445c5b321799d Mon Sep 17 00:00:00 2001 From: Jaxon Wright Date: Thu, 3 Sep 2026 23:08:37 -0400 Subject: [PATCH 02/10] Add opt-in album artwork with a hardened URL allowlist Expose a safe trackArtUrl on the media service and show the album cover in the bar (between the waveform and the title) and as the panel header art. Art loading is opt-in via the showCover setting and only accepts local file:// paths plus a trusted allowlist of cover CDNs (Apple Music, Spotify, YouTube, Tidal/Deezer); all other MPRIS-provided URLs are rejected and treated as no cover. The allowlist lives on the service so the bar and panel share a single check. --- BarWidget.qml | 17 +++++++++++++++++ Panel.qml | 11 +++++++++++ Service.qml | 26 ++++++++++++++++++++++++++ manifest.json | 10 +++++++++- 4 files changed, 63 insertions(+), 1 deletion(-) diff --git a/BarWidget.qml b/BarWidget.qml index d8ac690..5c59f42 100644 --- a/BarWidget.qml +++ b/BarWidget.qml @@ -13,9 +13,11 @@ BarWidget { readonly property bool playing: waveformService ? waveformService.playing : false readonly property bool showControls: setting("showControls", true) === true readonly property bool showTitle: setting("showTitle", true) === true + readonly property bool showCover: String(setting("showCover", false)).toLowerCase() === "true" readonly property bool hideWhenPaused: setting("hideWhenPaused", false) === true readonly property real waveformWidth: Math.min(240, Math.max(40, Number(setting("waveformWidth", 72)) || 72)) + readonly property string artUrl: waveformService ? waveformService.trackArtUrl : "" readonly property real maxTitleWidth: Math.min(320, Math.max(60, Number(setting("maxTitleWidth", 150)) || 150)) @@ -110,6 +112,21 @@ BarWidget { anchors.verticalCenter: parent.verticalCenter } + Item { + visible: root.showCover && root.artUrl !== "" && !root.vertical + width: Style.space(18) + height: Style.space(18) + clip: true + anchors.verticalCenter: parent.verticalCenter + + Image { + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + asynchronous: true + source: root.artUrl + } + } + Item { visible: root.showTitle && !root.vertical width: visible ? Math.min(root.maxTitleWidth, titleText.implicitWidth) : 0 diff --git a/Panel.qml b/Panel.qml index 66c6a9b..3268374 100644 --- a/Panel.qml +++ b/Panel.qml @@ -20,6 +20,7 @@ Panel { readonly property bool playing: service ? service.playing : false readonly property bool hasLength: player && player.positionSupported && player.lengthSupported && Number(player.length) > 0 + readonly property string artUrl: service ? service.trackArtUrl : "" function open() { controller.show() @@ -116,8 +117,18 @@ Panel { borderSpec: Border.controlSpec("normal", root.barForeground, Color.accent) clip: true + Image { + anchors.fill: parent + anchors.margins: Style.space(2) + fillMode: Image.PreserveAspectCrop + asynchronous: true + source: root.artUrl + visible: source !== "" + } + Text { anchors.centerIn: parent + visible: root.artUrl === "" text: "󰝚" color: root.barForeground font.family: root.bar ? root.bar.fontFamily : Style.font.family diff --git a/Service.qml b/Service.qml index 54f04a7..3781fc6 100644 --- a/Service.qml +++ b/Service.qml @@ -36,6 +36,8 @@ Item { readonly property string artist: MediaModel.playerArtist(activePlayer) readonly property string album: MediaModel.playerAlbum(activePlayer) readonly property string identity: MediaModel.playerIdentity(activePlayer) + readonly property string trackArtUrl: safeTrackArt(activePlayer && activePlayer.trackArtUrl + ? activePlayer.trackArtUrl : "") // Matching player metadata to PipeWire nodes is the most expensive model // pass. Reuse one bounded result for capture and volume instead of scoring @@ -82,6 +84,30 @@ Item { function playerArtist(player) { return MediaModel.playerArtist(player) } function playerIdentity(player) { return MediaModel.playerIdentity(player) } + // Album art hardening: only allow local files or trusted cover CDNs (Spotify, + // YouTube, Apple Music, Tidal/Deezer). Anything else is rejected so we never + // load an untrusted remote URL supplied by a media player. + function isSafeTrackArt(url) { + if (typeof url !== "string" || url === "") return false + if (url.startsWith("file://")) return true + if (url.startsWith("/")) return true + var m = /^https?:\/\/([^\/?#]+)/.exec(url) + if (!m) return false + var host = m[1] + if (host === "localhost" || host === "127.0.0.1") return true + if (host.endsWith(".mzstatic.com")) return true // Apple Music + if (host.endsWith(".scdn.co")) return true // Spotify + if (host === "i.ytimg.com" || host.endsWith(".ggpht.com") + || host.endsWith(".googleusercontent.com")) return true // YouTube + if (host.endsWith(".tidal.com") || host === "e-cdns-images.dzcdn.net" + || host.endsWith(".dzcdn.net")) return true // Tidal / Deezer CDN + return false + } + + function safeTrackArt(url) { + return isSafeTrackArt(url) ? url : "" + } + function playerKey(player) { return MediaModel.playerKey(player) } diff --git a/manifest.json b/manifest.json index a6f4e5a..2b11626 100644 --- a/manifest.json +++ b/manifest.json @@ -26,7 +26,8 @@ "showTitle": true, "hideWhenPaused": false, "waveformWidth": 72, - "maxTitleWidth": 150 + "maxTitleWidth": 150, + "showCover": false }, "schema": [ { @@ -50,6 +51,13 @@ "description": "Remove WaveBar from the bar while the selected media source is paused.", "defaultValue": false }, + { + "key": "showCover", + "type": "boolean", + "label": "Show album cover", + "description": "Show the album art thumbnail between the waveform and the track title when a cover is available.", + "defaultValue": false + }, { "key": "waveformWidth", "type": "integer", From c5b84e6d34537d58e40e41f3a24874c1013d548f Mon Sep 17 00:00:00 2001 From: Jaxon Wright Date: Thu, 3 Sep 2026 23:09:46 -0400 Subject: [PATCH 03/10] Show artist after the title and add a no-scroll full-title mode Add a showArtist setting that places the artist name after the track title, matching the panel tooltip, and a showFullTitle setting that stops truncation and horizontal scrolling so the full title (and artist) is shown. When full title is enabled the scroll animation is disabled. --- BarWidget.qml | 17 ++++++++++++++--- manifest.json | 18 +++++++++++++++++- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/BarWidget.qml b/BarWidget.qml index 5c59f42..e7e3e91 100644 --- a/BarWidget.qml +++ b/BarWidget.qml @@ -13,6 +13,8 @@ BarWidget { readonly property bool playing: waveformService ? waveformService.playing : false readonly property bool showControls: setting("showControls", true) === true readonly property bool showTitle: setting("showTitle", true) === true + readonly property bool showArtist: String(setting("showArtist", false)).toLowerCase() === "true" + readonly property bool showFullTitle: String(setting("showFullTitle", false)).toLowerCase() === "true" readonly property bool showCover: String(setting("showCover", false)).toLowerCase() === "true" readonly property bool hideWhenPaused: setting("hideWhenPaused", false) === true readonly property real waveformWidth: Math.min(240, @@ -129,7 +131,9 @@ BarWidget { Item { visible: root.showTitle && !root.vertical - width: visible ? Math.min(root.maxTitleWidth, titleText.implicitWidth) : 0 + width: visible ? (root.showFullTitle + ? titleText.implicitWidth + : Math.min(root.maxTitleWidth, titleText.implicitWidth)) : 0 height: titleText.implicitHeight clip: true anchors.verticalCenter: parent.verticalCenter @@ -138,11 +142,18 @@ BarWidget { id: titleText width: parent.width height: implicitHeight - text: root.waveformService ? root.waveformService.title : "" + text: { + if (!root.waveformService) return "" + var title = root.waveformService.title || "" + if (root.showArtist && root.waveformService.artist) { + return title + " \u2014 " + root.waveformService.artist + } + return title + } foreground: root.bar ? root.bar.barForeground : Color.foreground fontFamily: root.bar ? root.bar.fontFamily : Style.font.family fontPixelSize: Style.font.bodySmall - active: !root.opened + active: !root.opened && !root.showFullTitle } } } diff --git a/manifest.json b/manifest.json index 2b11626..3cd2a2a 100644 --- a/manifest.json +++ b/manifest.json @@ -27,7 +27,9 @@ "hideWhenPaused": false, "waveformWidth": 72, "maxTitleWidth": 150, - "showCover": false + "showCover": false, + "showArtist": false, + "showFullTitle": false }, "schema": [ { @@ -44,6 +46,13 @@ "description": "Show the scrolling track title beside the waveform on horizontal bars.", "defaultValue": true }, + { + "key": "showArtist", + "type": "boolean", + "label": "Show artist in title", + "description": "Include the artist name after the track title in the scrolling text.", + "defaultValue": false + }, { "key": "hideWhenPaused", "type": "boolean", @@ -51,6 +60,13 @@ "description": "Remove WaveBar from the bar while the selected media source is paused.", "defaultValue": false }, + { + "key": "showFullTitle", + "type": "boolean", + "label": "Show full track info", + "description": "Show the whole track title (and artist) without scrolling or clipping, instead of truncating and scrolling long titles.", + "defaultValue": false + }, { "key": "showCover", "type": "boolean", From 6dde8ca030465ac5596765fb0c6d18d959900873 Mon Sep 17 00:00:00 2001 From: Jaxon Wright Date: Thu, 3 Sep 2026 23:12:50 -0400 Subject: [PATCH 04/10] Add an option to group the playback controls into one cluster Add a groupControls setting that places previous/play/next into a single tight group beside the media info. When disabled (default), the previous button stays separable on the leading edge. Each transport button now owns its own hover tooltip so grouping never blocks tooltips on the other controls. --- BarWidget.qml | 164 ++++++++++++++++++++++++++++++++++++++------------ manifest.json | 10 ++- 2 files changed, 136 insertions(+), 38 deletions(-) diff --git a/BarWidget.qml b/BarWidget.qml index e7e3e91..c335b68 100644 --- a/BarWidget.qml +++ b/BarWidget.qml @@ -15,6 +15,7 @@ BarWidget { readonly property bool showTitle: setting("showTitle", true) === true readonly property bool showArtist: String(setting("showArtist", false)).toLowerCase() === "true" readonly property bool showFullTitle: String(setting("showFullTitle", false)).toLowerCase() === "true" + readonly property bool groupControls: String(setting("groupControls", false)).toLowerCase() === "true" readonly property bool showCover: String(setting("showCover", false)).toLowerCase() === "true" readonly property bool hideWhenPaused: setting("hideWhenPaused", false) === true readonly property real waveformWidth: Math.min(240, @@ -79,24 +80,41 @@ BarWidget { anchors.centerIn: parent spacing: Style.space(3) - Button { - visible: root.showControls - enabled: root.actionEnabled("previous") - opacity: enabled ? 1 : 0.35 - iconText: "󰒮" - foreground: root.bar ? root.bar.barForeground : Color.foreground - fontFamily: root.bar ? root.bar.fontFamily : Style.font.family - iconSize: Style.font.body - horizontalPadding: Style.space(3) - verticalPadding: Style.space(2) - tooltipText: "Previous" - onClicked: if (root.waveformService) root.waveformService.runAction("previous") - } + Item { + id: prevLeft + visible: root.showControls && !root.groupControls + implicitWidth: prevLeftBtn.implicitWidth + implicitHeight: prevLeftBtn.implicitHeight + readonly property bool tooltipHovered: prevLeftHover.containsMouse + Button { + id: prevLeftBtn + anchors.fill: parent + enabled: root.actionEnabled("previous") + opacity: enabled ? 1 : 0.35 + iconText: "󰒮" + foreground: root.bar ? root.bar.barForeground : Color.foreground + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + iconSize: Style.font.body + horizontalPadding: Style.space(3) + verticalPadding: Style.space(2) + onClicked: if (root.waveformService) root.waveformService.runAction("previous") + } + + MouseArea { + id: prevLeftHover + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + onEntered: if (root.bar) root.bar.showTooltip(prevLeft, "Previous") + onExited: if (root.bar) root.bar.hideTooltip(prevLeft) + } + } Item { id: mediaButton implicitWidth: mediaRow.implicitWidth implicitHeight: Math.max(Style.space(24), mediaRow.implicitHeight) + readonly property bool tooltipHovered: mediaHover.containsMouse Row { id: mediaRow @@ -175,33 +193,105 @@ BarWidget { } } - Button { + Row { + id: controlsGroup visible: root.showControls - enabled: root.actionEnabled("playPause") - opacity: enabled ? 1 : 0.35 - iconText: root.playing ? "󰏤" : "󰐊" - foreground: root.bar ? root.bar.barForeground : Color.foreground - fontFamily: root.bar ? root.bar.fontFamily : Style.font.family - iconSize: Style.font.body - horizontalPadding: Style.space(4) - verticalPadding: Style.space(2) - tooltipText: root.playing ? "Pause" : "Play" - onClicked: if (root.waveformService) root.waveformService.runAction("playPause") - } + spacing: Style.space(3) - Button { - visible: root.showControls - enabled: root.actionEnabled("next") - opacity: enabled ? 1 : 0.35 - iconText: "󰒭" - foreground: root.bar ? root.bar.barForeground : Color.foreground - fontFamily: root.bar ? root.bar.fontFamily : Style.font.family - iconSize: Style.font.body - horizontalPadding: Style.space(3) - verticalPadding: Style.space(2) - tooltipText: "Next" - onClicked: if (root.waveformService) root.waveformService.runAction("next") + Item { + id: prevButton + visible: root.groupControls + implicitWidth: prevBtn.implicitWidth + implicitHeight: prevBtn.implicitHeight + readonly property bool tooltipHovered: prevHover.containsMouse + + Button { + id: prevBtn + anchors.fill: parent + enabled: root.actionEnabled("previous") + opacity: enabled ? 1 : 0.35 + iconText: "󰒮" + foreground: root.bar ? root.bar.barForeground : Color.foreground + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + iconSize: Style.font.body + horizontalPadding: Style.space(3) + verticalPadding: Style.space(2) + onClicked: if (root.waveformService) root.waveformService.runAction("previous") + } + + MouseArea { + id: prevHover + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + onEntered: if (root.bar) root.bar.showTooltip(prevButton, "Previous") + onExited: if (root.bar) root.bar.hideTooltip(prevButton) + } + } + + Item { + id: playButton + visible: true + implicitWidth: playBtn.implicitWidth + implicitHeight: playBtn.implicitHeight + readonly property bool tooltipHovered: playHover.containsMouse + + Button { + id: playBtn + anchors.fill: parent + enabled: root.actionEnabled("playPause") + opacity: enabled ? 1 : 0.35 + iconText: root.playing ? "󰏤" : "󰐊" + foreground: root.bar ? root.bar.barForeground : Color.foreground + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + iconSize: Style.font.body + horizontalPadding: Style.space(3) + verticalPadding: Style.space(2) + onClicked: if (root.waveformService) root.waveformService.runAction("playPause") + } + + MouseArea { + id: playHover + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + onEntered: if (root.bar) root.bar.showTooltip(playButton, root.playing ? "Pause" : "Play") + onExited: if (root.bar) root.bar.hideTooltip(playButton) + } + } + + Item { + id: nextButton + visible: true + implicitWidth: nextBtn.implicitWidth + implicitHeight: nextBtn.implicitHeight + readonly property bool tooltipHovered: nextHover.containsMouse + + Button { + id: nextBtn + anchors.fill: parent + enabled: root.actionEnabled("next") + opacity: enabled ? 1 : 0.35 + iconText: "󰒭" + foreground: root.bar ? root.bar.barForeground : Color.foreground + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + iconSize: Style.font.body + horizontalPadding: Style.space(3) + verticalPadding: Style.space(2) + onClicked: if (root.waveformService) root.waveformService.runAction("next") + } + + MouseArea { + id: nextHover + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + onEntered: if (root.bar) root.bar.showTooltip(nextButton, "Next") + onExited: if (root.bar) root.bar.hideTooltip(nextButton) + } + } } + } Column { diff --git a/manifest.json b/manifest.json index 3cd2a2a..fffc13b 100644 --- a/manifest.json +++ b/manifest.json @@ -29,7 +29,8 @@ "maxTitleWidth": 150, "showCover": false, "showArtist": false, - "showFullTitle": false + "showFullTitle": false, + "groupControls": false }, "schema": [ { @@ -39,6 +40,13 @@ "description": "Show previous, play/pause, and next controls in the bar.", "defaultValue": true }, + { + "key": "groupControls", + "type": "boolean", + "label": "Group playback controls", + "description": "Place the previous/play/next buttons in one tight group next to the media info instead of separating previous from the rest.", + "defaultValue": false + }, { "key": "showTitle", "type": "boolean", From 6f2e9dc16fc4480fe9718be0a5635238b85d6256 Mon Sep 17 00:00:00 2001 From: Jaxon Wright Date: Thu, 3 Sep 2026 23:14:13 -0400 Subject: [PATCH 05/10] Add an in-panel settings section and polish the playback controls Add a gear button next to the volume slider that reveals a Settings section in the panel with toggles for every WaveBar option: show track title (and the hidden artist/title toggles), show album cover, show playback controls (and the grouped-controls toggle), and hide when paused. Toggles write back to the shell config through a setBooleanSetting helper so changes persist, and a taller content height is used while the section is open. The panel's sliders now use the same squared WavebarSlider as the bar, the volume row keeps its gap for the settings button, and the playback buttons use consistent icon sizing. --- Panel.qml | 160 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 151 insertions(+), 9 deletions(-) diff --git a/Panel.qml b/Panel.qml index 3268374..341256b 100644 --- a/Panel.qml +++ b/Panel.qml @@ -20,6 +20,8 @@ Panel { readonly property bool playing: service ? service.playing : false readonly property bool hasLength: player && player.positionSupported && player.lengthSupported && Number(player.length) > 0 + property bool showSettings: false + readonly property string artUrl: service ? service.trackArtUrl : "" function open() { @@ -51,6 +53,26 @@ Panel { return minutes + ":" + String(remainder).padStart(2, "0") } + function setBooleanSetting(key, value) { + var shell = root.bar && root.bar.shell + if (!shell || typeof shell.mutateShellConfig !== "function") return + shell.mutateShellConfig(function(config) { + if (!Util.isPlainObject(config.bar)) config.bar = {} + if (!Util.isPlainObject(config.bar.layout)) config.bar.layout = {} + for (var region in config.bar.layout) { + var entries = config.bar.layout[region] + if (!Array.isArray(entries)) continue + for (var i = 0; i < entries.length; i++) { + var entry = entries[i] + if (entry && entry.id === root.moduleName) { + entry[key] = !!value + return + } + } + } + }) + } + function captureMessage() { if (!service) return "Media service is loading" if (service.inputRejected) return "Media inputs exceeded safety limits" @@ -66,7 +88,6 @@ Panel { } onPlayerChanged: updatePosition() - Timer { interval: 500 repeat: true @@ -82,7 +103,8 @@ Panel { open: root.opened focusTarget: keyCatcher contentWidth: panel.fittedContentWidth(Style.space(390)) - contentHeight: panel.fittedContentHeight(content.implicitHeight, Style.space(620)) + contentHeight: panel.fittedContentHeight(content.implicitHeight, + Style.space(root.showSettings ? 860 : 620)) PanelKeyCatcher { id: keyCatcher @@ -220,7 +242,7 @@ Panel { anchors.verticalCenter: parent.verticalCenter } - PanelSlider { + WavebarSlider { id: positionSlider width: parent.width - Style.space(90) bar: root.bar @@ -253,6 +275,7 @@ Panel { Button { iconText: "󰒮" foreground: root.barForeground + iconSize: Style.font.icon enabled: root.player && root.player.canGoPrevious opacity: enabled ? 1 : 0.35 tooltipText: "Previous (P)" @@ -262,8 +285,7 @@ Panel { Button { iconText: root.playing ? "󰏤" : "󰐊" foreground: root.barForeground - iconSize: Style.font.iconLarge - horizontalPadding: Style.spacing.panelGap + iconSize: Style.font.icon enabled: root.player && (root.player.canTogglePlaying || root.player.canPlay || root.player.canPause) opacity: enabled ? 1 : 0.35 tooltipText: root.playing ? "Pause (Space)" : "Play (Space)" @@ -273,6 +295,7 @@ Panel { Button { iconText: "󰒭" foreground: root.barForeground + iconSize: Style.font.icon enabled: root.player && root.player.canGoNext opacity: enabled ? 1 : 0.35 tooltipText: "Next (N)" @@ -282,19 +305,21 @@ Panel { Row { width: parent.width - visible: root.service && root.service.volumeSupported spacing: Style.space(8) Text { text: "󰕾" + visible: root.service && root.service.volumeSupported color: root.barForeground font.family: root.bar ? root.bar.fontFamily : Style.font.family - font.pixelSize: Style.font.body + font.pixelSize: Style.font.icon anchors.verticalCenter: parent.verticalCenter } - PanelSlider { - width: parent.width - Style.space(28) + WavebarSlider { + width: parent.width - Style.space(28) - settingsButton.implicitWidth + visible: root.service && root.service.volumeSupported + anchors.verticalCenter: parent.verticalCenter bar: root.bar minimum: 0 maximum: 1 @@ -303,6 +328,16 @@ Panel { onMoved: function(value) { if (root.service) root.service.setVolume(value) } onReleased: function(value) { if (root.service) root.service.setVolume(value) } } + + Button { + id: settingsButton + iconText: "" + foreground: root.barForeground + iconSize: Style.font.icon + anchors.verticalCenter: parent.verticalCenter + tooltipText: root.showSettings ? "Hide settings" : "Settings" + onClicked: root.showSettings = !root.showSettings + } } PanelSeparator { @@ -379,6 +414,113 @@ Panel { } } } + + Column { + width: parent.width + visible: root.showSettings + spacing: Style.space(4) + + PanelSeparator { + foreground: root.barForeground + } + + PanelSectionHeader { + text: "Settings" + foreground: root.barForeground + } + + Column { + width: parent.width + spacing: Style.space(4) + + Toggle { + width: parent.width + label: "Show track title" + description: "Show the scrolling track title beside the waveform." + checked: String(root.setting("showTitle", true)).toLowerCase() === "true" + foreground: root.barForeground + accent: Color.accent + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + onClicked: root.setBooleanSetting("showTitle", + String(root.setting("showTitle", true)).toLowerCase() !== "true") + } + + Toggle { + width: parent.width + visible: String(root.setting("showTitle", true)).toLowerCase() === "true" + label: "Show artist in title" + description: "Show the artist name after the track title." + checked: String(root.setting("showArtist", false)).toLowerCase() === "true" + foreground: root.barForeground + accent: Color.accent + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + onClicked: root.setBooleanSetting("showArtist", + String(root.setting("showArtist", false)).toLowerCase() !== "true") + } + + Toggle { + width: parent.width + visible: String(root.setting("showTitle", true)).toLowerCase() === "true" + label: "Show full track info" + description: "Show the whole track title (and artist) without scrolling or clipping, instead of truncating and scrolling long titles." + checked: String(root.setting("showFullTitle", false)).toLowerCase() === "true" + foreground: root.barForeground + accent: Color.accent + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + onClicked: root.setBooleanSetting("showFullTitle", + String(root.setting("showFullTitle", false)).toLowerCase() !== "true") + } + + Toggle { + width: parent.width + label: "Show album cover" + description: "Show the album art thumbnail between the waveform and the track title when a cover is available." + checked: String(root.setting("showCover", false)).toLowerCase() === "true" + foreground: root.barForeground + accent: Color.accent + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + onClicked: root.setBooleanSetting("showCover", + String(root.setting("showCover", false)).toLowerCase() !== "true") + } + + Toggle { + width: parent.width + label: "Show playback controls" + description: "Show previous, play/pause, and next buttons in the bar." + checked: String(root.setting("showControls", true)).toLowerCase() === "true" + foreground: root.barForeground + accent: Color.accent + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + onClicked: root.setBooleanSetting("showControls", + String(root.setting("showControls", true)).toLowerCase() !== "true") + } + + Toggle { + width: parent.width + visible: String(root.setting("showControls", true)).toLowerCase() === "true" + label: "Group playback controls" + description: "Keep previous next to play/pause and next, beside the waveform." + checked: String(root.setting("groupControls", false)).toLowerCase() === "true" + foreground: root.barForeground + accent: Color.accent + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + onClicked: root.setBooleanSetting("groupControls", + String(root.setting("groupControls", false)).toLowerCase() !== "true") + } + + Toggle { + width: parent.width + label: "Hide when paused" + description: "Remove WaveBar from the bar while playback is paused." + checked: String(root.setting("hideWhenPaused", false)).toLowerCase() === "true" + foreground: root.barForeground + accent: Color.accent + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + onClicked: root.setBooleanSetting("hideWhenPaused", + String(root.setting("hideWhenPaused", false)).toLowerCase() !== "true") + } + } + } } } } From 915bfb6506e0ceb0ac4b12c3de467b71c893cf32 Mon Sep 17 00:00:00 2001 From: Jaxon Wright Date: Thu, 3 Sep 2026 23:14:22 -0400 Subject: [PATCH 06/10] docs: refresh README and CHANGELOG Document the new artist, full-title, album-cover, and grouped-controls options, the add-on panel settings section, and the squared waveform/slider theming. Describe the opt-in album-art URL allowlist in the security section and add WavebarSlider to the qmllint validation command. Add an Unreleased changelog section covering the media/interface and security changes. --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ README.md | 37 ++++++++++++++++++++++++++++--------- 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 666130e..35b0202 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # Changelog +## Unreleased + +### Media and interface + +- Added a `showArtist` setting to display the artist name after the track title + in the widget (matching the panel tooltip). +- Added a `showFullTitle` setting to show the whole track title without + truncating or horizontal scrolling. +- Added a `showCover` setting to show the album art thumbnail between the + waveform and the track title, and as the panel header art. +- Added a `groupControls` setting to keep the previous/play/pause/next buttons + grouped beside the waveform instead of splitting previous to the far side. +- Added an in-panel **Settings** section (behind a cog button) that toggles + title, artist, full-title, album-cover, controls, grouped-controls, and + hide-when-paused options. +- Used squared theming for the waveform bars and progress/volume controls via a + plugin-local `WavebarSlider`, keeping other Omarchy panels' sliders unchanged. +- Corrected panel tooltips to use the bar's native tooltip targets and made the + previous button tooltip switch targets when controls are grouped. + +### Security + +- Reintroduced opt-in album art with a hardened allowlist: only local `file://` + paths and trusted cover CDNs (Apple Music, Spotify, YouTube, Tidal/Deezer) are + ever loaded; all other MPRIS-provided URLs are rejected and treated as no + cover. The allowlist lives on the service so bar and panel share one check. + ## 1.0.2 — 2026-09-02 ### Security and lifecycle diff --git a/README.md b/README.md index 14e687d..c7b6795 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,17 @@ desktop media players. - Controls the matched local PipeWire stream volume for browsers whose MPRIS endpoint ignores volume writes. - Selecting a media source immediately starts it and pauses the prior source. -- Keeps long titles inside the widget and source list with horizontal scrolling. +- Keeps long titles inside the widget and source list with horizontal scrolling, + or shows the full title without scrolling or clipping when enabled. +- Can show the artist name after the track title in the widget. +- Can show the album art thumbnail beside the waveform and as the panel header + art, using only trusted local files or known cover CDNs. +- Squared waveform and progress/volume controls with an optional grouped + previous/play/pause/next button cluster. - Supports horizontal and vertical Omarchy bars. - Can live in the left, center, or right section of the Omarchy bar. -- Exposes display options through Omarchy's native widget settings UI. +- Exposes display options through Omarchy's native widget settings UI and an + in-panel settings section. ## How media filtering works @@ -83,8 +90,10 @@ omarchy bar move io.github.erikburdett.wavebar --before omarchy.tray The manifest uses `left` only as the initial default. Omarchy preserves the user's chosen placement in `~/.config/omarchy/shell.json`. -Use WaveBar's native widget settings in Omarchy to show or hide the playback -controls and title, hide the widget while paused, or adjust the waveform and +Use WaveBar's native widget settings in Omarchy, or the **Settings** section in +the media panel (click the cog), to show or hide the playback controls and title, +show the artist and album cover, group the playback buttons, show the full title +without scrolling, hide the widget while paused, or adjust the waveform and title widths. The equivalent inline configuration is: ```json @@ -92,7 +101,11 @@ title widths. The equivalent inline configuration is: "id": "io.github.erikburdett.wavebar", "showControls": true, "showTitle": true, + "showArtist": false, "hideWhenPaused": false, + "groupControls": false, + "showFullTitle": false, + "showCover": false, "waveformWidth": 72, "maxTitleWidth": 150 } @@ -108,10 +121,16 @@ title widths. The equivalent inline configuration is: - `/usr/bin/pw-record` from the `pipewire-audio` package - `/usr/bin/python3` and the Python 3 standard library from the `python` package -WaveBar opens no network connections and rejects MPRIS-provided artwork rather -than loading an untrusted URL or file. It uses only local MPRIS and PipeWire -services. Media collections, metadata fields, capture targets, waveform frames, -and user-configurable widths all have explicit limits. +WaveBar opens no network connections for waveform or media handling. Album art +is the one optional exception: when the user enables cover display, only local +`file://` paths and a known allowlist of trusted cover CDNs (Apple Music +`mzstatic.com`, Spotify `scdn.co`, YouTube `yimg.com`/`ggpht.com`/ +`googleusercontent.com`, and Tidal/Deezer `tidal.com`/`dzcdn.net`) are loaded. +All other MPRIS-provided URLs — remote hosts, `data:`, special files, and +oversized sources — are rejected and treated as no cover. WaveBar otherwise uses +only local MPRIS and PipeWire services. Media collections, metadata fields, +capture targets, waveform frames, and user-configurable widths all have explicit +limits. The service invokes fixed `/usr/bin/python3` and `/usr/bin/pw-record` paths with a cleared environment and no shell. Its helper validates system-executable @@ -156,7 +175,7 @@ omarchy plugin validate "$PLUGIN_DIR" -i /usr/share/omarchy/shell/Ui/qmldir \ "$PLUGIN_DIR/Service.qml" "$PLUGIN_DIR/BarWidget.qml" \ "$PLUGIN_DIR/Panel.qml" "$PLUGIN_DIR/Waveform.qml" \ - "$PLUGIN_DIR/MarqueeText.qml" + "$PLUGIN_DIR/WavebarSlider.qml" "$PLUGIN_DIR/MarqueeText.qml" node "$PLUGIN_DIR/tests/test_media_model.js" /usr/bin/python3 "$PLUGIN_DIR/tests/test_manifest.py" /usr/bin/python3 "$PLUGIN_DIR/tests/test_waveform.py" From 5dc5daeaa3fc01fd59e23005fe294298aa1162b2 Mon Sep 17 00:00:00 2001 From: Jaxon Wright Date: Thu, 3 Sep 2026 23:43:56 -0400 Subject: [PATCH 07/10] Give the panel room for the full settings list Raise the fitted-content height cap while the panel settings section is open so the last toggle (hide when paused) is not clipped at the bottom when every option is shown. --- Panel.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Panel.qml b/Panel.qml index 341256b..d48f056 100644 --- a/Panel.qml +++ b/Panel.qml @@ -104,7 +104,7 @@ Panel { focusTarget: keyCatcher contentWidth: panel.fittedContentWidth(Style.space(390)) contentHeight: panel.fittedContentHeight(content.implicitHeight, - Style.space(root.showSettings ? 860 : 620)) + Style.space(root.showSettings ? 1180 : 620)) PanelKeyCatcher { id: keyCatcher From 8e32f26a45df2a96cf8f0bd63bddbfa37727f4db Mon Sep 17 00:00:00 2001 From: Jaxon Wright Date: Thu, 3 Sep 2026 23:55:19 -0400 Subject: [PATCH 08/10] Make the panel content scroll instead of clipping Wrap the panel's content in a ScrollView so that when the settings section is open every toggle stays reachable: content taller than the panel's fitted height scrolls vertically instead of the last option overflowing the bottom edge. The vertical scrollbar appears only when the content actually overflows. --- Panel.qml | 664 +++++++++++++++++++++++++++--------------------------- 1 file changed, 329 insertions(+), 335 deletions(-) diff --git a/Panel.qml b/Panel.qml index d48f056..2d269b8 100644 --- a/Panel.qml +++ b/Panel.qml @@ -122,402 +122,396 @@ Panel { else if (key === "p" || key === "P") root.service.runAction("previous") } - Column { - id: content - width: parent.width - spacing: Style.space(10) + ScrollView { + id: scrollArea + anchors.fill: parent + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: content.implicitHeight > height ? ScrollBar.AsNeeded : ScrollBar.AlwaysOff + Binding { + target: scrollArea.contentItem + property: "interactive" + value: content.implicitHeight > scrollArea.height + } - Row { - width: parent.width + Column { + id: content + width: scrollArea.availableWidth spacing: Style.space(10) + Row { + spacing: Style.space(10) + + BorderSurface { + width: Style.space(72) + height: Style.space(72) + radius: Style.cornerRadius + color: Style.normalFillFor(root.barForeground, Color.accent) + borderSpec: Border.controlSpec("normal", root.barForeground, Color.accent) + clip: true + + Image { + anchors.fill: parent + anchors.margins: Style.space(2) + fillMode: Image.PreserveAspectCrop + asynchronous: true + source: root.artUrl + visible: source !== "" + } + + Text { + anchors.centerIn: parent + visible: root.artUrl === "" + text: "󰝚" + color: root.barForeground + font.family: root.bar ? root.bar.fontFamily : Style.font.family + font.pixelSize: Style.font.displayLarge + } + } + + Column { + width: parent.width - Style.space(82) + spacing: Style.space(3) + anchors.verticalCenter: parent.verticalCenter + + Text { + textFormat: Text.PlainText + text: root.service && root.service.title ? root.service.title : "Nothing playing" + color: root.barForeground + font.family: root.bar ? root.bar.fontFamily : Style.font.family + font.pixelSize: Style.font.subtitle + font.bold: true + elide: Text.ElideRight + } + + Text { + visible: text !== "" + textFormat: Text.PlainText + text: root.service ? root.service.artist : "" + color: Qt.darker(root.barForeground, 1.35) + font.family: root.bar ? root.bar.fontFamily : Style.font.family + font.pixelSize: Style.font.bodySmall + elide: Text.ElideRight + } + + Text { + textFormat: Text.PlainText + text: root.service ? root.service.identity : "" + color: Qt.darker(root.barForeground, 1.55) + font.family: root.bar ? root.bar.fontFamily : Style.font.family + font.pixelSize: Style.font.caption + elide: Text.ElideRight + } + } + } + BorderSurface { - width: Style.space(72) - height: Style.space(72) + height: Style.space(88) radius: Style.cornerRadius color: Style.normalFillFor(root.barForeground, Color.accent) borderSpec: Border.controlSpec("normal", root.barForeground, Color.accent) - clip: true - Image { + Waveform { anchors.fill: parent - anchors.margins: Style.space(2) - fillMode: Image.PreserveAspectCrop - asynchronous: true - source: root.artUrl - visible: source !== "" + anchors.margins: Style.space(12) + barCount: 24 + samples: root.service ? root.service.samples : [] + active: root.playing + live: root.service ? root.service.receivingFrames : false + foreground: root.barForeground + gap: Style.space(2) + minimumBarHeight: Style.space(2) } + } - Text { - anchors.centerIn: parent - visible: root.artUrl === "" - text: "󰝚" - color: root.barForeground - font.family: root.bar ? root.bar.fontFamily : Style.font.family - font.pixelSize: Style.font.displayLarge - } + Text { + textFormat: Text.PlainText + text: root.captureMessage() + color: Qt.darker(root.barForeground, 1.4) + font.family: root.bar ? root.bar.fontFamily : Style.font.family + font.pixelSize: Style.font.caption + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap } - Column { - width: parent.width - Style.space(82) - spacing: Style.space(3) - anchors.verticalCenter: parent.verticalCenter + Row { + visible: root.hasLength + spacing: Style.space(6) Text { - width: parent.width - textFormat: Text.PlainText - text: root.service && root.service.title ? root.service.title : "Nothing playing" + width: Style.space(42) + text: root.formatDuration(positionSlider.dragging ? positionSlider.liveValue : root.displayedPosition) color: root.barForeground font.family: root.bar ? root.bar.fontFamily : Style.font.family - font.pixelSize: Style.font.subtitle - font.bold: true - elide: Text.ElideRight + font.pixelSize: Style.font.caption + anchors.verticalCenter: parent.verticalCenter } - Text { - width: parent.width - visible: text !== "" - textFormat: Text.PlainText - text: root.service ? root.service.artist : "" - color: Qt.darker(root.barForeground, 1.35) - font.family: root.bar ? root.bar.fontFamily : Style.font.family - font.pixelSize: Style.font.bodySmall - elide: Text.ElideRight + WavebarSlider { + id: positionSlider + width: parent.width - Style.space(90) + bar: root.bar + minimum: 0 + maximum: root.player ? Math.max(1, Number(root.player.length) || 1) : 1 + value: root.displayedPosition + step: 5 + onMoved: function(value) { root.displayedPosition = value } + onReleased: function(value) { + if (root.service) root.service.seekTo(value) + root.displayedPosition = value + } } Text { - width: parent.width - textFormat: Text.PlainText - text: root.service ? root.service.identity : "" - color: Qt.darker(root.barForeground, 1.55) + width: Style.space(42) + text: root.formatDuration(root.player ? root.player.length : 0) + color: root.barForeground font.family: root.bar ? root.bar.fontFamily : Style.font.family font.pixelSize: Style.font.caption - elide: Text.ElideRight + horizontalAlignment: Text.AlignRight + anchors.verticalCenter: parent.verticalCenter } } - } - - BorderSurface { - width: parent.width - height: Style.space(88) - radius: Style.cornerRadius - color: Style.normalFillFor(root.barForeground, Color.accent) - borderSpec: Border.controlSpec("normal", root.barForeground, Color.accent) - - Waveform { - anchors.fill: parent - anchors.margins: Style.space(12) - barCount: 24 - samples: root.service ? root.service.samples : [] - active: root.playing - live: root.service ? root.service.receivingFrames : false - foreground: root.barForeground - gap: Style.space(2) - minimumBarHeight: Style.space(2) - } - } - - Text { - width: parent.width - textFormat: Text.PlainText - text: root.captureMessage() - color: Qt.darker(root.barForeground, 1.4) - font.family: root.bar ? root.bar.fontFamily : Style.font.family - font.pixelSize: Style.font.caption - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.WordWrap - } - - Row { - width: parent.width - visible: root.hasLength - spacing: Style.space(6) - Text { - width: Style.space(42) - text: root.formatDuration(positionSlider.dragging ? positionSlider.liveValue : root.displayedPosition) - color: root.barForeground - font.family: root.bar ? root.bar.fontFamily : Style.font.family - font.pixelSize: Style.font.caption - anchors.verticalCenter: parent.verticalCenter - } + Row { + anchors.horizontalCenter: parent.horizontalCenter + spacing: Style.space(8) - WavebarSlider { - id: positionSlider - width: parent.width - Style.space(90) - bar: root.bar - minimum: 0 - maximum: root.player ? Math.max(1, Number(root.player.length) || 1) : 1 - value: root.displayedPosition - step: 5 - onMoved: function(value) { root.displayedPosition = value } - onReleased: function(value) { - if (root.service) root.service.seekTo(value) - root.displayedPosition = value + Button { + iconText: "󰒮" + foreground: root.barForeground + iconSize: Style.font.icon + enabled: root.player && root.player.canGoPrevious + opacity: enabled ? 1 : 0.35 + tooltipText: "Previous (P)" + onClicked: if (root.service) root.service.runAction("previous") } - } - Text { - width: Style.space(42) - text: root.formatDuration(root.player ? root.player.length : 0) - color: root.barForeground - font.family: root.bar ? root.bar.fontFamily : Style.font.family - font.pixelSize: Style.font.caption - horizontalAlignment: Text.AlignRight - anchors.verticalCenter: parent.verticalCenter - } - } - - Row { - anchors.horizontalCenter: parent.horizontalCenter - spacing: Style.space(8) - - Button { - iconText: "󰒮" - foreground: root.barForeground - iconSize: Style.font.icon - enabled: root.player && root.player.canGoPrevious - opacity: enabled ? 1 : 0.35 - tooltipText: "Previous (P)" - onClicked: if (root.service) root.service.runAction("previous") - } + Button { + iconText: root.playing ? "󰏤" : "󰐊" + foreground: root.barForeground + iconSize: Style.font.icon + enabled: root.player && (root.player.canTogglePlaying || root.player.canPlay || root.player.canPause) + opacity: enabled ? 1 : 0.35 + tooltipText: root.playing ? "Pause (Space)" : "Play (Space)" + onClicked: if (root.service) root.service.runAction("playPause") + } - Button { - iconText: root.playing ? "󰏤" : "󰐊" - foreground: root.barForeground - iconSize: Style.font.icon - enabled: root.player && (root.player.canTogglePlaying || root.player.canPlay || root.player.canPause) - opacity: enabled ? 1 : 0.35 - tooltipText: root.playing ? "Pause (Space)" : "Play (Space)" - onClicked: if (root.service) root.service.runAction("playPause") + Button { + iconText: "󰒭" + foreground: root.barForeground + iconSize: Style.font.icon + enabled: root.player && root.player.canGoNext + opacity: enabled ? 1 : 0.35 + tooltipText: "Next (N)" + onClicked: if (root.service) root.service.runAction("next") + } } - Button { - iconText: "󰒭" - foreground: root.barForeground - iconSize: Style.font.icon - enabled: root.player && root.player.canGoNext - opacity: enabled ? 1 : 0.35 - tooltipText: "Next (N)" - onClicked: if (root.service) root.service.runAction("next") - } - } + Row { + spacing: Style.space(8) - Row { - width: parent.width - spacing: Style.space(8) + Text { + text: "󰕾" + visible: root.service && root.service.volumeSupported + color: root.barForeground + font.family: root.bar ? root.bar.fontFamily : Style.font.family + font.pixelSize: Style.font.icon + anchors.verticalCenter: parent.verticalCenter + } - Text { - text: "󰕾" - visible: root.service && root.service.volumeSupported - color: root.barForeground - font.family: root.bar ? root.bar.fontFamily : Style.font.family - font.pixelSize: Style.font.icon - anchors.verticalCenter: parent.verticalCenter - } + WavebarSlider { + width: parent.width - Style.space(28) - settingsButton.implicitWidth + visible: root.service && root.service.volumeSupported + anchors.verticalCenter: parent.verticalCenter + bar: root.bar + minimum: 0 + maximum: 1 + step: 0.05 + value: root.service ? root.service.volume : 0 + onMoved: function(value) { if (root.service) root.service.setVolume(value) } + onReleased: function(value) { if (root.service) root.service.setVolume(value) } + } - WavebarSlider { - width: parent.width - Style.space(28) - settingsButton.implicitWidth - visible: root.service && root.service.volumeSupported - anchors.verticalCenter: parent.verticalCenter - bar: root.bar - minimum: 0 - maximum: 1 - step: 0.05 - value: root.service ? root.service.volume : 0 - onMoved: function(value) { if (root.service) root.service.setVolume(value) } - onReleased: function(value) { if (root.service) root.service.setVolume(value) } + Button { + id: settingsButton + iconText: "" + foreground: root.barForeground + iconSize: Style.font.icon + anchors.verticalCenter: parent.verticalCenter + tooltipText: root.showSettings ? "Hide settings" : "Settings" + onClicked: root.showSettings = !root.showSettings + } } - Button { - id: settingsButton - iconText: "" + PanelSeparator { + visible: root.service && root.service.focusedPlayers.length > 1 foreground: root.barForeground - iconSize: Style.font.icon - anchors.verticalCenter: parent.verticalCenter - tooltipText: root.showSettings ? "Hide settings" : "Settings" - onClicked: root.showSettings = !root.showSettings } - } - - PanelSeparator { - visible: root.service && root.service.focusedPlayers.length > 1 - foreground: root.barForeground - } - Column { - id: sourceList - width: parent.width - visible: root.service && root.service.focusedPlayers.length > 1 - spacing: Style.space(4) + Column { + id: sourceList + visible: root.service && root.service.focusedPlayers.length > 1 + spacing: Style.space(4) - PanelSectionHeader { - text: "Media sources" - foreground: root.barForeground - } + PanelSectionHeader { + text: "Media sources" + foreground: root.barForeground + } - ListView { - id: sourceView - width: parent.width - height: Math.min(contentHeight, Style.space(210)) - spacing: Style.space(4) - clip: true - boundsBehavior: Flickable.StopAtBounds - interactive: contentHeight > height - - QQC.ScrollBar.vertical: QQC.ScrollBar { policy: QQC.ScrollBar.AsNeeded } - - model: root.service - ? root.service.focusedPlayers.slice(0, root.service.maxPlayers) : [] - - delegate: Button { - id: sourceButton - required property var modelData - required property int index - readonly property var sourcePlayer: modelData - readonly property bool isCurrent: root.player && root.service - && root.service.playerKey(root.player) === root.service.playerKey(sourcePlayer) - readonly property string sourceTitle: root.service - ? (root.service.playerTitle(sourcePlayer) - || root.service.playerIdentity(sourcePlayer) || "Media") : "Media" - readonly property string sourceArtist: root.service - ? root.service.playerArtist(sourcePlayer) : "" - - width: ListView.view.width - height: sourceButton.implicitHeight + ListView { + id: sourceView + height: Math.min(contentHeight, Style.space(210)) + spacing: Style.space(4) clip: true - leftAlign: true - foreground: root.barForeground - selected: isCurrent - iconText: sourcePlayer && sourcePlayer.isPlaying ? "󰏤" : "󰐊" - text: "" - tooltipText: sourceTitle + (sourceArtist ? " — " + sourceArtist : "") - onClicked: if (root.service) root.service.selectAndPlay(root.service.playerKey(sourcePlayer)) - - MarqueeText { - z: 1 - anchors.left: parent.left - anchors.leftMargin: sourceButton.horizontalPadding + Style.space(22) - anchors.right: parent.right - anchors.rightMargin: sourceButton.horizontalPadding - anchors.verticalCenter: parent.verticalCenter - height: implicitHeight - text: sourceButton.sourceTitle - foreground: sourceButton.selected - ? Style.selectedStateColor(root.barForeground, Color.accent) - : root.barForeground - fontFamily: root.bar ? root.bar.fontFamily : Style.font.family - fontPixelSize: Style.font.bodySmall - fontBold: sourceButton.selected - active: sourceButton.hot || sourceButton.isCurrent + boundsBehavior: Flickable.StopAtBounds + interactive: contentHeight > height + + QQC.ScrollBar.vertical: QQC.ScrollBar { policy: QQC.ScrollBar.AsNeeded } + + model: root.service + ? root.service.focusedPlayers.slice(0, root.service.maxPlayers) : [] + + delegate: Button { + id: sourceButton + required property var modelData + required property int index + readonly property var sourcePlayer: modelData + readonly property bool isCurrent: root.player && root.service + && root.service.playerKey(root.player) === root.service.playerKey(sourcePlayer) + readonly property string sourceTitle: root.service + ? (root.service.playerTitle(sourcePlayer) + || root.service.playerIdentity(sourcePlayer) || "Media") : "Media" + readonly property string sourceArtist: root.service + ? root.service.playerArtist(sourcePlayer) : "" + + width: ListView.view.width + height: sourceButton.implicitHeight + clip: true + leftAlign: true + foreground: root.barForeground + selected: isCurrent + iconText: sourcePlayer && sourcePlayer.isPlaying ? "󰏤" : "󰐊" + text: "" + tooltipText: sourceTitle + (sourceArtist ? " — " + sourceArtist : "") + onClicked: if (root.service) root.service.selectAndPlay(root.service.playerKey(sourcePlayer)) + + MarqueeText { + z: 1 + anchors.left: parent.left + anchors.leftMargin: sourceButton.horizontalPadding + Style.space(22) + anchors.right: parent.right + anchors.rightMargin: sourceButton.horizontalPadding + anchors.verticalCenter: parent.verticalCenter + height: implicitHeight + text: sourceButton.sourceTitle + foreground: sourceButton.selected + ? Style.selectedStateColor(root.barForeground, Color.accent) + : root.barForeground + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + fontPixelSize: Style.font.bodySmall + fontBold: sourceButton.selected + active: sourceButton.hot || sourceButton.isCurrent + } } } } - } - - Column { - width: parent.width - visible: root.showSettings - spacing: Style.space(4) - - PanelSeparator { - foreground: root.barForeground - } - - PanelSectionHeader { - text: "Settings" - foreground: root.barForeground - } Column { - width: parent.width + visible: root.showSettings spacing: Style.space(4) - Toggle { - width: parent.width - label: "Show track title" - description: "Show the scrolling track title beside the waveform." - checked: String(root.setting("showTitle", true)).toLowerCase() === "true" + PanelSeparator { foreground: root.barForeground - accent: Color.accent - fontFamily: root.bar ? root.bar.fontFamily : Style.font.family - onClicked: root.setBooleanSetting("showTitle", - String(root.setting("showTitle", true)).toLowerCase() !== "true") } - Toggle { - width: parent.width - visible: String(root.setting("showTitle", true)).toLowerCase() === "true" - label: "Show artist in title" - description: "Show the artist name after the track title." - checked: String(root.setting("showArtist", false)).toLowerCase() === "true" + PanelSectionHeader { + text: "Settings" foreground: root.barForeground - accent: Color.accent - fontFamily: root.bar ? root.bar.fontFamily : Style.font.family - onClicked: root.setBooleanSetting("showArtist", - String(root.setting("showArtist", false)).toLowerCase() !== "true") } - Toggle { - width: parent.width - visible: String(root.setting("showTitle", true)).toLowerCase() === "true" - label: "Show full track info" - description: "Show the whole track title (and artist) without scrolling or clipping, instead of truncating and scrolling long titles." - checked: String(root.setting("showFullTitle", false)).toLowerCase() === "true" - foreground: root.barForeground - accent: Color.accent - fontFamily: root.bar ? root.bar.fontFamily : Style.font.family - onClicked: root.setBooleanSetting("showFullTitle", - String(root.setting("showFullTitle", false)).toLowerCase() !== "true") - } + Column { + spacing: Style.space(4) - Toggle { - width: parent.width - label: "Show album cover" - description: "Show the album art thumbnail between the waveform and the track title when a cover is available." - checked: String(root.setting("showCover", false)).toLowerCase() === "true" - foreground: root.barForeground - accent: Color.accent - fontFamily: root.bar ? root.bar.fontFamily : Style.font.family - onClicked: root.setBooleanSetting("showCover", - String(root.setting("showCover", false)).toLowerCase() !== "true") - } + Toggle { + label: "Show track title" + description: "Show the scrolling track title beside the waveform." + checked: String(root.setting("showTitle", true)).toLowerCase() === "true" + foreground: root.barForeground + accent: Color.accent + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + onClicked: root.setBooleanSetting("showTitle", + String(root.setting("showTitle", true)).toLowerCase() !== "true") + } - Toggle { - width: parent.width - label: "Show playback controls" - description: "Show previous, play/pause, and next buttons in the bar." - checked: String(root.setting("showControls", true)).toLowerCase() === "true" - foreground: root.barForeground - accent: Color.accent - fontFamily: root.bar ? root.bar.fontFamily : Style.font.family - onClicked: root.setBooleanSetting("showControls", - String(root.setting("showControls", true)).toLowerCase() !== "true") - } + Toggle { + visible: String(root.setting("showTitle", true)).toLowerCase() === "true" + label: "Show artist in title" + description: "Show the artist name after the track title." + checked: String(root.setting("showArtist", false)).toLowerCase() === "true" + foreground: root.barForeground + accent: Color.accent + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + onClicked: root.setBooleanSetting("showArtist", + String(root.setting("showArtist", false)).toLowerCase() !== "true") + } - Toggle { - width: parent.width - visible: String(root.setting("showControls", true)).toLowerCase() === "true" - label: "Group playback controls" - description: "Keep previous next to play/pause and next, beside the waveform." - checked: String(root.setting("groupControls", false)).toLowerCase() === "true" - foreground: root.barForeground - accent: Color.accent - fontFamily: root.bar ? root.bar.fontFamily : Style.font.family - onClicked: root.setBooleanSetting("groupControls", - String(root.setting("groupControls", false)).toLowerCase() !== "true") - } + Toggle { + visible: String(root.setting("showTitle", true)).toLowerCase() === "true" + label: "Show full track info" + description: "Show the whole track title (and artist) without scrolling or clipping, instead of truncating and scrolling long titles." + checked: String(root.setting("showFullTitle", false)).toLowerCase() === "true" + foreground: root.barForeground + accent: Color.accent + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + onClicked: root.setBooleanSetting("showFullTitle", + String(root.setting("showFullTitle", false)).toLowerCase() !== "true") + } - Toggle { - width: parent.width - label: "Hide when paused" - description: "Remove WaveBar from the bar while playback is paused." - checked: String(root.setting("hideWhenPaused", false)).toLowerCase() === "true" - foreground: root.barForeground - accent: Color.accent - fontFamily: root.bar ? root.bar.fontFamily : Style.font.family - onClicked: root.setBooleanSetting("hideWhenPaused", - String(root.setting("hideWhenPaused", false)).toLowerCase() !== "true") + Toggle { + label: "Show album cover" + description: "Show the album art thumbnail between the waveform and the track title when a cover is available." + checked: String(root.setting("showCover", false)).toLowerCase() === "true" + foreground: root.barForeground + accent: Color.accent + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + onClicked: root.setBooleanSetting("showCover", + String(root.setting("showCover", false)).toLowerCase() !== "true") + } + + Toggle { + label: "Show playback controls" + description: "Show previous, play/pause, and next buttons in the bar." + checked: String(root.setting("showControls", true)).toLowerCase() === "true" + foreground: root.barForeground + accent: Color.accent + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + onClicked: root.setBooleanSetting("showControls", + String(root.setting("showControls", true)).toLowerCase() !== "true") + } + + Toggle { + visible: String(root.setting("showControls", true)).toLowerCase() === "true" + label: "Group playback controls" + description: "Keep previous next to play/pause and next, beside the waveform." + checked: String(root.setting("groupControls", false)).toLowerCase() === "true" + foreground: root.barForeground + accent: Color.accent + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + onClicked: root.setBooleanSetting("groupControls", + String(root.setting("groupControls", false)).toLowerCase() !== "true") + } + + Toggle { + label: "Hide when paused" + description: "Remove WaveBar from the bar while playback is paused." + checked: String(root.setting("hideWhenPaused", false)).toLowerCase() === "true" + foreground: root.barForeground + accent: Color.accent + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + onClicked: root.setBooleanSetting("hideWhenPaused", + String(root.setting("hideWhenPaused", false)).toLowerCase() !== "true") + } } } } From cfb196e366daf942f2046d24a9f8f20d1b1e92d1 Mon Sep 17 00:00:00 2001 From: Jaxon Wright Date: Fri, 4 Sep 2026 00:42:47 -0400 Subject: [PATCH 09/10] Scroll the panel body instead of overflowing the card MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The panel body was a bare Column with no height bound to the card, so it drew past the card edge once the settings section was expanded. The settings list had its own fixed-height Flickable, which bounded only the toggles and left everything else to overflow. That inner Flickable also wrote `ScrollBar.vertical: ScrollBar { ... }` unqualified, but this file imports QtQuick.Controls as QQC, so the type was unresolvable and Panel.qml failed to load outright. The previous attempt had the same defect with a bare ScrollView. The omarchy shell panels this was modelled on import QtQuick.Controls unqualified, which is why the pattern looked correct when copied over. Replace both with a single Flickable filling the key catcher so the whole body scrolls as one list, and qualify the scrollbar as QQC.ScrollBar — matching the sourceView ListView, which already had it right. Drop the now-redundant settingsList wrapper Column. Verified on a 2560x1440 screen with settings expanded: content 915px in a 947px card against 1414px available, so the panel sizes to fit and the scrollbar stays hidden; it engages only when the content exceeds the card. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Rk2aBjm8DnhCip4mm5DbgH --- Panel.qml | 185 +++++++++++++++++++++++++++++------------------------- 1 file changed, 99 insertions(+), 86 deletions(-) diff --git a/Panel.qml b/Panel.qml index 2d269b8..d72cb1a 100644 --- a/Panel.qml +++ b/Panel.qml @@ -122,24 +122,24 @@ Panel { else if (key === "p" || key === "P") root.service.runAction("previous") } - ScrollView { - id: scrollArea + Flickable { + id: panelFlick anchors.fill: parent + contentWidth: width + contentHeight: content.implicitHeight clip: true - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - ScrollBar.vertical.policy: content.implicitHeight > height ? ScrollBar.AsNeeded : ScrollBar.AlwaysOff - Binding { - target: scrollArea.contentItem - property: "interactive" - value: content.implicitHeight > scrollArea.height - } + boundsBehavior: Flickable.StopAtBounds + flickableDirection: Flickable.VerticalFlick + interactive: contentHeight > height + QQC.ScrollBar.vertical: QQC.ScrollBar { policy: QQC.ScrollBar.AsNeeded } Column { id: content - width: scrollArea.availableWidth + width: panelFlick.width spacing: Style.space(10) Row { + width: parent.width spacing: Style.space(10) BorderSurface { @@ -175,6 +175,7 @@ Panel { anchors.verticalCenter: parent.verticalCenter Text { + width: parent.width textFormat: Text.PlainText text: root.service && root.service.title ? root.service.title : "Nothing playing" color: root.barForeground @@ -185,6 +186,7 @@ Panel { } Text { + width: parent.width visible: text !== "" textFormat: Text.PlainText text: root.service ? root.service.artist : "" @@ -195,6 +197,7 @@ Panel { } Text { + width: parent.width textFormat: Text.PlainText text: root.service ? root.service.identity : "" color: Qt.darker(root.barForeground, 1.55) @@ -206,6 +209,7 @@ Panel { } BorderSurface { + width: parent.width height: Style.space(88) radius: Style.cornerRadius color: Style.normalFillFor(root.barForeground, Color.accent) @@ -225,6 +229,7 @@ Panel { } Text { + width: parent.width textFormat: Text.PlainText text: root.captureMessage() color: Qt.darker(root.barForeground, 1.4) @@ -235,6 +240,7 @@ Panel { } Row { + width: parent.width visible: root.hasLength spacing: Style.space(6) @@ -309,6 +315,7 @@ Panel { } Row { + width: parent.width spacing: Style.space(8) Text { @@ -351,6 +358,7 @@ Panel { Column { id: sourceList + width: parent.width visible: root.service && root.service.focusedPlayers.length > 1 spacing: Style.space(4) @@ -361,6 +369,7 @@ Panel { ListView { id: sourceView + width: parent.width height: Math.min(contentHeight, Style.space(210)) spacing: Style.space(4) clip: true @@ -418,6 +427,7 @@ Panel { } Column { + width: parent.width visible: root.showSettings spacing: Style.space(4) @@ -430,88 +440,91 @@ Panel { foreground: root.barForeground } - Column { - spacing: Style.space(4) - - Toggle { - label: "Show track title" - description: "Show the scrolling track title beside the waveform." - checked: String(root.setting("showTitle", true)).toLowerCase() === "true" - foreground: root.barForeground - accent: Color.accent - fontFamily: root.bar ? root.bar.fontFamily : Style.font.family - onClicked: root.setBooleanSetting("showTitle", - String(root.setting("showTitle", true)).toLowerCase() !== "true") - } + Toggle { + width: parent.width + label: "Show track title" + description: "Show the scrolling track title beside the waveform." + checked: String(root.setting("showTitle", true)).toLowerCase() === "true" + foreground: root.barForeground + accent: Color.accent + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + onClicked: root.setBooleanSetting("showTitle", + String(root.setting("showTitle", true)).toLowerCase() !== "true") + } - Toggle { - visible: String(root.setting("showTitle", true)).toLowerCase() === "true" - label: "Show artist in title" - description: "Show the artist name after the track title." - checked: String(root.setting("showArtist", false)).toLowerCase() === "true" - foreground: root.barForeground - accent: Color.accent - fontFamily: root.bar ? root.bar.fontFamily : Style.font.family - onClicked: root.setBooleanSetting("showArtist", - String(root.setting("showArtist", false)).toLowerCase() !== "true") - } + Toggle { + width: parent.width + visible: String(root.setting("showTitle", true)).toLowerCase() === "true" + label: "Show artist in title" + description: "Show the artist name after the track title." + checked: String(root.setting("showArtist", false)).toLowerCase() === "true" + foreground: root.barForeground + accent: Color.accent + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + onClicked: root.setBooleanSetting("showArtist", + String(root.setting("showArtist", false)).toLowerCase() !== "true") + } - Toggle { - visible: String(root.setting("showTitle", true)).toLowerCase() === "true" - label: "Show full track info" - description: "Show the whole track title (and artist) without scrolling or clipping, instead of truncating and scrolling long titles." - checked: String(root.setting("showFullTitle", false)).toLowerCase() === "true" - foreground: root.barForeground - accent: Color.accent - fontFamily: root.bar ? root.bar.fontFamily : Style.font.family - onClicked: root.setBooleanSetting("showFullTitle", - String(root.setting("showFullTitle", false)).toLowerCase() !== "true") - } + Toggle { + width: parent.width + visible: String(root.setting("showTitle", true)).toLowerCase() === "true" + label: "Show full track info" + description: "Show the whole track title (and artist) without scrolling or clipping, instead of truncating and scrolling long titles." + checked: String(root.setting("showFullTitle", false)).toLowerCase() === "true" + foreground: root.barForeground + accent: Color.accent + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + onClicked: root.setBooleanSetting("showFullTitle", + String(root.setting("showFullTitle", false)).toLowerCase() !== "true") + } - Toggle { - label: "Show album cover" - description: "Show the album art thumbnail between the waveform and the track title when a cover is available." - checked: String(root.setting("showCover", false)).toLowerCase() === "true" - foreground: root.barForeground - accent: Color.accent - fontFamily: root.bar ? root.bar.fontFamily : Style.font.family - onClicked: root.setBooleanSetting("showCover", - String(root.setting("showCover", false)).toLowerCase() !== "true") - } + Toggle { + width: parent.width + label: "Show album cover" + description: "Show the album art thumbnail between the waveform and the track title when a cover is available." + checked: String(root.setting("showCover", false)).toLowerCase() === "true" + foreground: root.barForeground + accent: Color.accent + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + onClicked: root.setBooleanSetting("showCover", + String(root.setting("showCover", false)).toLowerCase() !== "true") + } - Toggle { - label: "Show playback controls" - description: "Show previous, play/pause, and next buttons in the bar." - checked: String(root.setting("showControls", true)).toLowerCase() === "true" - foreground: root.barForeground - accent: Color.accent - fontFamily: root.bar ? root.bar.fontFamily : Style.font.family - onClicked: root.setBooleanSetting("showControls", - String(root.setting("showControls", true)).toLowerCase() !== "true") - } + Toggle { + width: parent.width + label: "Show playback controls" + description: "Show previous, play/pause, and next buttons in the bar." + checked: String(root.setting("showControls", true)).toLowerCase() === "true" + foreground: root.barForeground + accent: Color.accent + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + onClicked: root.setBooleanSetting("showControls", + String(root.setting("showControls", true)).toLowerCase() !== "true") + } - Toggle { - visible: String(root.setting("showControls", true)).toLowerCase() === "true" - label: "Group playback controls" - description: "Keep previous next to play/pause and next, beside the waveform." - checked: String(root.setting("groupControls", false)).toLowerCase() === "true" - foreground: root.barForeground - accent: Color.accent - fontFamily: root.bar ? root.bar.fontFamily : Style.font.family - onClicked: root.setBooleanSetting("groupControls", - String(root.setting("groupControls", false)).toLowerCase() !== "true") - } + Toggle { + width: parent.width + visible: String(root.setting("showControls", true)).toLowerCase() === "true" + label: "Group playback controls" + description: "Keep previous next to play/pause and next, beside the waveform." + checked: String(root.setting("groupControls", false)).toLowerCase() === "true" + foreground: root.barForeground + accent: Color.accent + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + onClicked: root.setBooleanSetting("groupControls", + String(root.setting("groupControls", false)).toLowerCase() !== "true") + } - Toggle { - label: "Hide when paused" - description: "Remove WaveBar from the bar while playback is paused." - checked: String(root.setting("hideWhenPaused", false)).toLowerCase() === "true" - foreground: root.barForeground - accent: Color.accent - fontFamily: root.bar ? root.bar.fontFamily : Style.font.family - onClicked: root.setBooleanSetting("hideWhenPaused", - String(root.setting("hideWhenPaused", false)).toLowerCase() !== "true") - } + Toggle { + width: parent.width + label: "Hide when paused" + description: "Remove WaveBar from the bar while playback is paused." + checked: String(root.setting("hideWhenPaused", false)).toLowerCase() === "true" + foreground: root.barForeground + accent: Color.accent + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + onClicked: root.setBooleanSetting("hideWhenPaused", + String(root.setting("hideWhenPaused", false)).toLowerCase() !== "true") } } } From 20e38f509e0dec86422300f1e4aeb69052d444a5 Mon Sep 17 00:00:00 2001 From: Jaxon Wright Date: Fri, 4 Sep 2026 00:47:46 -0400 Subject: [PATCH 10/10] Fix the media button's tooltip target and hover reference mediaButton declares `tooltipHovered: mediaHover.containsMouse`, but the MouseArea it names had lost its `id: mediaHover`, so every load threw "ReferenceError: mediaHover is not defined" from BarWidget.qml:117. The same edit also retargeted the tooltip from mediaButton to root. The bar resolves tooltip visibility through `target.tooltipHovered === true` (Bar.qml:174) and qs.Ui/BarWidget does not define that property, so the media tooltip's hover check silently evaluated false. Restore the id and target mediaButton, matching prevLeft, prevButton, playButton and nextButton: the hoverable Item owns tooltipHovered, its MouseArea carries the matching Hover id, and showTooltip receives that Item. The shell now loads the plugin with no warnings. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Rk2aBjm8DnhCip4mm5DbgH --- BarWidget.qml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/BarWidget.qml b/BarWidget.qml index c335b68..727e078 100644 --- a/BarWidget.qml +++ b/BarWidget.qml @@ -177,6 +177,7 @@ BarWidget { } MouseArea { + id: mediaHover anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor @@ -187,9 +188,9 @@ BarWidget { root.waveformService.runAction(wheel.angleDelta.y > 0 ? "previous" : "next") } onEntered: if (root.bar && root.waveformService) - root.bar.showTooltip(root, root.waveformService.title + root.bar.showTooltip(mediaButton, root.waveformService.title + (root.waveformService.artist ? " — " + root.waveformService.artist : "")) - onExited: if (root.bar) root.bar.hideTooltip(root) + onExited: if (root.bar) root.bar.hideTooltip(mediaButton) } }