diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index 5b91a794..f21244b7 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -2147,7 +2147,7 @@ Singleton { field: "appName", pattern: "", matchType: "contains", - action: "mute", + action: "default", urgency: "default" }); notificationRules = rules; @@ -2172,6 +2172,51 @@ Singleton { saveSettings(); } + function isAppMuted(appName, desktopEntry) { + const rules = notificationRules || []; + const pat = (desktopEntry && desktopEntry !== "" ? desktopEntry : appName || "").toString().toLowerCase(); + if (!pat) + return false; + for (let i = 0; i < rules.length; i++) { + const r = rules[i]; + if ((r.action || "").toString().toLowerCase() !== "mute" || r.enabled === false) + continue; + const field = (r.field || "appName").toString().toLowerCase(); + const rulePat = (r.pattern || "").toString().toLowerCase(); + if (!rulePat) + continue; + const useDesktop = field === "desktopentry"; + const matches = (useDesktop && desktopEntry) ? (desktopEntry.toString().toLowerCase() === rulePat) : (appName && appName.toString().toLowerCase() === rulePat); + if (matches) + return true; + if (rulePat === pat) + return true; + } + return false; + } + + function removeMuteRuleForApp(appName, desktopEntry) { + var rules = JSON.parse(JSON.stringify(notificationRules || [])); + const app = (appName || "").toString().toLowerCase(); + const desktop = (desktopEntry || "").toString().toLowerCase(); + if (!app && !desktop) + return; + for (let i = rules.length - 1; i >= 0; i--) { + const r = rules[i]; + if ((r.action || "").toString().toLowerCase() !== "mute") + continue; + const rulePat = (r.pattern || "").toString().toLowerCase(); + if (!rulePat) + continue; + if (rulePat === app || rulePat === desktop) { + rules.splice(i, 1); + notificationRules = rules; + saveSettings(); + return; + } + } + } + function updateNotificationRule(index, ruleData) { var rules = JSON.parse(JSON.stringify(notificationRules || [])); if (index < 0 || index >= rules.length) diff --git a/quickshell/Modules/Notifications/Center/NotificationCard.qml b/quickshell/Modules/Notifications/Center/NotificationCard.qml index 4febbdd6..af526421 100644 --- a/quickshell/Modules/Notifications/Center/NotificationCard.qml +++ b/quickshell/Modules/Notifications/Center/NotificationCard.qml @@ -749,7 +749,7 @@ Rectangle { Menu { id: notificationCardContextMenu - width: 220 + width: 300 closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside background: Rectangle { @@ -760,7 +760,9 @@ Rectangle { } MenuItem { - text: I18n.tr("Mute popups for %1").arg(notificationGroup?.appName || I18n.tr("this app")) + id: muteUnmuteItem + readonly property bool isMuted: SettingsData.isAppMuted(notificationGroup?.appName || "", notificationGroup?.latestNotification?.desktopEntry || "") + text: isMuted ? I18n.tr("Unmute popups for %1").arg(notificationGroup?.appName || I18n.tr("this app")) : I18n.tr("Mute popups for %1").arg(notificationGroup?.appName || I18n.tr("this app")) contentItem: StyledText { text: parent.text @@ -778,8 +780,12 @@ Rectangle { onTriggered: { const appName = notificationGroup?.appName || ""; const desktopEntry = notificationGroup?.latestNotification?.desktopEntry || ""; - SettingsData.addMuteRuleForApp(appName, desktopEntry); - NotificationService.dismissGroup(notificationGroup?.key || ""); + if (isMuted) { + SettingsData.removeMuteRuleForApp(appName, desktopEntry); + } else { + SettingsData.addMuteRuleForApp(appName, desktopEntry); + NotificationService.dismissGroup(notificationGroup?.key || ""); + } } } diff --git a/quickshell/Modules/Notifications/Popup/NotificationPopup.qml b/quickshell/Modules/Notifications/Popup/NotificationPopup.qml index 90f601dc..cae4e89b 100644 --- a/quickshell/Modules/Notifications/Popup/NotificationPopup.qml +++ b/quickshell/Modules/Notifications/Popup/NotificationPopup.qml @@ -28,7 +28,7 @@ PanelWindow { readonly property real popupIconSize: compactMode ? 48 : 63 readonly property real contentSpacing: compactMode ? Theme.spacingXS : Theme.spacingS readonly property real actionButtonHeight: compactMode ? 20 : 24 - readonly property real collapsedContentHeight: popupIconSize + readonly property real collapsedContentHeight: popupIconSize + (compactMode ? 0 : Theme.fontSizeSmall * 1.2) readonly property real basePopupHeight: cardPadding * 2 + collapsedContentHeight + actionButtonHeight + Theme.spacingS signal entered @@ -104,9 +104,9 @@ PanelWindow { if (!descriptionExpanded) return basePopupHeight; const bodyTextHeight = bodyText.contentHeight || 0; - const twoLineHeight = Theme.fontSizeSmall * 1.2 * 2; - if (bodyTextHeight > twoLineHeight + 2) - return basePopupHeight + bodyTextHeight - twoLineHeight; + const collapsedBodyHeight = Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 3); + if (bodyTextHeight > collapsedBodyHeight + 2) + return basePopupHeight + bodyTextHeight - collapsedBodyHeight; return basePopupHeight; } onHasValidDataChanged: { @@ -317,8 +317,8 @@ PanelWindow { id: notificationContent readonly property real expandedTextHeight: bodyText.contentHeight || 0 - readonly property real twoLineHeight: Theme.fontSizeSmall * 1.2 * 2 - readonly property real extraHeight: (descriptionExpanded && expandedTextHeight > twoLineHeight + 2) ? (expandedTextHeight - twoLineHeight) : 0 + readonly property real collapsedBodyHeight: Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 3) + readonly property real extraHeight: (descriptionExpanded && expandedTextHeight > collapsedBodyHeight + 2) ? (expandedTextHeight - collapsedBodyHeight) : 0 anchors.top: parent.top anchors.left: parent.left @@ -437,7 +437,7 @@ PanelWindow { width: parent.width elide: descriptionExpanded ? Text.ElideNone : Text.ElideRight horizontalAlignment: Text.AlignLeft - maximumLineCount: descriptionExpanded ? -1 : (compactMode ? 1 : 2) + maximumLineCount: descriptionExpanded ? -1 : (compactMode ? 1 : 3) wrapMode: Text.WordWrap visible: text.length > 0 linkColor: Theme.primary @@ -816,7 +816,7 @@ PanelWindow { Menu { id: popupContextMenu - width: 220 + width: 300 closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside background: Rectangle { @@ -827,7 +827,9 @@ PanelWindow { } MenuItem { - text: I18n.tr("Mute popups for %1").arg(notificationData?.appName || I18n.tr("this app")) + id: muteUnmuteItem + readonly property bool isMuted: SettingsData.isAppMuted(notificationData?.appName || "", notificationData?.desktopEntry || "") + text: isMuted ? I18n.tr("Unmute popups for %1").arg(notificationData?.appName || I18n.tr("this app")) : I18n.tr("Mute popups for %1").arg(notificationData?.appName || I18n.tr("this app")) contentItem: StyledText { text: parent.text @@ -845,9 +847,13 @@ PanelWindow { onTriggered: { const appName = notificationData?.appName || ""; const desktopEntry = notificationData?.desktopEntry || ""; - SettingsData.addMuteRuleForApp(appName, desktopEntry); - if (notificationData && !exiting) - NotificationService.dismissNotification(notificationData); + if (isMuted) { + SettingsData.removeMuteRuleForApp(appName, desktopEntry); + } else { + SettingsData.addMuteRuleForApp(appName, desktopEntry); + if (notificationData && !exiting) + NotificationService.dismissNotification(notificationData); + } } } diff --git a/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml b/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml index 39144a95..cb98880e 100644 --- a/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml +++ b/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml @@ -11,7 +11,7 @@ QtObject { readonly property real cardPadding: compactMode ? Theme.spacingS : Theme.spacingM readonly property real popupIconSize: compactMode ? 48 : 63 readonly property real actionButtonHeight: compactMode ? 20 : 24 - readonly property real popupSpacing: 4 + readonly property real popupSpacing: 8 readonly property int baseNotificationHeight: cardPadding * 2 + popupIconSize + actionButtonHeight + Theme.spacingS + popupSpacing property int maxTargetNotifications: 4 property var popupWindows: [] // strong refs to windows (live until exitFinished) diff --git a/quickshell/Modules/Settings/NotificationsTab.qml b/quickshell/Modules/Settings/NotificationsTab.qml index d84cb6be..acc86fea 100644 --- a/quickshell/Modules/Settings/NotificationsTab.qml +++ b/quickshell/Modules/Settings/NotificationsTab.qml @@ -6,6 +6,16 @@ import qs.Modules.Settings.Widgets Item { id: root + readonly property var mutedRules: { + var rules = SettingsData.notificationRules || []; + var out = []; + for (var i = 0; i < rules.length; i++) { + if ((rules[i].action || "").toString().toLowerCase() === "mute") + out.push({ rule: rules[i], index: i }); + } + return out; + } + readonly property var timeoutOptions: [ { text: I18n.tr("Never"), @@ -478,6 +488,7 @@ Item { width: parent.width compactMode: true dropdownWidth: parent.width + popupWidth: 165 currentValue: root.getRuleOptionLabel(root.notificationRuleFieldOptions, modelData.field, root.notificationRuleFieldOptions[0].label) options: root.notificationRuleFieldOptions.map(o => o.label) onValueChanged: value => SettingsData.updateNotificationRuleField(index, "field", root.getRuleOptionValue(root.notificationRuleFieldOptions, value, "appName")) @@ -518,6 +529,7 @@ Item { width: parent.width compactMode: true dropdownWidth: parent.width + popupWidth: 170 currentValue: root.getRuleOptionLabel(root.notificationRuleActionOptions, modelData.action, root.notificationRuleActionOptions[0].label) options: root.notificationRuleActionOptions.map(o => o.label) onValueChanged: value => SettingsData.updateNotificationRuleField(index, "action", root.getRuleOptionValue(root.notificationRuleActionOptions, value, "default")) @@ -538,6 +550,7 @@ Item { width: parent.width compactMode: true dropdownWidth: parent.width + popupWidth: 165 currentValue: root.getRuleOptionLabel(root.notificationRuleUrgencyOptions, modelData.urgency, root.notificationRuleUrgencyOptions[0].label) options: root.notificationRuleUrgencyOptions.map(o => o.label) onValueChanged: value => SettingsData.updateNotificationRuleField(index, "urgency", root.getRuleOptionValue(root.notificationRuleUrgencyOptions, value, "default")) @@ -550,6 +563,95 @@ Item { } } + SettingsCard { + width: parent.width + iconName: "volume_off" + title: I18n.tr("Muted Apps") + settingKey: "mutedApps" + tags: ["notification", "mute", "unmute", "popup"] + + Column { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: mutedRules.length > 0 ? I18n.tr("Apps with notification popups muted. Unmute or delete to remove.") : I18n.tr("No apps muted. Right-click a notification and choose \"Mute popups\" to add one here.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + bottomPadding: Theme.spacingS + } + + Repeater { + model: mutedRules + + delegate: Rectangle { + width: parent.width + height: mutedRow.implicitHeight + Theme.spacingS * 2 + radius: Theme.cornerRadius + color: Theme.withAlpha(Theme.surfaceContainer, 0.5) + + Row { + id: mutedRow + anchors.fill: parent + anchors.margins: Theme.spacingS + spacing: Theme.spacingM + + StyledText { + id: mutedAppLabel + text: (modelData.rule && modelData.rule.pattern) ? modelData.rule.pattern : I18n.tr("Unknown") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Item { + width: Math.max(0, parent.width - parent.spacing - mutedAppLabel.width - unmuteBtn.width - deleteBtn.width - Theme.spacingS * 5) + height: 1 + } + + DankButton { + id: unmuteBtn + text: I18n.tr("Unmute") + backgroundColor: Theme.surfaceContainer + textColor: Theme.primary + onClicked: SettingsData.removeNotificationRule(modelData.index) + } + + Item { + id: deleteBtn + width: 28 + height: 28 + anchors.verticalCenter: parent.verticalCenter + + Rectangle { + anchors.fill: parent + radius: Theme.cornerRadius + color: deleteArea.containsMouse ? Theme.withAlpha(Theme.error, 0.2) : "transparent" + } + + DankIcon { + anchors.centerIn: parent + name: "delete" + size: 18 + color: deleteArea.containsMouse ? Theme.error : Theme.surfaceVariantText + } + + MouseArea { + id: deleteArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: SettingsData.removeNotificationRule(modelData.index) + } + } + } + } + } + } + } + SettingsCard { width: parent.width iconName: "lock" diff --git a/quickshell/Widgets/DankDropdown.qml b/quickshell/Widgets/DankDropdown.qml index 496d3fb6..c100c0ac 100644 --- a/quickshell/Widgets/DankDropdown.qml +++ b/quickshell/Widgets/DankDropdown.qml @@ -55,6 +55,10 @@ Item { signal valueChanged(string value) + function closeDropdownMenu() { + dropdownMenu.close(); + } + width: compactMode ? dropdownWidth : parent.width implicitHeight: compactMode ? 40 : Math.max(60, labelColumn.implicitHeight + Theme.spacingM) @@ -409,7 +413,7 @@ Item { onClicked: { root.currentValue = delegateRoot.modelData; root.valueChanged(delegateRoot.modelData); - dropdownMenu.close(); + root.closeDropdownMenu(); } } }