From de3e8cffa073dcae2c66c40565ba12d1c1412340 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hu=E1=BB=B3nh=20Thi=E1=BB=87n=20L=E1=BB=99c?= Date: Mon, 29 Jun 2026 23:01:19 +0700 Subject: [PATCH] feat(mpris): allow excluding specific media players by identity (#2712) * feat(mpris): allow excluding specific media players by identity * chore(translation): update settings search index and clean trailing whitespace --- quickshell/Common/SettingsData.qml | 27 +++ quickshell/Common/settings/SettingsSpec.js | 1 + .../Modals/Settings/SettingsContent.qml | 4 +- .../Modules/Settings/MediaPlayerTab.qml | 155 ++++++++++++++++++ quickshell/Services/MprisController.qml | 33 +++- .../translations/settings_search_index.json | 20 +++ 6 files changed, 238 insertions(+), 2 deletions(-) diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index 5619f754..909911e7 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -448,6 +448,7 @@ Singleton { property string audioScrollMode: "volume" property int audioWheelScrollAmount: 5 property bool audioDeviceScrollVolumeEnabled: false + property var mediaExcludePlayers: [] property bool clockCompactMode: false property int focusedWindowSize: 1 property bool focusedWindowCompactMode: false @@ -3062,6 +3063,32 @@ Singleton { saveSettings(); } + function addMediaExcludePlayer(identity) { + if (identity === undefined || identity === null) + return; + var normalizedIdentity = identity.toString().trim().toLowerCase(); + if (!normalizedIdentity) + return; + var list = mediaExcludePlayers ? mediaExcludePlayers.slice() : []; + var normalizedList = list.map(function(id) { + return id ? id.toString().trim().toLowerCase() : ""; + }); + if (normalizedList.indexOf(normalizedIdentity) >= 0) + return; + list.push(normalizedIdentity); + mediaExcludePlayers = list; + saveSettings(); + } + + function removeMediaExcludePlayer(index) { + var list = mediaExcludePlayers ? mediaExcludePlayers.slice() : []; + if (index < 0 || index >= list.length) + return; + list.splice(index, 1); + mediaExcludePlayers = list; + saveSettings(); + } + property bool _pendingExpandNotificationRules: false property int _pendingNotificationRuleIndex: -1 diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index 97570e2f..fe09468f 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -184,6 +184,7 @@ var SPEC = { audioScrollMode: { def: "volume" }, audioWheelScrollAmount: { def: 5 }, audioDeviceScrollVolumeEnabled: { def: false }, + mediaExcludePlayers: { def: [] }, clockCompactMode: { def: false }, focusedWindowCompactMode: { def: false }, focusedWindowSize: { def: 1 }, diff --git a/quickshell/Modals/Settings/SettingsContent.qml b/quickshell/Modals/Settings/SettingsContent.qml index 2a2eb8c1..4de08185 100644 --- a/quickshell/Modals/Settings/SettingsContent.qml +++ b/quickshell/Modals/Settings/SettingsContent.qml @@ -434,7 +434,9 @@ FocusScope { visible: active focus: active - sourceComponent: MediaPlayerTab {} + sourceComponent: MediaPlayerTab { + parentModal: root.parentModal + } onActiveChanged: { if (active && item) diff --git a/quickshell/Modules/Settings/MediaPlayerTab.qml b/quickshell/Modules/Settings/MediaPlayerTab.qml index c6b13adb..43fcb890 100644 --- a/quickshell/Modules/Settings/MediaPlayerTab.qml +++ b/quickshell/Modules/Settings/MediaPlayerTab.qml @@ -2,10 +2,22 @@ import QtQuick import qs.Common import qs.Widgets import qs.Modules.Settings.Widgets +import qs.Services Item { id: root + property var desktopApps: [] + property var parentModal: null + + Component.onCompleted: { + desktopApps = AppSearchService.getVisibleApplications() || []; + } + + Component.onDestruction: { + desktopApps = []; + } + DankFlickable { anchors.fill: parent clip: true @@ -121,6 +133,149 @@ Item { onToggled: checked => SettingsData.set("audioDeviceScrollVolumeEnabled", checked) } } + + SettingsCard { + width: parent.width + iconName: "do_not_disturb_on" + title: I18n.tr("Excluded Media Players") + settingKey: "mediaExcludePlayers" + tags: ["media", "music", "exclude", "ignore", "player", "mpris"] + + Column { + width: parent.width + spacing: Theme.spacingM + + StyledText { + text: I18n.tr("Prevent specific applications from displaying in the media controllers (e.g., browser audio streams, background tools). Matches player identity or desktop file name case-insensitively.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + } + + Row { + width: parent.width + spacing: Theme.spacingS + + DankTextField { + id: newExcludePlayerField + width: parent.width - addBtn.width - selectAppBtn.width - Theme.spacingS * 2 + height: 36 + placeholderText: I18n.tr("App name or identity (e.g., firefox)") + font.pixelSize: Theme.fontSizeSmall + onAccepted: { + if (text.trim() !== "") { + SettingsData.addMediaExcludePlayer(text.trim()); + text = ""; + } + } + } + + DankActionButton { + id: addBtn + buttonSize: 36 + iconName: "add" + iconSize: 20 + backgroundColor: Theme.primary + iconColor: Theme.onPrimary + onClicked: { + if (newExcludePlayerField.text.trim() !== "") { + SettingsData.addMediaExcludePlayer(newExcludePlayerField.text.trim()); + newExcludePlayerField.text = ""; + } + } + } + + DankActionButton { + id: selectAppBtn + buttonSize: 36 + iconName: "apps" + iconSize: 20 + backgroundColor: Theme.surfaceContainer + iconColor: Theme.primary + onClicked: appBrowserPopup.show() + } + } + + Column { + width: parent.width + spacing: Theme.spacingS + + Repeater { + model: SettingsData.mediaExcludePlayers + + delegate: Rectangle { + width: parent.width + height: 48 + radius: Theme.cornerRadius + color: Theme.withAlpha(Theme.surfaceContainer, 0.5) + + Row { + anchors.fill: parent + anchors.leftMargin: Theme.spacingM + anchors.rightMargin: Theme.spacingS + spacing: Theme.spacingM + + Row { + width: parent.width - deleteBtn.width - Theme.spacingS + height: parent.height + spacing: Theme.spacingS + + DankIcon { + name: "music_off" + size: 20 + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: modelData + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + DankActionButton { + id: deleteBtn + buttonSize: 32 + iconName: "delete" + iconSize: 18 + iconColor: Theme.error + backgroundColor: "transparent" + anchors.verticalCenter: parent.verticalCenter + onClicked: SettingsData.removeMediaExcludePlayer(index) + } + } + } + } + } + + StyledText { + visible: !SettingsData.mediaExcludePlayers || SettingsData.mediaExcludePlayers.length === 0 + text: I18n.tr("No excluded players configured") + font.pixelSize: Theme.fontSizeSmall + font.italic: true + color: Theme.surfaceVariantText + horizontalAlignment: Text.AlignHCenter + width: parent.width + topPadding: Theme.spacingS + } + } + } + } + } + + AppBrowserPopup { + id: appBrowserPopup + appsModel: root.desktopApps + parentModal: root.parentModal + onAppSelected: appId => { + var name = appId; + if (name.endsWith(".desktop")) { + name = name.slice(0, -8); + } + SettingsData.addMediaExcludePlayer(name); } } } diff --git a/quickshell/Services/MprisController.qml b/quickshell/Services/MprisController.qml index 14db41b6..a139a288 100644 --- a/quickshell/Services/MprisController.qml +++ b/quickshell/Services/MprisController.qml @@ -9,7 +9,38 @@ import qs.Common Singleton { id: root - readonly property list availablePlayers: Mpris.players.values + readonly property list availablePlayers: { + const players = Mpris.players.values; + const excluded = SettingsData.mediaExcludePlayers || []; + if (excluded.length === 0) + return players; + return players.filter(p => { + const identity = (p.identity || "").toLowerCase(); + const desktopEntry = ("desktopEntry" in p && p.desktopEntry) ? String(p.desktopEntry).toLowerCase() : ""; + return !excluded.some(ex => { + const exLower = String(ex).toLowerCase().trim(); + if (!exLower) return false; + + // 1. Substring match + if (identity.includes(exLower) || desktopEntry.includes(exLower)) + return true; + + // 2. Match reverse-DNS segments (e.g. app.zen_browser.zen -> zen) + if (exLower.indexOf(".") !== -1) { + const parts = exLower.split("."); + const lastPart = parts[parts.length - 1]; + if (lastPart && (identity.includes(lastPart) || desktopEntry.includes(lastPart))) + return true; + } + + // 3. Bidirectional match (longer excluded name contains shorter player identity) + if (identity.length >= 3 && exLower.includes(identity)) + return true; + + return false; + }); + }); + } property MprisPlayer activePlayer: null property real activePlayerStableLength: 0 diff --git a/quickshell/translations/settings_search_index.json b/quickshell/translations/settings_search_index.json index 11822d5c..28a6494e 100644 --- a/quickshell/translations/settings_search_index.json +++ b/quickshell/translations/settings_search_index.json @@ -5643,6 +5643,26 @@ ], "description": "Play sound when volume is adjusted" }, + { + "section": "mediaExcludePlayers", + "label": "Excluded Media Players", + "tabIndex": 16, + "category": "Media Player", + "keywords": [ + "audio", + "exclude", + "excluded", + "ignore", + "media", + "mpris", + "music", + "playback", + "player", + "players", + "spotify" + ], + "icon": "do_not_disturb_on" + }, { "section": "_tab_16", "label": "Media Player",