mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-29 07:52:50 -05:00
notifications: initial refactory of popups
This commit is contained in:
File diff suppressed because it is too large
Load Diff
115
Modules/Notifications/NotificationPopupManager.qml
Normal file
115
Modules/Notifications/NotificationPopupManager.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,20 @@ Singleton {
|
|||||||
readonly property list<NotifWrapper> allWrappers: []
|
readonly property list<NotifWrapper> allWrappers: []
|
||||||
readonly property list<NotifWrapper> popups: allWrappers.filter(n => n.popup)
|
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
|
// Android 16-style grouped notifications
|
||||||
readonly property var groupedNotifications: getGroupedNotifications()
|
readonly property var groupedNotifications: getGroupedNotifications()
|
||||||
readonly property var groupedPopups: getGroupedPopups()
|
readonly property var groupedPopups: getGroupedPopups()
|
||||||
@@ -39,29 +53,22 @@ Singleton {
|
|||||||
inlineReplySupported: true
|
inlineReplySupported: true
|
||||||
|
|
||||||
onNotification: notif => {
|
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;
|
notif.tracked = true;
|
||||||
|
|
||||||
const wrapper = notifComponent.createObject(root, {
|
const wrapper = notifComponent.createObject(root, {
|
||||||
popup: true, // Always show as popup initially
|
popup: !root.popupsDisabled,
|
||||||
notification: notif
|
notification: notif
|
||||||
});
|
});
|
||||||
|
|
||||||
if (wrapper) {
|
if (wrapper) {
|
||||||
const groupKey = getGroupKey(wrapper);
|
|
||||||
|
|
||||||
root.allWrappers.push(wrapper);
|
root.allWrappers.push(wrapper);
|
||||||
root.notifications.push(wrapper);
|
root.notifications.push(wrapper);
|
||||||
addToPersistentStorage(wrapper);
|
addToPersistentStorage(wrapper);
|
||||||
|
|
||||||
|
if (!root.popupsDisabled) {
|
||||||
|
notificationQueue = [...notificationQueue, wrapper];
|
||||||
|
processQueue();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,15 +77,20 @@ Singleton {
|
|||||||
id: wrapper
|
id: wrapper
|
||||||
|
|
||||||
property bool popup: false
|
property bool popup: false
|
||||||
|
property bool removedByLimit: false
|
||||||
|
|
||||||
Component.onCompleted: {
|
onPopupChanged: {
|
||||||
popup = !root.popupsDisabled;
|
if (!popup) {
|
||||||
|
removeFromVisibleNotifications(wrapper);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't override popup in onCompleted - it's set correctly during creation
|
||||||
|
|
||||||
readonly property Timer timer: Timer {
|
readonly property Timer timer: Timer {
|
||||||
interval: 5000
|
interval: 5000
|
||||||
repeat: false
|
repeat: false
|
||||||
running: wrapper.popup
|
running: false
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
wrapper.popup = false;
|
wrapper.popup = false;
|
||||||
}
|
}
|
||||||
@@ -185,11 +197,37 @@ Singleton {
|
|||||||
function disablePopups(disable) {
|
function disablePopups(disable) {
|
||||||
popupsDisabled = disable;
|
popupsDisabled = disable;
|
||||||
if (disable) {
|
if (disable) {
|
||||||
|
notificationQueue = [];
|
||||||
|
visibleNotifications = [];
|
||||||
for (const notif of root.allWrappers) {
|
for (const notif of root.allWrappers) {
|
||||||
notif.popup = false;
|
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
|
// Android 16-style notification grouping functions
|
||||||
@@ -264,11 +302,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Object.values(groups).sort((a, b) => {
|
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();
|
return b.latestNotification.time.getTime() - a.latestNotification.time.getTime();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user