1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 21:42:51 -05:00

notifications: initial refactory of popups

This commit is contained in:
bbedward
2025-07-25 22:39:01 -04:00
parent d28df49f18
commit ed7370361e
4 changed files with 476 additions and 816 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,115 @@
import QtQuick
import qs.Common
import qs.Services
Item {
id: root
property list<var> popupInstances: []
property int baseNotificationHeight: 130 // Height of a single notification + margins
property bool dismissalInProgress: false
property int maxTargetNotifications: 3 // Target number of notifications to maintain
Timer {
id: dismissalTimer
interval: 500 // Half second delay between dismissals
onTriggered: dismissNextOldest()
}
Timer {
id: dismissalDelayTimer
interval: 1 // Start immediately
onTriggered: startSequentialDismissal()
}
Component {
id: popupComponent
NotificationPopup {}
}
Connections {
target: NotificationService
function onNotificationQueueChanged() {
syncPopupsWithQueue();
repositionAll();
}
function onVisibleNotificationsChanged() {
repositionAll();
}
}
function syncPopupsWithQueue() {
const queue = NotificationService.notificationQueue.filter(n => n && n.popup);
// Clean up destroyed popups first
popupInstances = popupInstances.filter(p => p && p.notificationId);
// DON'T aggressively destroy popups - let them handle their own lifecycle
// Only remove popups that are actually destroyed/invalid
// The popup will destroy itself when notificationData.popup becomes false
// Only create NEW notifications, don't touch existing ones AT ALL
for (const notif of queue) {
const existingPopup = popupInstances.find(p => p.notificationId === notif.notification.id);
if (existingPopup) {
// CRITICAL: Do absolutely NOTHING to existing popups
// Don't change their verticalOffset, don't touch any properties
continue;
}
// Calculate position for NEW notification only - at the bottom of ACTIVE stack
const currentActive = popupInstances.filter(p => p && p.notificationData && p.notificationData.popup).length;
const popup = popupComponent.createObject(root, {
notificationData: notif,
notificationId: notif.notification.id,
verticalOffset: currentActive * baseNotificationHeight // ✅ bottom of active stack
});
if (popup) {
popupInstances.push(popup);
// Pin it until entrance finishes, then maybe start overflow
popup.entered.connect(function() {
repositionAll(); // it's now "stable"; allow vertical compaction
maybeStartOverflow(); // defer overflow until after slot N is fully occupied
});
}
}
// Overflow dismissal handled in Connections now
}
function repositionAll() {
// Only compact stable (non-entering) active popups
const stable = popupInstances.filter(p => p && p.notificationData && p.notificationData.popup && !p.entering);
for (let i = 0; i < stable.length; ++i)
stable[i].verticalOffset = i * baseNotificationHeight;
// Newcomers keep their creation-time offset (slot N) until `entered()`
}
function maybeStartOverflow() {
const active = popupInstances.filter(p => p && p.notificationData && p.notificationData.popup);
if (active.length > maxTargetNotifications && !dismissalInProgress)
startSequentialDismissal();
}
function startSequentialDismissal() {
if (dismissalInProgress) return; // Don't start multiple dismissals
dismissalInProgress = true;
dismissNextOldest();
}
function dismissNextOldest() {
const active = popupInstances.filter(p => p && p.notificationData.popup);
if (active.length <= maxTargetNotifications) { dismissalInProgress = false; return; }
const oldest = active[0];
if (oldest) {
oldest.notificationData.removedByLimit = true;
oldest.notificationData.popup = false; // triggers slide-out in popup
dismissalTimer.restart(); // your existing 500ms is fine
}
}
}

View File

@@ -13,6 +13,20 @@ Singleton {
readonly property list<NotifWrapper> allWrappers: []
readonly property list<NotifWrapper> popups: allWrappers.filter(n => n.popup)
property list<NotifWrapper> notificationQueue: []
property list<NotifWrapper> visibleNotifications: []
property int maxVisibleNotifications: 3
property bool addGateBusy: false
property int enterAnimMs: 400
Timer {
id: addGate
interval: enterAnimMs + 50
running: false
repeat: false
onTriggered: { addGateBusy = false; processQueue(); }
}
// Android 16-style grouped notifications
readonly property var groupedNotifications: getGroupedNotifications()
readonly property var groupedPopups: getGroupedPopups()
@@ -39,29 +53,22 @@ Singleton {
inlineReplySupported: true
onNotification: notif => {
console.log("=== RAW NOTIFICATION DATA ===");
console.log("appName:", notif.appName);
console.log("summary:", notif.summary);
console.log("body:", notif.body);
console.log("appIcon:", notif.appIcon);
console.log("image:", notif.image);
console.log("urgency:", notif.urgency);
console.log("hasInlineReply:", notif.hasInlineReply);
console.log("=============================");
notif.tracked = true;
const wrapper = notifComponent.createObject(root, {
popup: true, // Always show as popup initially
popup: !root.popupsDisabled,
notification: notif
});
if (wrapper) {
const groupKey = getGroupKey(wrapper);
root.allWrappers.push(wrapper);
root.notifications.push(wrapper);
addToPersistentStorage(wrapper);
if (!root.popupsDisabled) {
notificationQueue = [...notificationQueue, wrapper];
processQueue();
}
}
}
}
@@ -70,15 +77,20 @@ Singleton {
id: wrapper
property bool popup: false
property bool removedByLimit: false
Component.onCompleted: {
popup = !root.popupsDisabled;
onPopupChanged: {
if (!popup) {
removeFromVisibleNotifications(wrapper);
}
}
// Don't override popup in onCompleted - it's set correctly during creation
readonly property Timer timer: Timer {
interval: 5000
repeat: false
running: wrapper.popup
running: false
onTriggered: {
wrapper.popup = false;
}
@@ -185,11 +197,37 @@ Singleton {
function disablePopups(disable) {
popupsDisabled = disable;
if (disable) {
notificationQueue = [];
visibleNotifications = [];
for (const notif of root.allWrappers) {
notif.popup = false;
}
}
}
function processQueue() {
if (addGateBusy) return;
if (popupsDisabled) return;
if (notificationQueue.length === 0) return;
const [next, ...rest] = notificationQueue;
notificationQueue = rest;
visibleNotifications = [...visibleNotifications, next];
next.popup = true;
addGateBusy = true;
addGate.restart();
}
function removeFromVisibleNotifications(wrapper) {
const i = visibleNotifications.findIndex(n => n === wrapper);
if (i !== -1) {
const v = [...visibleNotifications]; v.splice(i, 1);
visibleNotifications = v;
processQueue();
}
}
// Android 16-style notification grouping functions
@@ -264,11 +302,6 @@ Singleton {
}
return Object.values(groups).sort((a, b) => {
const aUrgency = a.latestNotification.urgency || 0;
const bUrgency = b.latestNotification.urgency || 0;
if (aUrgency !== bUrgency) {
return bUrgency - aUrgency;
}
return b.latestNotification.time.getTime() - a.latestNotification.time.getTime();
});
}

View File

@@ -49,8 +49,8 @@ ShellRoot {
id: notificationCenter
}
NotificationPopup {
id: notificationPopup
NotificationPopupManager {
id: notificationPopupManager
}
ControlCenterPopout {