Files
DankMaterialShell/quickshell/Modules/Settings/NotificationRulesTab.qml
T
bbedward 1142390724 settings: add + item name on every creation fab
settings: stop repeating the page title as the first card label
translations: index untitled cards by their page, not a nested row

fixes #3604
fixes #3609
2026-09-28 14:38:43 -04:00

456 lines
21 KiB
QML

import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Settings.Widgets
Item {
id: root
function indexedRules(predicate) {
return (SettingsData.notificationRules || []).map((rule, index) => ({
rule: rule,
index: index
})).filter(entry => predicate(entry.rule));
}
readonly property var mutedRules: indexedRules(rule => (rule.action || "").toString().toLowerCase() === "mute")
readonly property var notificationRuleFieldOptions: [
{
value: "appName",
label: I18n.tr("App names", "notification rule match field option")
},
{
value: "desktopEntry",
label: I18n.tr("Desktop Entry", "notification rule match field option")
},
{
value: "summary",
label: I18n.tr("Summary", "notification rule match field option")
},
{
value: "body",
label: I18n.tr("Body", "notification rule match field option")
}
]
readonly property var notificationRuleMatchTypeOptions: [
{
value: "contains",
label: I18n.tr("Contains", "notification rule match type option")
},
{
value: "exact",
label: I18n.tr("Exact", "notification rule match type option")
},
{
value: "regex",
label: I18n.tr("Regex", "notification rule match type option")
}
]
readonly property var notificationRuleActionOptions: [
{
value: "default",
label: I18n.tr("Default", "notification rule action option")
},
{
value: "mute",
label: I18n.tr("Mute Popups", "notification rule action option")
},
{
value: "ignore",
label: I18n.tr("Ignore Completely", "notification rule action option")
},
{
value: "popup_only",
label: I18n.tr("Popup Only", "notification rule action option")
},
{
value: "no_history",
label: I18n.tr("No History", "notification rule action option")
}
]
readonly property var notificationRuleUrgencyOptions: [
{
value: "default",
label: I18n.tr("Default", "notification rule urgency option")
},
{
value: "low",
label: I18n.tr("Low Priority", "notification rule urgency option")
},
{
value: "normal",
label: I18n.tr("Normal Priority", "notification rule urgency option")
},
{
value: "critical",
label: I18n.tr("Critical Priority", "notification rule urgency option")
}
]
function getRuleOptionLabel(options, value, fallback) {
for (let i = 0; i < options.length; i++) {
if (options[i].value === value)
return options[i].label;
}
return fallback;
}
function getRuleOptionValue(options, label, fallback) {
for (let i = 0; i < options.length; i++) {
if (options[i].label === label)
return options[i].value;
}
return fallback;
}
SettingsPage {
id: mainColumn
SettingsCard {
id: notificationRulesCard
width: parent.width
iconName: "rule_settings"
title: I18n.tr("Notification rules")
settingKey: "notificationRules"
tags: ["notification", "rules", "mute", "ignore", "priority", "regex", "history"]
headerActions: DankActionButton {
buttonSize: 36
iconName: "restart_alt"
tooltipText: I18n.tr("Reset to default")
iconSize: 20
visible: JSON.stringify(SettingsData.notificationRules) !== JSON.stringify(SettingsData.getDefaultNotificationRules())
iconColor: Theme.surfaceVariantText
onClicked: SettingsData.resetNotificationRules()
}
SettingsRow {
body: Column {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: I18n.tr("The Default action only overrides priority")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
bottomPadding: Theme.spacingS
}
Repeater {
model: SettingsData.notificationRules
delegate: Rectangle {
id: ruleItem
width: parent.width
height: ruleColumn.implicitHeight + Theme.spacingM
radius: Theme.cornerRadius
color: Theme.floatingWindowFieldColor
Column {
id: ruleColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: Theme.spacingS
Row {
width: parent.width
spacing: Theme.spacingS
StyledText {
id: ruleLabel
text: I18n.tr("Rule %1", "notification rule heading, %1 is the rule number").arg(index + 1)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: Math.max(0, parent.width - ruleLabel.implicitWidth - enableToggle.width - deleteBtn.width - Theme.spacingS * 3)
height: 1
}
DankToggle {
id: enableToggle
width: 40
height: 24
hideText: true
checked: modelData.enabled !== false
onToggled: checked => SettingsData.updateNotificationRuleField(index, "enabled", checked)
}
Item {
id: deleteBtn
Accessible.role: Accessible.Button
Accessible.name: I18n.tr("Remove")
width: 28
height: 28
anchors.verticalCenter: parent.verticalCenter
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: deleteArea.containsMouse ? Theme.withAlpha(Theme.error, 0.2) : Theme.withAlpha(Theme.error, 0)
}
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(index)
}
}
}
Column {
width: parent.width
spacing: Theme.spacingXXS
DankTextField {
outlined: true
leftIconName: "filter_list"
labelText: I18n.tr("Pattern")
width: parent.width
text: modelData.pattern || ""
font.pixelSize: Theme.fontSizeSmall
onEditingFinished: SettingsData.updateNotificationRuleField(index, "pattern", text)
}
}
Row {
width: parent.width
spacing: Theme.spacingS
Column {
width: (parent.width - Theme.spacingS * 3) / 4
spacing: Theme.spacingXXS
StyledText {
text: I18n.tr("Field", "notification rule dropdown label, which notification field to match")
font.pixelSize: Theme.fontSizeSmall - 1
color: Theme.surfaceVariantText
}
DankDropdown {
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"))
}
}
Column {
width: (parent.width - Theme.spacingS * 3) / 4
spacing: Theme.spacingXXS
StyledText {
text: I18n.tr("Type")
font.pixelSize: Theme.fontSizeSmall - 1
color: Theme.surfaceVariantText
}
DankDropdown {
width: parent.width
compactMode: true
dropdownWidth: parent.width
currentValue: root.getRuleOptionLabel(root.notificationRuleMatchTypeOptions, modelData.matchType, root.notificationRuleMatchTypeOptions[0].label)
options: root.notificationRuleMatchTypeOptions.map(o => o.label)
onValueChanged: value => SettingsData.updateNotificationRuleField(index, "matchType", root.getRuleOptionValue(root.notificationRuleMatchTypeOptions, value, "contains"))
}
}
Column {
width: (parent.width - Theme.spacingS * 3) / 4
spacing: Theme.spacingXXS
StyledText {
text: I18n.tr("Action", "noun, dropdown label for what a notification rule or keybind does")
font.pixelSize: Theme.fontSizeSmall - 1
color: Theme.surfaceVariantText
}
DankDropdown {
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"))
}
}
Column {
width: (parent.width - Theme.spacingS * 3) / 4
spacing: Theme.spacingXXS
StyledText {
text: I18n.tr("Priority", "notification rule dropdown label, urgency assigned to matches")
font.pixelSize: Theme.fontSizeSmall - 1
color: Theme.surfaceVariantText
}
DankDropdown {
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"))
}
}
}
Row {
width: parent.width
spacing: Theme.spacingS
StyledText {
id: bypassDndLabel
text: I18n.tr("Allow in Do Not Disturb")
font.pixelSize: Theme.fontSizeSmall - 1
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: Math.max(0, parent.width - bypassDndLabel.implicitWidth - bypassDndToggle.width - Theme.spacingS * 2)
height: 1
}
DankToggle {
id: bypassDndToggle
width: 40
height: 24
hideText: true
checked: modelData.bypassDnd === true
onToggled: checked => SettingsData.updateNotificationRuleField(index, "bypassDnd", checked)
}
}
}
}
}
}
}
}
SettingsCard {
iconName: "volume_off"
title: I18n.tr("Muted apps")
settingKey: "mutedApps"
tags: ["notification", "mute", "unmute", "popup"]
SettingsRow {
body: 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.floatingWindowFieldColor
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 - mutedDeleteBtn.width - Theme.spacingS * 5)
height: 1
}
DankButton {
id: unmuteBtn
text: I18n.tr("Unmute")
backgroundColor: Theme.chipSurface
textColor: Theme.primary
onClicked: SettingsData.removeNotificationRule(modelData.index)
}
Item {
id: mutedDeleteBtn
Accessible.role: Accessible.Button
Accessible.name: I18n.tr("Remove")
width: 28
height: 28
anchors.verticalCenter: parent.verticalCenter
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: mutedDeleteArea.containsMouse ? Theme.withAlpha(Theme.error, 0.2) : Theme.withAlpha(Theme.error, 0)
}
DankIcon {
anchors.centerIn: parent
name: "delete"
size: 18
color: mutedDeleteArea.containsMouse ? Theme.error : Theme.surfaceVariantText
}
MouseArea {
id: mutedDeleteArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: SettingsData.removeNotificationRule(modelData.index)
}
}
}
}
}
}
}
}
SettingsFabBar {
DankFab {
text: I18n.tr("Add rule")
iconName: "add"
onClicked: SettingsData.addNotificationRule()
}
}
}
}