diff --git a/modules/BarIcon.qml b/modules/BarIcon.qml index a034e04..a9d0e89 100644 --- a/modules/BarIcon.qml +++ b/modules/BarIcon.qml @@ -14,9 +14,12 @@ Text { text: _displayIcon onIconChanged: { - _pendingIcon = icon; - if (!_crossfade.running) + if (_crossfade.running) { + _pendingIcon = icon; + } else { + _pendingIcon = icon; _crossfade.start(); + } } SequentialAnimation { diff --git a/modules/MprisMenu.qml b/modules/MprisMenu.qml index c81176e..b4bfb29 100644 --- a/modules/MprisMenu.qml +++ b/modules/MprisMenu.qml @@ -9,12 +9,7 @@ M.PopupPanel { required property MprisPlayer player - function _fmtTime(ms) { - const s = Math.floor(ms / 1000); - const m = Math.floor(s / 60); - return m + ":" + String(s % 60).padStart(2, "0"); - } - + // Album art Item { width: menuWindow.panelWidth height: _artImg.status === Image.Ready ? 140 : 60 @@ -33,6 +28,7 @@ M.PopupPanel { visible: status === Image.Ready } + // Gradient fade at the bottom so art blends into the panel Rectangle { anchors.left: parent.left anchors.right: parent.right @@ -45,6 +41,7 @@ M.PopupPanel { } } + // Fallback icon when no art Text { anchors.centerIn: parent text: "\uF001" @@ -55,6 +52,7 @@ M.PopupPanel { } } + // Track info Item { width: menuWindow.panelWidth height: titleCol.implicitHeight + 8 @@ -84,7 +82,7 @@ M.PopupPanel { const p = menuWindow.player; if (!p) return ""; const artist = Array.isArray(p.trackArtists) ? p.trackArtists.join(", ") : (p.trackArtists || ""); - return [artist, p.trackAlbum].filter(s => s).join(" \u2014 "); + return [artist, p.trackAlbum].filter(s => s).join(" — "); } color: M.Theme.base04 font.pixelSize: M.Theme.fontSize - 1 @@ -95,6 +93,7 @@ M.PopupPanel { } } + // Progress bar Item { width: menuWindow.panelWidth height: 20 @@ -103,25 +102,39 @@ M.PopupPanel { readonly property real dur: menuWindow.player?.length ?? 0 readonly property real frac: dur > 0 ? pos / dur : 0 + // Time labels Text { anchors.left: parent.left anchors.leftMargin: 12 anchors.verticalCenter: parent.verticalCenter - text: menuWindow._fmtTime(parent.pos) + text: _fmt(parent.pos) color: M.Theme.base04 font.pixelSize: M.Theme.fontSize - 2 font.family: M.Theme.fontFamily + + function _fmt(ms) { + const s = Math.floor(ms / 1000); + const m = Math.floor(s / 60); + return m + ":" + String(s % 60).padStart(2, "0"); + } } Text { anchors.right: parent.right anchors.rightMargin: 12 anchors.verticalCenter: parent.verticalCenter - text: menuWindow._fmtTime(parent.dur) + text: _fmt(parent.dur) color: M.Theme.base04 font.pixelSize: M.Theme.fontSize - 2 font.family: M.Theme.fontFamily + + function _fmt(ms) { + const s = Math.floor(ms / 1000); + const m = Math.floor(s / 60); + return m + ":" + String(s % 60).padStart(2, "0"); + } } + // Bar Item { anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter @@ -142,6 +155,7 @@ M.PopupPanel { } } + // Transport controls Item { width: menuWindow.panelWidth height: 36 @@ -150,6 +164,7 @@ M.PopupPanel { anchors.centerIn: parent spacing: 24 + // Previous Text { text: "\uF048" color: menuWindow.player?.canGoPrevious ? M.Theme.base05 : M.Theme.base03 @@ -165,6 +180,7 @@ M.PopupPanel { } } + // Play/Pause Text { text: menuWindow.player?.playbackState === MprisPlaybackState.Playing ? "\uF04C" : "\uF04B" color: M.Theme.base0E @@ -179,6 +195,7 @@ M.PopupPanel { } } + // Next Text { text: "\uF051" color: menuWindow.player?.canGoNext ? M.Theme.base05 : M.Theme.base03 @@ -196,6 +213,7 @@ M.PopupPanel { } } + // Player name Item { width: menuWindow.panelWidth height: 20 diff --git a/modules/Tray.qml b/modules/Tray.qml index 160121d..5f0650b 100644 --- a/modules/Tray.qml +++ b/modules/Tray.qml @@ -20,7 +20,7 @@ RowLayout { id: iconItem required property SystemTrayItem modelData - readonly property bool _needsAttention: modelData.status === 2 + readonly property bool _needsAttention: modelData.status === SystemTrayItemStatus.NeedsAttention property bool _hovered: false property real _pulseOpacity: 1 diff --git a/modules/Volume.qml b/modules/Volume.qml index 031c9cd..c631191 100644 --- a/modules/Volume.qml +++ b/modules/Volume.qml @@ -7,6 +7,7 @@ import "." as M M.BarSection { id: root spacing: M.Theme.moduleSpacing + // No tooltip — the panel replaces it tooltip: "" PwObjectTracker { @@ -16,32 +17,20 @@ M.BarSection { readonly property var sink: Pipewire.defaultAudioSink readonly property real volume: sink?.audio?.volume ?? 0 readonly property bool muted: sink?.audio?.muted ?? false - readonly property string _volumeIcon: muted ? "\uF026" : (volume > 0.5 ? "\uF028" : (volume > 0 ? "\uF027" : "\uF026")) - readonly property color _volumeColor: muted ? M.Theme.base04 : M.Theme.base0E - - readonly property var _sinkList: { - const sinks = []; - if (Pipewire.nodes) { - for (const node of Pipewire.nodes.values) - if (!node.isStream && node.isSink) - sinks.push(node); - } - return sinks; - } property bool _expanded: false property bool _panelHovered: false readonly property bool _showPanel: root._hovered || _panelHovered || _expanded M.BarIcon { - icon: root._volumeIcon - color: root._volumeColor + icon: root.muted ? "\uF026" : (root.volume > 0.5 ? "\uF028" : (root.volume > 0 ? "\uF027" : "\uF026")) + color: root.muted ? M.Theme.base04 : M.Theme.base0E anchors.verticalCenter: parent.verticalCenter } M.BarLabel { label: Math.round(root.volume * 100) + "%" minText: "100%" - color: root._volumeColor + color: root.muted ? M.Theme.base04 : M.Theme.base0E anchors.verticalCenter: parent.verticalCenter } @@ -121,25 +110,25 @@ M.BarSection { // Click inside panel doesn't dismiss } - Rectangle { - x: panelContent.x - y: panelContent.y - width: panelContent.width - height: panelContent.height - color: M.Theme.base01 - opacity: panelContent.opacity * Math.max(M.Theme.barOpacity, 0.85) - topLeftRadius: 0 - topRightRadius: 0 - bottomLeftRadius: M.Theme.radius - bottomRightRadius: M.Theme.radius - } - Column { id: panelContent width: 220 opacity: 0 y: -height + // Background + Rectangle { + width: parent.width + height: parent.height + color: M.Theme.base01 + opacity: Math.max(M.Theme.barOpacity, 0.85) + topLeftRadius: 0 + topRightRadius: 0 + bottomLeftRadius: M.Theme.radius + bottomRightRadius: M.Theme.radius + z: -1 + } + // Compact: slider row Item { width: parent.width @@ -151,8 +140,8 @@ M.BarSection { anchors.left: parent.left anchors.leftMargin: 12 anchors.verticalCenter: parent.verticalCenter - text: root._volumeIcon - color: root._volumeColor + text: root.muted ? "\uF026" : (root.volume > 0.5 ? "\uF028" : "\uF027") + color: root.muted ? M.Theme.base04 : M.Theme.base0E font.pixelSize: M.Theme.fontSize + 2 font.family: M.Theme.iconFontFamily @@ -182,7 +171,7 @@ M.BarSection { Rectangle { width: parent.width * Math.min(1, Math.max(0, root.volume)) height: parent.height - color: root._volumeColor + color: root.muted ? M.Theme.base04 : M.Theme.base0E radius: 3 Behavior on width { NumberAnimation { duration: 80 } } @@ -260,7 +249,16 @@ M.BarSection { } Repeater { - model: root._sinkList + model: { + const sinks = []; + if (Pipewire.nodes) { + for (const node of Pipewire.nodes.values) { + if (!node.isStream && node.isSink) + sinks.push(node); + } + } + return sinks; + } delegate: Item { required property var modelData