diff --git a/barstyles/imi/GlobalStates.qml b/barstyles/imi/GlobalStates.qml index 28689f3a..00bc42ee 100644 --- a/barstyles/imi/GlobalStates.qml +++ b/barstyles/imi/GlobalStates.qml @@ -4,7 +4,6 @@ import "modules/common" import "modules/common/functions" import "services" import shell.services as ShellState -import shell.services as HostServices import QtQuick import Quickshell import Quickshell.Io @@ -1629,17 +1628,6 @@ Singleton { // and the close grace, exactly as StyledPopup does for the other popups. property bool mediaWidgetHovered: false - // background.wallpaperPath follows the host Session wallpaper. - function syncWallpaperPath() { - const live = HostServices.Session.wallpaper || ""; - if (live !== "" && Config.ready && Config.options.background.wallpaperPath !== live) - Config.options.background.wallpaperPath = live; - } - Connections { - target: HostServices.Session - function onWallpaperChanged() { root.syncWallpaperPath(); } - } - function setMediaWidgetHovered(hovered) { root.mediaWidgetHovered = hovered; } @@ -2581,7 +2569,6 @@ Singleton { } Component.onCompleted: { - Qt.callLater(root.syncWallpaperPath); animatedLeftSidebarWidth = leftSidebarTargetWidth; animatedRightSidebarWidth = rightSidebarTargetWidth; root.enforceSidebarStyle(); diff --git a/barstyles/imi/Scene.qml b/barstyles/imi/Scene.qml index e44319c7..c4313760 100644 --- a/barstyles/imi/Scene.qml +++ b/barstyles/imi/Scene.qml @@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell +import shell.services as HostServices import Quickshell.Io import "modules/common" import "modules/ii/bar" @@ -119,6 +120,19 @@ Scope { sourceComponent: SettingsWindow {} } + // background.wallpaperPath follows the host Session wallpaper (used by + // the sidebar banner and search previews). + function syncWallpaperPath() { + const live = HostServices.Session.wallpaper || ""; + if (live !== "" && Config.ready && Config.options.background.wallpaperPath !== live) + Config.options.background.wallpaperPath = live; + } + Component.onCompleted: Qt.callLater(scene.syncWallpaperPath) + Connections { + target: HostServices.Session + function onWallpaperChanged() { scene.syncWallpaperPath(); } + } + // Handoff for Ryoku Hub > Bar Studio. IpcHandler { target: "imi" diff --git a/barstyles/imi/VERSION b/barstyles/imi/VERSION index 227cea21..38f77a65 100644 --- a/barstyles/imi/VERSION +++ b/barstyles/imi/VERSION @@ -1 +1 @@ -2.0.0 +2.0.1 diff --git a/barstyles/imi/assets/preview.png b/barstyles/imi/assets/preview.png index 262db789..5b8101ab 100644 Binary files a/barstyles/imi/assets/preview.png and b/barstyles/imi/assets/preview.png differ diff --git a/barstyles/imi/manifest.json b/barstyles/imi/manifest.json index 7714a326..7cda41ca 100644 --- a/barstyles/imi/manifest.json +++ b/barstyles/imi/manifest.json @@ -2,7 +2,7 @@ "schema": 1, "id": "imi", "category": "barstyles", - "version": "2.0.0", + "version": "2.0.1", "destination": "ryoku/barstyles/imi", "files": [ { @@ -17,8 +17,8 @@ "source": "GlobalStates.qml", "destination": "GlobalStates.qml", "mode": "0644", - "size": 153864, - "sha256": "b8955110b9dea3e14e72608ddf19bce8ecccf602a618b89f42f0783af361e3d5", + "size": 153329, + "sha256": "3defad1dd43b947c86cc78bb41dc398268d3b4715e3b3d85b5f1eb3a20c58d72", "install": true }, { @@ -49,8 +49,8 @@ "source": "Scene.qml", "destination": "Scene.qml", "mode": "0644", - "size": 4003, - "sha256": "59a0f4b31e81e99ae841f1579b3f4cfb5bb0baabc68189c1e4c59cbc5b6f786c", + "size": 4614, + "sha256": "fcb5c5829557c186a4fe2c052b385c57653ff745548b1a68ff3ba015be97bb5c", "install": true }, { @@ -66,7 +66,7 @@ "destination": "VERSION", "mode": "0644", "size": 6, - "sha256": "c28fcca53637bc88e124af1725df13cb98c69dedefd62fb3cdbe1cdb6b760624", + "sha256": "fe840f52fb5578d3f77eb497115eb02c6036601c3a53e60df7995c7276baa77a", "install": true }, { @@ -2281,8 +2281,8 @@ "source": "assets/preview.png", "destination": "assets/preview.png", "mode": "0644", - "size": 1048017, - "sha256": "621dc0329d095ee25edebdb9e8070ef64c9a6a4f62a6fb2107dccedda63b2969", + "size": 659831, + "sha256": "d8a626ca7ab84d9913e0634ddce8fe7d4a6b18d17e749d48e128394571a9a9a4", "install": false }, { @@ -7761,8 +7761,8 @@ "source": "modules/ii/bar/popups/bluetooth/ExpressiveBluetoothDevicesPopup.qml", "destination": "modules/ii/bar/popups/bluetooth/ExpressiveBluetoothDevicesPopup.qml", "mode": "0644", - "size": 23495, - "sha256": "4152a3fed6a92a958608f477a36a5ad90c7096dd31fa2fca50cfe472064b1ec7", + "size": 23520, + "sha256": "5458730faea12d15433180b4d34f76e480eb65b8be23f984ccb2b16bd05f51ea", "install": true }, { @@ -12770,7 +12770,7 @@ "destination": "modules/settings/configs/AboutConfig.qml", "mode": "0644", "size": 31950, - "sha256": "ee016658e0614ed502566f028c06ffce5aa8e41f15010de0bfe751fc8ffa7d97", + "sha256": "f2dc2dee7f54adb471d883f6001bcec7b61f69d17de8a3f1cf09cea7fd188960", "install": true }, { @@ -14665,8 +14665,8 @@ "source": "services/Notifications.qml", "destination": "services/Notifications.qml", "mode": "0644", - "size": 21334, - "sha256": "65a75705253ad6b85b512eebe8a930b8c281295202d1fdd3da485e4b54c3b43e", + "size": 24097, + "sha256": "4fd91d21be039ec2d2e0e42c3e2124c010afc576318ad4ed231495723a380804", "install": true }, { diff --git a/barstyles/imi/modules/ii/bar/popups/bluetooth/ExpressiveBluetoothDevicesPopup.qml b/barstyles/imi/modules/ii/bar/popups/bluetooth/ExpressiveBluetoothDevicesPopup.qml index e03f73c6..a17e51af 100644 --- a/barstyles/imi/modules/ii/bar/popups/bluetooth/ExpressiveBluetoothDevicesPopup.qml +++ b/barstyles/imi/modules/ii/bar/popups/bluetooth/ExpressiveBluetoothDevicesPopup.qml @@ -1,3 +1,4 @@ +import "../../../../../" import "../../shared" import "../../../../common" import "../../../../common/functions" diff --git a/barstyles/imi/modules/settings/configs/AboutConfig.qml b/barstyles/imi/modules/settings/configs/AboutConfig.qml index b605de8c..81362deb 100644 --- a/barstyles/imi/modules/settings/configs/AboutConfig.qml +++ b/barstyles/imi/modules/settings/configs/AboutConfig.qml @@ -566,7 +566,7 @@ Item { StyledText { Layout.fillWidth: true - text: "Imi 2.0.0 - maintained by Dodo986" + text: "Imi 2.0.1 - maintained by Dodo986" color: Appearance.colors.colSubtext font.pixelSize: Appearance.font.pixelSize.small } diff --git a/barstyles/imi/services/Notifications.qml b/barstyles/imi/services/Notifications.qml index b38d040c..da697e18 100644 --- a/barstyles/imi/services/Notifications.qml +++ b/barstyles/imi/services/Notifications.qml @@ -93,6 +93,8 @@ Singleton { // null entries ("Cannot append Notif(...) to a QML list"). property list list: [] property var _livePopups: [] + property var _mirrored: [] + property bool _releasingHostPopups: false // The sidebar covering the bar holds the banners back, exactly as the // upstream popups were never armed while it was open. readonly property var popupList: root.popupInhibited ? [] : root._livePopups @@ -128,6 +130,7 @@ Singleton { if (!wrapper) return null; root._serverWrappers[key] = wrapper; + raw.closed.connect(function() { root.forgetPopup(raw.id); }); root._handleShellNotification(wrapper); } return wrapper; @@ -169,6 +172,10 @@ Singleton { if (notif && !notif.internal) alive[String(notif.notificationId)] = true; }); + root._mirrored.forEach((notif) => { + if (notif && !notif.internal) + alive[String(notif.notificationId)] = true; + }); for (const id in root._serverWrappers) { if (!alive[id]) { const gone = root._serverWrappers[id]; @@ -180,32 +187,81 @@ Singleton { function rebuildPopups() { const popups = HostServices.Notifs.popups ?? []; - const out = []; for (let i = 0; i < popups.length; i++) { - if (!popups[i]) + const raw = popups[i]; + if (!raw) continue; - const wrapper = root.ensureWrapper(popups[i]); + const wrapper = root.ensureWrapper(raw); if (!wrapper) continue; + for (const entry of root._mirrored) { + if (entry && entry !== wrapper && entry.popup && root.sameContent(entry.notification ?? {}, raw)) { + root.stopTimeout(entry); + entry.popup = false; + } + } + if (root._mirrored.indexOf(wrapper) === -1) + root._mirrored = root._mirrored.concat([wrapper]); wrapper.popup = true; - out.push(wrapper); + root.armNotificationTimeout(wrapper); // Count each popup once, no matter how often the host re-fronts it. - const key = String(popups[i].id); + const key = String(raw.id); if (!(key in root._countedPopupIds)) { root._countedPopupIds[key] = true; root.unread++; } } + + const out = root._mirrored.filter(entry => entry && entry.popup); + root._mirrored = out.filter(entry => !entry.internal); for (let j = 0; j < root._internalList.length; j++) { if (root._internalList[j].popup) out.push(root._internalList[j]); } + out.sort((a, b) => (b?.time ?? 0) - (a?.time ?? 0)); root._livePopups = out; for (const id in root._serverWrappers) { const wrapper = root._serverWrappers[id]; if (wrapper.popup && out.indexOf(wrapper) === -1) wrapper.popup = false; } + + // The host banner window draws from this list. + if (!root._releasingHostPopups) { + root._releasingHostPopups = true; + for (let k = 0; k < popups.length; k++) { + if (popups[k]) + HostServices.Notifs.removePopup(popups[k]); + } + root._releasingHostPopups = false; + } + } + + function stopTimeout(notifObject) { + if (!notifObject) + return; + const timer = notifObject.timer ?? null; + notifObject.timer = null; + if (timer) { + timer.stop(); + timer.destroy(); + } + } + + function sameContent(a, b) { + return (a?.appName || "") === (b?.appName || "") + && (a?.summary || "") === (b?.summary || "") + && (a?.body || "") === (b?.body || ""); + } + + function forgetPopup(id) { + const wrapper = root._serverWrappers[String(id)]; + if (!wrapper || !wrapper.popup) + return; + root.stopTimeout(wrapper); + wrapper.popup = false; + root._mirrored = root._mirrored.filter(entry => entry !== wrapper); + root.rebuildPopups(); } function releaseNotificationObject(notifObject) { @@ -308,14 +364,23 @@ Singleton { } function armNotificationTimeout(notifObject) { - if (root.popupInhibited) + if (notifObject.internal && root.popupInhibited) return; - const expireTimeout = notifObject.internalExpireTimeout; + const fallback = Config?.options.notifications.timeout ?? 7000; + let expireTimeout; + if (notifObject.internal) { + expireTimeout = notifObject.internalExpireTimeout; + } else { + const raw = notifObject.notification; + if (!raw) + return; + expireTimeout = raw.urgency === NotificationUrgency.Critical ? 0 : raw.expireTimeout; + } if (expireTimeout === 0) return; const interval = expireTimeout < 0 - ? (Config?.options.notifications.timeout ?? 7000) - : expireTimeout; + ? fallback + : (notifObject.internal ? expireTimeout : Math.min(fallback, expireTimeout)); notifObject.popup = true; if (notifObject.timer) { notifObject.timer.interval = interval; @@ -407,6 +472,8 @@ Singleton { const internals = root._internalList.slice(); root._internalList = []; internals.forEach(notif => root.releaseNotificationObject(notif)); + root._mirrored.forEach(notif => root.stopTimeout(notif)); + root._mirrored = []; // History items first (the host marks them seen), then whatever // transient popups are still up - they never entered history. HostServices.Notifs.clearAll(); @@ -421,24 +488,28 @@ Singleton { function cancelTimeout(id) { const wrapper = root.findNotification(id); - if (wrapper && wrapper.internal) { - // The timer is gone once it has fired. - wrapper.timer?.stop(); - } else { - HostServices.Notifs.cancelPopupTimeout(id); + if (!wrapper) + return; + // The timer is gone once it has fired. + if (wrapper.timer) { + wrapper.timer.stop(); + wrapper.timer.destroy(); + wrapper.timer = null; } + if (!wrapper.internal) + HostServices.Notifs.cancelPopupTimeout(id); } function timeoutNotification(id) { const wrapper = root.findNotification(id); if (!wrapper) return; - if (wrapper.internal) { - wrapper.popup = false; - } else if (wrapper.notification) { + root.stopTimeout(wrapper); + wrapper.popup = false; + if (!wrapper.internal && wrapper.notification) HostServices.Notifs.removePopup(wrapper.notification); - wrapper.popup = false; - } + root._mirrored = root._mirrored.filter(entry => entry !== wrapper); + root.rebuildPopups(); root.timeout(id); } diff --git a/barstyles/registry.json b/barstyles/registry.json index 745e0842..e6f8b22e 100644 --- a/barstyles/registry.json +++ b/barstyles/registry.json @@ -49,7 +49,7 @@ { "id": "imi", "name": "Imi", - "version": "2.0.0", + "version": "2.0.1", "path": "barstyles/imi", "author": "dodo986", "upstream": "https://github.com/ryoku-dev/ryostore/tree/main/barstyles/imi", @@ -69,8 +69,8 @@ "preview": "assets/preview.png", "screenshots": [], "manifest": "manifest.json", - "manifestSha256": "c97045cef32156f8e3f419e304559ecc3f3a579ca4576a60315f9a7d51487da9", - "lastUpdated": "2026-11-03", + "manifestSha256": "b78d4b81cd186d049c426c8414a3cd2ca0f90882de0abb314d586165c017744e", + "lastUpdated": "2026-11-05", "official": false, "license": "GPL-3.0-only" },