feat: add notification application rules
This commit is contained in:
@@ -50,6 +50,70 @@ SettingsPage {
|
||||
SliderRow { setting: "maxVisibleToasts"; divider: false }
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
title: "Application rules"
|
||||
subtitle: "Apps appear here after they send a notification."
|
||||
|
||||
TextRow {
|
||||
visible: Notifs.applications.length === 0
|
||||
label: "No applications remembered yet"
|
||||
detail: "Application controls will appear after the first notification arrives."
|
||||
divider: false
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: Notifs.applications
|
||||
|
||||
Column {
|
||||
required property var modelData
|
||||
|
||||
readonly property var app: modelData
|
||||
|
||||
width: parent.width
|
||||
|
||||
SettingRow {
|
||||
label: app.name
|
||||
detail: app.id
|
||||
controlWidth: 48
|
||||
|
||||
SettingsToggle {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: Notifs.appRule(app.id).enabled
|
||||
onToggled: value => Notifs.setAppRule(app.id, { enabled: value })
|
||||
}
|
||||
}
|
||||
|
||||
SettingRow {
|
||||
label: "Show on lock screen"
|
||||
detail: "Allow this app's notifications on the lock screen"
|
||||
controlWidth: 48
|
||||
|
||||
SettingsToggle {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: Notifs.appRule(app.id).showOnLockScreen
|
||||
onToggled: value => Notifs.setAppRule(app.id, { showOnLockScreen: value })
|
||||
}
|
||||
}
|
||||
|
||||
SettingRow {
|
||||
label: "Show content on lock screen"
|
||||
detail: "Show message details when this app is visible there"
|
||||
divider: false
|
||||
controlWidth: 48
|
||||
|
||||
SettingsToggle {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: Notifs.appRule(app.id).showContentOnLockScreen
|
||||
onToggled: value => Notifs.setAppRule(app.id, { showContentOnLockScreen: value })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
title: "Focus sessions"
|
||||
subtitle: "A focus session binds quiet mode and Caffeine to the current workspace."
|
||||
|
||||
@@ -37,6 +37,31 @@ Singleton {
|
||||
// Cleared when the notification centre is opened. The bar binds to this.
|
||||
property int unreadCount: 0
|
||||
|
||||
// Kept separate from the persisted map so this version can safely run
|
||||
// before the matching schema entry lands. A later accepted write folds the
|
||||
// complete map into DesktopPreferences and clears this fallback.
|
||||
property var fallbackAppRules: ({})
|
||||
|
||||
// Display metadata is intentionally session-only. The durable shape stays
|
||||
// just the per-application rule map, while a fresh notification gives the
|
||||
// settings page a human-readable name straight away.
|
||||
property var rememberedApplications: ({})
|
||||
|
||||
readonly property var persistedAppRules: {
|
||||
const stored = DesktopPreferences.get("notificationAppRules");
|
||||
return stored && typeof stored === "object" && !Array.isArray(stored) ? stored : {};
|
||||
}
|
||||
|
||||
readonly property var appRules: Object.assign({}, root.persistedAppRules, root.fallbackAppRules)
|
||||
|
||||
readonly property var applications: {
|
||||
const remembered = root.rememberedApplications;
|
||||
return Object.keys(root.appRules).map(appId => ({
|
||||
id: appId,
|
||||
name: remembered[appId]?.name || appId
|
||||
})).sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
// Arrival times, keyed by notification id — the protocol carries no
|
||||
// timestamp. Deliberately formatted once at arrival rather than shown as
|
||||
// "5 minutes ago", which would need a clock ticking behind every card.
|
||||
@@ -49,6 +74,70 @@ Singleton {
|
||||
|
||||
readonly property bool hasNotifications: root.history.length > 0
|
||||
|
||||
function notificationAppId(notification: Notification): string {
|
||||
const desktopEntry = String(notification.desktopEntry ?? "").trim();
|
||||
return desktopEntry || String(notification.appName ?? "").trim() || "Notifications";
|
||||
}
|
||||
|
||||
function normalizedAppRule(rule: var): var {
|
||||
const source = rule && typeof rule === "object" && !Array.isArray(rule) ? rule : {};
|
||||
return {
|
||||
enabled: source.enabled !== false,
|
||||
showOnLockScreen: source.showOnLockScreen !== false,
|
||||
showContentOnLockScreen: source.showContentOnLockScreen !== false
|
||||
};
|
||||
}
|
||||
|
||||
function appRule(appId: string): var {
|
||||
return root.normalizedAppRule(root.appRules[appId]);
|
||||
}
|
||||
|
||||
function setAppRule(appId: string, patch: var): bool {
|
||||
if (!appId)
|
||||
return false;
|
||||
|
||||
const current = root.appRule(appId);
|
||||
const next = {};
|
||||
for (const knownAppId of Object.keys(root.appRules))
|
||||
next[knownAppId] = root.appRule(knownAppId);
|
||||
next[appId] = {
|
||||
enabled: patch.enabled === undefined ? current.enabled : patch.enabled === true,
|
||||
showOnLockScreen: patch.showOnLockScreen === undefined ? current.showOnLockScreen : patch.showOnLockScreen === true,
|
||||
showContentOnLockScreen: patch.showContentOnLockScreen === undefined ? current.showContentOnLockScreen : patch.showContentOnLockScreen === true
|
||||
};
|
||||
|
||||
if (DesktopPreferences.set("notificationAppRules", next))
|
||||
root.fallbackAppRules = {};
|
||||
else
|
||||
root.fallbackAppRules = next;
|
||||
return true;
|
||||
}
|
||||
|
||||
function rememberApplication(notification: Notification): string {
|
||||
const appId = root.notificationAppId(notification);
|
||||
const next = Object.assign({}, root.rememberedApplications);
|
||||
next[appId] = {
|
||||
name: String(notification.appName ?? "").trim() || appId
|
||||
};
|
||||
root.rememberedApplications = next;
|
||||
|
||||
if (root.appRules[appId] === undefined)
|
||||
root.setAppRule(appId, {});
|
||||
return appId;
|
||||
}
|
||||
|
||||
// These policy getters deliberately accept Notification objects, so a lock
|
||||
// screen can use the same source of truth without duplicating app matching.
|
||||
function shouldShowOnLockScreen(notification: Notification): bool {
|
||||
const rule = root.appRule(root.notificationAppId(notification));
|
||||
return rule.enabled && rule.showOnLockScreen;
|
||||
}
|
||||
|
||||
function shouldShowContentOnLockScreen(notification: Notification): bool {
|
||||
const rule = root.appRule(root.notificationAppId(notification));
|
||||
return rule.enabled && rule.showOnLockScreen && rule.showContentOnLockScreen;
|
||||
}
|
||||
|
||||
// history grouped by app, in most-recent-app-first order — the shape
|
||||
// NotificationCenter.qml renders directly.
|
||||
readonly property var groups: {
|
||||
@@ -92,6 +181,10 @@ Singleton {
|
||||
if (notification.lastGeneration)
|
||||
return;
|
||||
|
||||
const appId = root.rememberApplication(notification);
|
||||
if (!root.appRule(appId).enabled)
|
||||
return;
|
||||
|
||||
// Without this the object is destroyed the instant this returns.
|
||||
notification.tracked = true;
|
||||
root.arrivals[notification.id] = new Date();
|
||||
|
||||
Reference in New Issue
Block a user