From a49534d7f7611e21bc65de12916564908e47dd3c Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sat, 25 Apr 2026 15:51:53 +0200 Subject: [PATCH 1/3] feat(Bar/Widgets/MediaMini): change volume on scroll Signed-off-by: Sefa Eyeoglu --- Modules/Bar/Widgets/MediaMini.qml | 24 ++++++++++++++++++++++++ Services/Control/IPCService.qml | 8 ++++++++ Services/Media/MediaService.qml | 16 ++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/Modules/Bar/Widgets/MediaMini.qml b/Modules/Bar/Widgets/MediaMini.qml index d7b6b74aa5..df9fddb077 100644 --- a/Modules/Bar/Widgets/MediaMini.qml +++ b/Modules/Bar/Widgets/MediaMini.qml @@ -64,6 +64,9 @@ Item { readonly property bool shouldHideEmpty: !hasPlayer && hideMode === "hidden" readonly property bool isHidden: shouldHideIdle || shouldHideEmpty + // Volume scroll + property int wheelAccumulator: 0 + // Title readonly property string title: { if (!hasPlayer) @@ -392,6 +395,27 @@ Item { cursorShape: Qt.PointingHandCursor acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton | Qt.ForwardButton | Qt.BackButton + onWheel: function (ev) { + // Hide tooltip as soon as the user starts scrolling to adjust volume + TooltipService.hide(); + + ev.accepted = true; + + var delta = ev.pixelDelta.y; + + if (ev.inverted) + delta *= -1; + + wheelAccumulator += delta; + if (wheelAccumulator >= 120) { + wheelAccumulator = 0; + MediaService.increaseVolume(); + } else if (wheelAccumulator <= -120) { + wheelAccumulator = 0; + MediaService.decreaseVolume(); + } + } + onClicked: mouse => { TooltipService.hide(); if (mouse.button === Qt.LeftButton) { diff --git a/Services/Control/IPCService.qml b/Services/Control/IPCService.qml index d8308e5de4..b3f0acea35 100644 --- a/Services/Control/IPCService.qml +++ b/Services/Control/IPCService.qml @@ -814,6 +814,14 @@ Singleton { } MediaService.seekByRatio(positionVal); } + + function increase() { + MediaService.increaseVolume(); + } + + function decrease() { + MediaService.decreaseVolume(); + } } IpcHandler { diff --git a/Services/Media/MediaService.qml b/Services/Media/MediaService.qml index e30762d620..bf1e17a953 100644 --- a/Services/Media/MediaService.qml +++ b/Services/Media/MediaService.qml @@ -45,6 +45,8 @@ Singleton { property string lengthString: formatTime(trackLength) property real infiniteTrackLength: 922337203685 + readonly property real stepVolume: Settings.data.audio.volumeStep / 100.0 + Component.onCompleted: { updateCurrentPlayer(); } @@ -292,6 +294,20 @@ Singleton { } } + function increaseVolume() { + let target = currentPlayer ? (currentPlayer._controlTarget || currentPlayer) : null; + if (target && target.canControl && target.volumeSupported) { + target.volume = Math.min(1.0, target.volume + stepVolume); + } + } + + function decreaseVolume() { + let target = currentPlayer ? (currentPlayer._controlTarget || currentPlayer) : null; + if (target && target.canControl && target.volumeSupported) { + target.volume = Math.max(0.0, target.volume - stepVolume); + } + } + // Update progress bar every second while playing Timer { id: positionTimer From 8965aa6581c38588230a991e104229a1af128e03 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sat, 25 Apr 2026 16:25:56 +0200 Subject: [PATCH 2/3] feat(OSD): add support for media volume Signed-off-by: Sefa Eyeoglu --- Assets/Translations/en-GB.json | 2 ++ Assets/Translations/en.json | 2 ++ Assets/settings-default.json | 3 ++- Commons/Settings.qml | 2 +- Modules/OSD/OSD.qml | 22 ++++++++++++++++--- .../Panels/Settings/Tabs/Osd/EventsSubTab.qml | 4 ++++ Services/Media/MediaService.qml | 2 ++ 7 files changed, 32 insertions(+), 5 deletions(-) diff --git a/Assets/Translations/en-GB.json b/Assets/Translations/en-GB.json index 38ed26c1e2..5b4c843a27 100644 --- a/Assets/Translations/en-GB.json +++ b/Assets/Translations/en-GB.json @@ -1574,6 +1574,8 @@ "types-lockkey-label": "Lock keys", "types-media-description": "Show OSD when media playback state changes (play, pause, skip).", "types-media-label": "Media playback", + "types-media-volume-description": "Show OSD when media volume changes.", + "types-media-volume-label": "Media volume", "types-title": "OSD trigger events", "types-volume-description": "Show OSD when audio output volume changes.", "types-volume-label": "Output volume" diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index 24dca05a72..b8fca75fd2 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -1574,6 +1574,8 @@ "types-lockkey-label": "Lock keys", "types-media-description": "Show OSD when media playback state changes (play, pause, skip).", "types-media-label": "Media playback", + "types-media-volume-description": "Show OSD when media volume changes.", + "types-media-volume-label": "Media volume", "types-title": "OSD trigger events", "types-volume-description": "Show OSD when audio output volume changes.", "types-volume-label": "Output volume" diff --git a/Assets/settings-default.json b/Assets/settings-default.json index 262d6a7604..3fe51c0cb7 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -478,7 +478,8 @@ "enabledTypes": [ 0, 1, - 2 + 2, + 4 ], "monitors": [] }, diff --git a/Commons/Settings.qml b/Commons/Settings.qml index aecf878359..384b02165a 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -707,7 +707,7 @@ Singleton { property int autoHideMs: 2000 property bool overlayLayer: true property real backgroundOpacity: 1.0 - property list enabledTypes: [OSD.Type.Volume, OSD.Type.InputVolume, OSD.Type.Brightness] + property list enabledTypes: [OSD.Type.Volume, OSD.Type.InputVolume, OSD.Type.Brightness, OSD.Type.MediaVolume] property list monitors: [] // holds osd visibility per monitor } diff --git a/Modules/OSD/OSD.qml b/Modules/OSD/OSD.qml index a64eee6123..f1cf4da575 100644 --- a/Modules/OSD/OSD.qml +++ b/Modules/OSD/OSD.qml @@ -19,7 +19,8 @@ Variants { Volume, InputVolume, Brightness, - LockKey + LockKey, + MediaVolume } model: Quickshell.screens.filter(screen => (Settings.data.osd.monitors.includes(screen.name) || Settings.data.osd.monitors.length === 0) && Settings.data.osd.enabled) @@ -45,6 +46,7 @@ Variants { readonly property bool isMuted: AudioService.muted readonly property real currentInputVolume: AudioService.inputVolume readonly property bool isInputMuted: AudioService.inputMuted + readonly property real currentMediaVolume: MediaService.volume readonly property real epsilon: 0.005 // LockKey OSD enabled state (reactive to settings) @@ -72,6 +74,9 @@ Variants { return currentVolume <= 0.5 ? "volume-low" : "volume-high"; case OSD.Type.InputVolume: return isInputMuted ? "microphone-off" : "microphone"; + case OSD.Type.MediaVolume: + // Show music-off icon when volume is effectively 0% (within rounding threshold) + return currentMediaVolume < root.epsilon ? "music-off" : "music"; case OSD.Type.Brightness: // Show sun-off icon when brightness is effectively 0% (within rounding threshold) if (currentBrightness < root.epsilon) @@ -90,6 +95,8 @@ Variants { return isMuted ? 0 : currentVolume; case OSD.Type.InputVolume: return isInputMuted ? 0 : currentInputVolume; + case OSD.Type.MediaVolume: + return currentMediaVolume; case OSD.Type.Brightness: return currentBrightness; case OSD.Type.LockKey: @@ -124,7 +131,7 @@ Variants { } function getProgressColor() { - const isMutedState = (currentOSDType === OSD.Type.Volume && isMuted) || (currentOSDType === OSD.Type.InputVolume && isInputMuted); + const isMutedState = (currentOSDType === OSD.Type.Volume && isMuted) || (currentOSDType === OSD.Type.InputVolume && isInputMuted) || (currentOSDType === OSD.Type.MediaVolume && currentMediaVolume < root.epsilon); if (isMutedState) { return Color.mError; } @@ -150,7 +157,7 @@ Variants { } function getIconColor() { - const isMutedState = (currentOSDType === OSD.Type.Volume && isMuted) || (currentOSDType === OSD.Type.InputVolume && isInputMuted); + const isMutedState = (currentOSDType === OSD.Type.Volume && isMuted) || (currentOSDType === OSD.Type.InputVolume && isInputMuted) || (currentOSDType === OSD.Type.MediaVolume && currentMediaVolume < root.epsilon); if (isMutedState) return Color.mError; @@ -313,6 +320,15 @@ Variants { } } + // MediaService monitoring + Connections { + target: MediaService + + function onVolumeChanged() { + showOSD(OSD.Type.MediaVolume); + } + } + // Brightness monitoring Connections { target: BrightnessService diff --git a/Modules/Panels/Settings/Tabs/Osd/EventsSubTab.qml b/Modules/Panels/Settings/Tabs/Osd/EventsSubTab.qml index c780d7d7a8..2cb316d1c7 100644 --- a/Modules/Panels/Settings/Tabs/Osd/EventsSubTab.qml +++ b/Modules/Panels/Settings/Tabs/Osd/EventsSubTab.qml @@ -30,6 +30,10 @@ ColumnLayout { { type: OSD.Type.LockKey, key: "types-lockkey" + }, + { + type: OSD.Type.MediaVolume, + key: "types-media-volume" } ] delegate: NCheckbox { diff --git a/Services/Media/MediaService.qml b/Services/Media/MediaService.qml index bf1e17a953..7d0c65e08b 100644 --- a/Services/Media/MediaService.qml +++ b/Services/Media/MediaService.qml @@ -41,6 +41,8 @@ Singleton { property bool canGoNext: currentPlayer ? currentPlayer.canGoNext : false property bool canGoPrevious: currentPlayer ? currentPlayer.canGoPrevious : false property bool canSeek: currentPlayer ? currentPlayer.canSeek : false + property bool isMuted: currentPlayer ? currentPlayer.volume < root.epsilon : false + property real volume: currentPlayer ? currentPlayer.volume : 0.0 property string positionString: formatTime(currentPosition) property string lengthString: formatTime(trackLength) property real infiniteTrackLength: 922337203685 From 78db6c60dbb7f411856d0ebec3bdbab856fddb57 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sat, 25 Apr 2026 19:30:28 +0200 Subject: [PATCH 3/3] feat(MediaCard,MediaPlayerPanel): add volume slider Signed-off-by: Sefa Eyeoglu --- Modules/Cards/MediaCard.qml | 71 +++++++++++++++ Modules/Panels/Media/MediaPlayerPanel.qml | 103 ++++++++++++++++++++++ Services/Media/MediaService.qml | 15 ++++ 3 files changed, 189 insertions(+) diff --git a/Modules/Cards/MediaCard.qml b/Modules/Cards/MediaCard.qml index ab604be125..ba5fd5899c 100644 --- a/Modules/Cards/MediaCard.qml +++ b/Modules/Cards/MediaCard.qml @@ -420,6 +420,77 @@ NBox { } } + // Volume slider + Item { + id: volumeWrapper + visible: MediaService.volumeSupported + Layout.fillWidth: true + height: Style.baseWidgetSize * 0.5 + + property real localVolume: -1 + property real lastSentVolume: -1 + property real volumeEpsilon: 0.01 + property real volume: { + if (!MediaService.volumeSupported) + return 0; + return MediaService.volume; + } + + Timer { + id: volumeDebounce + interval: 75 + repeat: false + onTriggered: { + if (volumeWrapper.localVolume >= 0) { + const next = Math.max(0, Math.min(1, volumeWrapper.localVolume)); + if (volumeWrapper.lastSentVolume < 0 || Math.abs(next - volumeWrapper.lastSentVolume) >= volumeWrapper.volumeEpsilon) { + MediaService.setVolume(next); + volumeWrapper.lastSentVolume = next; + } + } + } + } + + RowLayout { + anchors.fill: parent + spacing: 2 + + NIcon { + icon: "volume" + color: Color.mOnSurfaceVariant + } + + NSlider { + id: volumeSlider + Layout.fillWidth: true + from: 0 + to: 1 + stepSize: 0 + snapAlways: false + heightRatio: 0.6 + + value: MediaService.volume + + onMoved: { + volumeWrapper.localVolume = value; + volumeDebounce.restart(); + } + onPressedChanged: { + if (pressed) { + volumeWrapper.localVolume = value; + MediaService.setVolume(value); + volumeWrapper.lastSentVolume = value; + } else { + volumeDebounce.stop(); + MediaService.setVolume(value); + volumeWrapper.localVolume = -1; + volumeWrapper.lastSentVolume = -1; + } + } + } + } + } + // Spacer to push media controls down Item { Layout.preferredHeight: Style.marginL diff --git a/Modules/Panels/Media/MediaPlayerPanel.qml b/Modules/Panels/Media/MediaPlayerPanel.qml index 081c43dd5d..9f3e238559 100644 --- a/Modules/Panels/Media/MediaPlayerPanel.qml +++ b/Modules/Panels/Media/MediaPlayerPanel.qml @@ -478,6 +478,109 @@ SmartPanel { } } + Item { + id: volumeWrapper + visible: MediaService.volumeSupported + Layout.fillWidth: true + Layout.preferredHeight: volumeColumn.implicitHeight + + property real localVolume: -1 + property real lastSentVolume: -1 + property real volumeEpsilon: 0.01 + property real volume: { + if (!MediaService.volumeSupported) + return 0; + return MediaService.volume; + } + + Timer { + id: volumeDebounce + interval: 75 + repeat: false + onTriggered: { + if (volumeWrapper.localVolume >= 0) { + const next = Math.max(0, Math.min(1, volumeWrapper.localVolume)); + if (volumeWrapper.lastSentVolume < 0 || Math.abs(next - volumeWrapper.lastSentVolume) >= volumeWrapper.volumeEpsilon) { + MediaService.setVolume(next); + volumeWrapper.lastSentVolume = next; + } + } + } + } + + + + ColumnLayout { + id: volumeColumn + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: 2 + + RowLayout { + anchors.fill: parent + spacing: 2 + + NIcon { + icon: "volume" + color: Color.mOnSurfaceVariant + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: root.compactMode ? (Style.baseWidgetSize * 0.4) : (Style.baseWidgetSize * 0.5) + + NSlider { + id: volumeSlider + anchors.fill: parent + from: 0 + to: 1 + stepSize: 0 + snapAlways: false + heightRatio: 0.4 + + value: MediaService.volume + + onMoved: { + volumeWrapper.localVolume = value; + volumeDebounce.restart(); + } + onPressedChanged: { + if (pressed) { + volumeWrapper.localVolume = value; + MediaService.setVolume(value); + volumeWrapper.lastSentVolume = value; + } else { + volumeDebounce.stop(); + MediaService.setVolume(value); + volumeWrapper.localVolume = -1; + volumeWrapper.lastSentVolume = -1; + } + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 0 + + Item { + Layout.fillWidth: true + Layout.minimumWidth: 0 + } + + NText { + text: MediaService.volumeString + pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + horizontalAlignment: Text.AlignRight + visible: volumeWrapper.visible + } + } + } + } + Item { Layout.preferredHeight: root.isSideBySide ? Style.marginM : Style.marginS } diff --git a/Services/Media/MediaService.qml b/Services/Media/MediaService.qml index 7d0c65e08b..9c197049d0 100644 --- a/Services/Media/MediaService.qml +++ b/Services/Media/MediaService.qml @@ -25,6 +25,12 @@ Singleton { } } + function formatVolume(volume) { + if (isNaN(volume) || volume < 0) + volume = 0; + return Math.floor(volume * 100) + " %"; + } + property var currentPlayer: null property string playerIdentity: currentPlayer ? (currentPlayer.identity || "") : "" property real currentPosition: 0 @@ -43,6 +49,8 @@ Singleton { property bool canSeek: currentPlayer ? currentPlayer.canSeek : false property bool isMuted: currentPlayer ? currentPlayer.volume < root.epsilon : false property real volume: currentPlayer ? currentPlayer.volume : 0.0 + property bool volumeSupported: currentPlayer ? currentPlayer.volumeSupported : false + property string volumeString: formatVolume(volume) property string positionString: formatTime(currentPosition) property string lengthString: formatTime(trackLength) property real infiniteTrackLength: 922337203685 @@ -296,6 +304,13 @@ Singleton { } } + function setVolume(value) { + let target = currentPlayer ? (currentPlayer._controlTarget || currentPlayer) : null; + if (target && target.canControl && target.volumeSupported) { + target.volume = Math.max(0.0, Math.min(1.0, value)); + } + } + function increaseVolume() { let target = currentPlayer ? (currentPlayer._controlTarget || currentPlayer) : null; if (target && target.canControl && target.volumeSupported) {