mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-04-14 17:52:10 -04:00
feat(notifications): add configurable notification rules (#1655)
This commit is contained in:
@@ -504,6 +504,7 @@ Singleton {
|
|||||||
property bool notificationHistorySaveLow: true
|
property bool notificationHistorySaveLow: true
|
||||||
property bool notificationHistorySaveNormal: true
|
property bool notificationHistorySaveNormal: true
|
||||||
property bool notificationHistorySaveCritical: true
|
property bool notificationHistorySaveCritical: true
|
||||||
|
property var notificationRules: []
|
||||||
|
|
||||||
property bool osdAlwaysShowValue: false
|
property bool osdAlwaysShowValue: false
|
||||||
property int osdPosition: SettingsData.Position.BottomCenter
|
property int osdPosition: SettingsData.Position.BottomCenter
|
||||||
@@ -2134,6 +2135,56 @@ Singleton {
|
|||||||
saveSettings();
|
saveSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addNotificationRule() {
|
||||||
|
var rules = JSON.parse(JSON.stringify(notificationRules || []));
|
||||||
|
rules.push({
|
||||||
|
enabled: true,
|
||||||
|
field: "appName",
|
||||||
|
pattern: "",
|
||||||
|
matchType: "contains",
|
||||||
|
action: "mute",
|
||||||
|
urgency: "default"
|
||||||
|
});
|
||||||
|
notificationRules = rules;
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNotificationRule(index, ruleData) {
|
||||||
|
var rules = JSON.parse(JSON.stringify(notificationRules || []));
|
||||||
|
if (index < 0 || index >= rules.length)
|
||||||
|
return;
|
||||||
|
var existing = rules[index] || {};
|
||||||
|
rules[index] = Object.assign({}, existing, ruleData || {});
|
||||||
|
notificationRules = rules;
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNotificationRuleField(index, key, value) {
|
||||||
|
if (key === undefined || key === null || key === "")
|
||||||
|
return;
|
||||||
|
var patch = {};
|
||||||
|
patch[key] = value;
|
||||||
|
updateNotificationRule(index, patch);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeNotificationRule(index) {
|
||||||
|
var rules = JSON.parse(JSON.stringify(notificationRules || []));
|
||||||
|
if (index < 0 || index >= rules.length)
|
||||||
|
return;
|
||||||
|
rules.splice(index, 1);
|
||||||
|
notificationRules = rules;
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultNotificationRules() {
|
||||||
|
return Spec.SPEC.notificationRules.def;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetNotificationRules() {
|
||||||
|
notificationRules = JSON.parse(JSON.stringify(Spec.SPEC.notificationRules.def));
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
function getDefaultAppIdSubstitutions() {
|
function getDefaultAppIdSubstitutions() {
|
||||||
return Spec.SPEC.appIdSubstitutions.def;
|
return Spec.SPEC.appIdSubstitutions.def;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -330,6 +330,7 @@ var SPEC = {
|
|||||||
notificationHistorySaveLow: { def: true },
|
notificationHistorySaveLow: { def: true },
|
||||||
notificationHistorySaveNormal: { def: true },
|
notificationHistorySaveNormal: { def: true },
|
||||||
notificationHistorySaveCritical: { def: true },
|
notificationHistorySaveCritical: { def: true },
|
||||||
|
notificationRules: { def: [] },
|
||||||
|
|
||||||
osdAlwaysShowValue: { def: false },
|
osdAlwaysShowValue: { def: false },
|
||||||
osdPosition: { def: 5 },
|
osdPosition: { def: 5 },
|
||||||
|
|||||||
@@ -153,12 +153,12 @@ QtObject {
|
|||||||
if (!wrapper || !wrapper.notification) {
|
if (!wrapper || !wrapper.notification) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const incomingUrgency = wrapper.notification.urgency || 0;
|
const incomingUrgency = wrapper.urgency || 0;
|
||||||
for (const p of activeWindows) {
|
for (const p of activeWindows) {
|
||||||
if (!p.notificationData || !p.notificationData.notification) {
|
if (!p.notificationData || !p.notificationData.notification) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const existingUrgency = p.notificationData.notification.urgency || 0;
|
const existingUrgency = p.notificationData.urgency || 0;
|
||||||
if (existingUrgency < incomingUrgency) {
|
if (existingUrgency < incomingUrgency) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -188,10 +188,9 @@ QtObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _selectPopupToRemove(activeWindows, incomingWrapper) {
|
function _selectPopupToRemove(activeWindows, incomingWrapper) {
|
||||||
const incomingUrgency = (incomingWrapper && incomingWrapper.notification) ? incomingWrapper.notification.urgency || 0 : 0;
|
|
||||||
const sortedWindows = activeWindows.slice().sort((a, b) => {
|
const sortedWindows = activeWindows.slice().sort((a, b) => {
|
||||||
const aUrgency = (a.notificationData && a.notificationData.notification) ? a.notificationData.notification.urgency || 0 : 0;
|
const aUrgency = (a.notificationData) ? a.notificationData.urgency || 0 : 0;
|
||||||
const bUrgency = (b.notificationData && b.notificationData.notification) ? b.notificationData.notification.urgency || 0 : 0;
|
const bUrgency = (b.notificationData) ? b.notificationData.urgency || 0 : 0;
|
||||||
if (aUrgency !== bUrgency) {
|
if (aUrgency !== bUrgency) {
|
||||||
return aUrgency - bUrgency;
|
return aUrgency - bUrgency;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,82 @@ Item {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
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 getTimeoutText(value) {
|
function getTimeoutText(value) {
|
||||||
if (value === undefined || value === null || isNaN(value))
|
if (value === undefined || value === null || isNaN(value))
|
||||||
return I18n.tr("5 seconds");
|
return I18n.tr("5 seconds");
|
||||||
@@ -73,6 +149,22 @@ Item {
|
|||||||
return Math.round(value / 60000) + " " + I18n.tr("minutes");
|
return Math.round(value / 60000) + " " + I18n.tr("minutes");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
DankFlickable {
|
DankFlickable {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
clip: true
|
clip: true
|
||||||
@@ -165,6 +257,228 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
width: parent.width
|
||||||
|
iconName: "rule_settings"
|
||||||
|
title: I18n.tr("Notification Rules")
|
||||||
|
settingKey: "notificationRules"
|
||||||
|
tags: ["notification", "rules", "mute", "ignore", "priority", "regex", "history"]
|
||||||
|
collapsible: true
|
||||||
|
expanded: false
|
||||||
|
|
||||||
|
headerActions: [
|
||||||
|
DankActionButton {
|
||||||
|
buttonSize: 36
|
||||||
|
iconName: "restart_alt"
|
||||||
|
iconSize: 20
|
||||||
|
visible: JSON.stringify(SettingsData.notificationRules) !== JSON.stringify(SettingsData.getDefaultNotificationRules())
|
||||||
|
backgroundColor: Theme.surfaceContainer
|
||||||
|
iconColor: Theme.surfaceVariantText
|
||||||
|
onClicked: SettingsData.resetNotificationRules()
|
||||||
|
},
|
||||||
|
DankActionButton {
|
||||||
|
buttonSize: 36
|
||||||
|
iconName: "add"
|
||||||
|
iconSize: 20
|
||||||
|
backgroundColor: Theme.surfaceContainer
|
||||||
|
iconColor: Theme.primary
|
||||||
|
onClicked: SettingsData.addNotificationRule()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Create rules to mute, ignore, hide from history, or override notification 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.withAlpha(Theme.surfaceContainer, 0.5)
|
||||||
|
|
||||||
|
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") + " " + (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
|
||||||
|
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(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Pattern")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall - 1
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
DankTextField {
|
||||||
|
width: parent.width
|
||||||
|
text: modelData.pattern || ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
placeholderText: I18n.tr("Pattern")
|
||||||
|
onEditingFinished: SettingsData.updateNotificationRuleField(index, "pattern", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: (parent.width - Theme.spacingS * 3) / 4
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Field")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall - 1
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
DankDropdown {
|
||||||
|
width: parent.width
|
||||||
|
compactMode: true
|
||||||
|
dropdownWidth: parent.width
|
||||||
|
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: 2
|
||||||
|
|
||||||
|
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: 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Action")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall - 1
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
DankDropdown {
|
||||||
|
width: parent.width
|
||||||
|
compactMode: true
|
||||||
|
dropdownWidth: parent.width
|
||||||
|
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: 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Priority")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall - 1
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
DankDropdown {
|
||||||
|
width: parent.width
|
||||||
|
compactMode: true
|
||||||
|
dropdownWidth: parent.width
|
||||||
|
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"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
iconName: "lock"
|
iconName: "lock"
|
||||||
|
|||||||
@@ -260,14 +260,14 @@ Singleton {
|
|||||||
return Date.now() / 1000.0;
|
return Date.now() / 1000.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _ingressAllowed(notif) {
|
function _ingressAllowed(urgency) {
|
||||||
const t = _nowSec();
|
const t = _nowSec();
|
||||||
if (t - _lastIngressSec >= 1.0) {
|
if (t - _lastIngressSec >= 1.0) {
|
||||||
_lastIngressSec = t;
|
_lastIngressSec = t;
|
||||||
_ingressCountThisSec = 0;
|
_ingressCountThisSec = 0;
|
||||||
}
|
}
|
||||||
_ingressCountThisSec += 1;
|
_ingressCountThisSec += 1;
|
||||||
if (notif.urgency === NotificationUrgency.Critical) {
|
if (urgency === NotificationUrgency.Critical) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return _ingressCountThisSec <= maxIngressPerSecond;
|
return _ingressCountThisSec <= maxIngressPerSecond;
|
||||||
@@ -294,11 +294,13 @@ Singleton {
|
|||||||
|
|
||||||
function _initWrapperPersistence(wrapper) {
|
function _initWrapperPersistence(wrapper) {
|
||||||
const timeoutMs = wrapper.timer ? wrapper.timer.interval : 5000;
|
const timeoutMs = wrapper.timer ? wrapper.timer.interval : 5000;
|
||||||
const isCritical = wrapper.notification && wrapper.notification.urgency === NotificationUrgency.Critical;
|
const isCritical = wrapper && wrapper.urgency === NotificationUrgency.Critical;
|
||||||
wrapper.isPersistent = isCritical || (timeoutMs === 0);
|
wrapper.isPersistent = isCritical || (timeoutMs === 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _shouldSaveToHistory(urgency) {
|
function _shouldSaveToHistory(urgency, forceDisable) {
|
||||||
|
if (forceDisable === true)
|
||||||
|
return false;
|
||||||
if (!SettingsData.notificationHistoryEnabled)
|
if (!SettingsData.notificationHistoryEnabled)
|
||||||
return false;
|
return false;
|
||||||
switch (urgency) {
|
switch (urgency) {
|
||||||
@@ -311,6 +313,126 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _resolveAppNameForRule(notif) {
|
||||||
|
if (!notif)
|
||||||
|
return "";
|
||||||
|
if (notif.appName && notif.appName !== "")
|
||||||
|
return notif.appName;
|
||||||
|
const entry = DesktopEntries.heuristicLookup(notif.desktopEntry);
|
||||||
|
if (entry && entry.name)
|
||||||
|
return entry.name;
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function _ruleFieldValue(field, info) {
|
||||||
|
switch ((field || "").toString()) {
|
||||||
|
case "desktopEntry":
|
||||||
|
return info.desktopEntry;
|
||||||
|
case "summary":
|
||||||
|
return info.summary;
|
||||||
|
case "body":
|
||||||
|
return info.body;
|
||||||
|
case "appName":
|
||||||
|
default:
|
||||||
|
return info.appName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _coerceRuleUrgency(value, fallbackUrgency) {
|
||||||
|
if (typeof value === "number" && value >= NotificationUrgency.Low && value <= NotificationUrgency.Critical)
|
||||||
|
return value;
|
||||||
|
|
||||||
|
const mapped = (value || "default").toString().toLowerCase();
|
||||||
|
switch (mapped) {
|
||||||
|
case "low":
|
||||||
|
return NotificationUrgency.Low;
|
||||||
|
case "normal":
|
||||||
|
return NotificationUrgency.Normal;
|
||||||
|
case "critical":
|
||||||
|
return NotificationUrgency.Critical;
|
||||||
|
default:
|
||||||
|
return fallbackUrgency;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _matchesNotificationRule(rule, info) {
|
||||||
|
if (!rule)
|
||||||
|
return false;
|
||||||
|
if (rule.enabled === false)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const pattern = (rule.pattern || "").toString();
|
||||||
|
if (!pattern.trim())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const value = (_ruleFieldValue(rule.field, info) || "").toString();
|
||||||
|
const matchType = (rule.matchType || "contains").toString().toLowerCase();
|
||||||
|
|
||||||
|
if (matchType === "exact")
|
||||||
|
return value.toLowerCase() === pattern.toLowerCase();
|
||||||
|
if (matchType === "regex") {
|
||||||
|
try {
|
||||||
|
return new RegExp(pattern, "i").test(value);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("NotificationService: invalid notification rule regex:", pattern);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.toLowerCase().includes(pattern.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function _evaluateNotificationPolicy(notif) {
|
||||||
|
const baseUrgency = typeof notif.urgency === "number" ? notif.urgency : NotificationUrgency.Normal;
|
||||||
|
const policy = {
|
||||||
|
"drop": false,
|
||||||
|
"disablePopup": false,
|
||||||
|
"hideFromCenter": false,
|
||||||
|
"disableHistory": false,
|
||||||
|
"urgency": baseUrgency
|
||||||
|
};
|
||||||
|
|
||||||
|
const rules = SettingsData.notificationRules || [];
|
||||||
|
if (!rules.length)
|
||||||
|
return policy;
|
||||||
|
|
||||||
|
const info = {
|
||||||
|
"appName": _resolveAppNameForRule(notif),
|
||||||
|
"desktopEntry": notif.desktopEntry || "",
|
||||||
|
"summary": notif.summary || "",
|
||||||
|
"body": notif.body || ""
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const rule of rules) {
|
||||||
|
if (!_matchesNotificationRule(rule, info))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const action = (rule.action || "default").toString().toLowerCase();
|
||||||
|
switch (action) {
|
||||||
|
case "ignore":
|
||||||
|
policy.drop = true;
|
||||||
|
break;
|
||||||
|
case "mute":
|
||||||
|
policy.disablePopup = true;
|
||||||
|
break;
|
||||||
|
case "popup_only":
|
||||||
|
policy.hideFromCenter = true;
|
||||||
|
policy.disableHistory = true;
|
||||||
|
break;
|
||||||
|
case "no_history":
|
||||||
|
policy.disableHistory = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
policy.urgency = _coerceRuleUrgency(rule.urgency, policy.urgency);
|
||||||
|
return policy;
|
||||||
|
}
|
||||||
|
|
||||||
|
return policy;
|
||||||
|
}
|
||||||
|
|
||||||
function pruneHistory() {
|
function pruneHistory() {
|
||||||
const maxAgeDays = SettingsData.notificationHistoryMaxAgeDays;
|
const maxAgeDays = SettingsData.notificationHistoryMaxAgeDays;
|
||||||
if (maxAgeDays <= 0)
|
if (maxAgeDays <= 0)
|
||||||
@@ -440,8 +562,16 @@ Singleton {
|
|||||||
onNotification: notif => {
|
onNotification: notif => {
|
||||||
notif.tracked = true;
|
notif.tracked = true;
|
||||||
|
|
||||||
if (!_ingressAllowed(notif)) {
|
const policy = _evaluateNotificationPolicy(notif);
|
||||||
if (notif.urgency !== NotificationUrgency.Critical) {
|
if (policy.drop) {
|
||||||
|
try {
|
||||||
|
notif.dismiss();
|
||||||
|
} catch (e) {}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_ingressAllowed(policy.urgency)) {
|
||||||
|
if (policy.urgency !== NotificationUrgency.Critical) {
|
||||||
try {
|
try {
|
||||||
notif.dismiss();
|
notif.dismiss();
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
@@ -450,25 +580,35 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (SettingsData.soundsEnabled && SettingsData.soundNewNotification) {
|
if (SettingsData.soundsEnabled && SettingsData.soundNewNotification) {
|
||||||
if (notif.urgency === NotificationUrgency.Critical) {
|
if (policy.urgency === NotificationUrgency.Critical) {
|
||||||
AudioService.playCriticalNotificationSound();
|
AudioService.playCriticalNotificationSound();
|
||||||
} else {
|
} else {
|
||||||
AudioService.playNormalNotificationSound();
|
AudioService.playNormalNotificationSound();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldShowPopup = !root.popupsDisabled && !SessionData.doNotDisturb;
|
const shouldShowPopup = !root.popupsDisabled && !SessionData.doNotDisturb && !policy.disablePopup;
|
||||||
const isTransient = notif.transient;
|
const isTransient = notif.transient;
|
||||||
|
const shouldKeepInCenter = !isTransient && !policy.hideFromCenter;
|
||||||
|
|
||||||
|
if (!shouldShowPopup && !shouldKeepInCenter) {
|
||||||
|
try {
|
||||||
|
notif.dismiss();
|
||||||
|
} catch (e) {}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const wrapper = notifComponent.createObject(root, {
|
const wrapper = notifComponent.createObject(root, {
|
||||||
"popup": shouldShowPopup,
|
"popup": shouldShowPopup,
|
||||||
"notification": notif
|
"notification": notif,
|
||||||
|
"urgencyOverride": policy.urgency
|
||||||
});
|
});
|
||||||
|
|
||||||
if (wrapper) {
|
if (wrapper) {
|
||||||
root.allWrappers.push(wrapper);
|
root.allWrappers.push(wrapper);
|
||||||
if (!isTransient) {
|
if (shouldKeepInCenter) {
|
||||||
root.notifications.push(wrapper);
|
root.notifications.push(wrapper);
|
||||||
if (_shouldSaveToHistory(notif.urgency)) {
|
if (_shouldSaveToHistory(wrapper.urgency, policy.disableHistory)) {
|
||||||
root.addToHistory(wrapper);
|
root.addToHistory(wrapper);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -505,7 +645,7 @@ Singleton {
|
|||||||
interval: {
|
interval: {
|
||||||
if (!wrapper.notification)
|
if (!wrapper.notification)
|
||||||
return 5000;
|
return 5000;
|
||||||
switch (wrapper.notification.urgency) {
|
switch (wrapper.urgency) {
|
||||||
case NotificationUrgency.Low:
|
case NotificationUrgency.Low:
|
||||||
return SettingsData.notificationTimeoutLow;
|
return SettingsData.notificationTimeoutLow;
|
||||||
case NotificationUrgency.Critical:
|
case NotificationUrgency.Critical:
|
||||||
@@ -600,7 +740,8 @@ Singleton {
|
|||||||
return "";
|
return "";
|
||||||
return Paths.strip(image);
|
return Paths.strip(image);
|
||||||
}
|
}
|
||||||
readonly property int urgency: notification?.urgency ?? 1
|
property int urgencyOverride: notification?.urgency ?? NotificationUrgency.Normal
|
||||||
|
readonly property int urgency: urgencyOverride
|
||||||
readonly property list<NotificationAction> actions: notification?.actions ?? []
|
readonly property list<NotificationAction> actions: notification?.actions ?? []
|
||||||
|
|
||||||
readonly property Connections conn: Connections {
|
readonly property Connections conn: Connections {
|
||||||
|
|||||||
Reference in New Issue
Block a user