Add notification history
This commit is contained in:
parent
cc0ea7f37a
commit
9008d6bab9
4 changed files with 288 additions and 2 deletions
|
|
@ -4,6 +4,7 @@ import QtQuick.Layouts
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
import qs.Modules.Notification
|
||||||
|
|
||||||
Variants {
|
Variants {
|
||||||
model: Quickshell.screens
|
model: Quickshell.screens
|
||||||
|
|
@ -83,6 +84,10 @@ Variants {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Notification Icon
|
// TODO: Notification Icon
|
||||||
|
NotificationHistory {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
WiFi {
|
WiFi {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,8 @@ PanelWindow {
|
||||||
WlrLayershell.layer: WlrLayer.Overlay
|
WlrLayershell.layer: WlrLayer.Overlay
|
||||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||||
|
|
||||||
// Use the notification service
|
// Use the notification service singleton
|
||||||
property var notificationService: NotificationService {}
|
property var notificationService: NotificationService
|
||||||
|
|
||||||
// Access the notification model from the service
|
// Access the notification model from the service
|
||||||
property ListModel notificationModel: notificationService.notificationModel
|
property ListModel notificationModel: notificationService.notificationModel
|
||||||
|
|
|
||||||
180
Modules/Notification/NotificationHistory.qml
Normal file
180
Modules/Notification/NotificationHistory.qml
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Services.Notifications
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property real scaling: Scaling.scale(screen)
|
||||||
|
sizeMultiplier: 0.8
|
||||||
|
showBorder: false
|
||||||
|
icon: "notifications"
|
||||||
|
tooltipText: "Notification History"
|
||||||
|
onClicked: {
|
||||||
|
notificationHistoryLoader.active = !notificationHistoryLoader.active
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loader for Notification History menu
|
||||||
|
NLoader {
|
||||||
|
id: notificationHistoryLoader
|
||||||
|
active: false
|
||||||
|
|
||||||
|
content: Component {
|
||||||
|
NPanel {
|
||||||
|
id: notificationPanel
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: notificationPanel
|
||||||
|
ignoreUnknownSignals: true
|
||||||
|
function onDismissed() {
|
||||||
|
notificationHistoryLoader.active = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
color: Colors.backgroundSecondary
|
||||||
|
radius: Style.radiusMedium * scaling
|
||||||
|
border.color: Colors.backgroundTertiary
|
||||||
|
border.width: Math.max(1, Style.borderMedium * scaling)
|
||||||
|
width: 400 * scaling
|
||||||
|
height: 500 * scaling
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.topMargin: Style.marginTiny * scaling
|
||||||
|
anchors.rightMargin: Style.marginTiny * scaling
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Style.marginLarge * scaling
|
||||||
|
spacing: Style.marginMedium * scaling
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Style.marginMedium * scaling
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: "notifications"
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pointSize: Style.fontSizeXL * scaling
|
||||||
|
color: Colors.accentPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: "Notification History"
|
||||||
|
font.pointSize: Style.fontSizeLarge * scaling
|
||||||
|
font.bold: true
|
||||||
|
color: Colors.textPrimary
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
icon: "delete"
|
||||||
|
sizeMultiplier: 0.8
|
||||||
|
tooltipText: "Clear history"
|
||||||
|
onClicked: NotificationService.clearHistory()
|
||||||
|
}
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
icon: "close"
|
||||||
|
sizeMultiplier: 0.8
|
||||||
|
onClicked: {
|
||||||
|
notificationHistoryLoader.active = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NDivider {}
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: notificationList
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
model: NotificationService.historyModel
|
||||||
|
spacing: Style.marginMedium * scaling
|
||||||
|
clip: true
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
width: notificationList ? (notificationList.width - 20) : 380 * scaling
|
||||||
|
height: 80
|
||||||
|
radius: Style.radiusMedium * scaling
|
||||||
|
color: notificationMouseArea.containsMouse ? Colors.accentPrimary : "transparent"
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors {
|
||||||
|
fill: parent
|
||||||
|
margins: 15
|
||||||
|
}
|
||||||
|
spacing: 15
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Notification content
|
||||||
|
Column {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
spacing: 5
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: (summary || "No summary").substring(0, 100)
|
||||||
|
font.pointSize: Style.fontSizeMedium * scaling
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: notificationMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textPrimary
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
width: parent.width - 30
|
||||||
|
maximumLineCount: 2
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: (body || "").substring(0, 150)
|
||||||
|
font.pointSize: Style.fontSizeSmall * scaling
|
||||||
|
color: notificationMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textSecondary
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
width: parent.width - 30
|
||||||
|
maximumLineCount: 3
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: NotificationService.formatTimestamp(timestamp)
|
||||||
|
font.pointSize: Style.fontSizeSmall * scaling
|
||||||
|
color: notificationMouseArea.containsMouse ? Colors.backgroundPrimary : Colors.textSecondary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: notificationMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
onClicked: {
|
||||||
|
console.log("[NotificationHistory] Removing notification:", summary)
|
||||||
|
NotificationService.historyModel.remove(index)
|
||||||
|
NotificationService.saveHistory()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollBar.vertical: ScrollBar {
|
||||||
|
active: true
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import Quickshell.Services.Notifications
|
import Quickshell.Services.Notifications
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
id: root
|
id: root
|
||||||
|
|
@ -34,12 +37,45 @@ QtObject {
|
||||||
|
|
||||||
// Add to our model
|
// Add to our model
|
||||||
root.addNotification(notification)
|
root.addNotification(notification)
|
||||||
|
// Also add to history
|
||||||
|
root.addToHistory(notification)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// List model to hold notifications
|
// List model to hold notifications
|
||||||
property ListModel notificationModel: ListModel {}
|
property ListModel notificationModel: ListModel {}
|
||||||
|
|
||||||
|
// Persistent history of notifications (most recent first)
|
||||||
|
property ListModel historyModel: ListModel {}
|
||||||
|
property int maxHistory: 100
|
||||||
|
|
||||||
|
// Cached history file path
|
||||||
|
property string historyFile: Quickshell.env("NOCTALIA_NOTIF_HISTORY_FILE") || (Settings.cacheDir + "notifications.json")
|
||||||
|
|
||||||
|
// Persisted storage for history
|
||||||
|
property FileView historyFileView: FileView {
|
||||||
|
id: historyFileView
|
||||||
|
objectName: "notificationHistoryFileView"
|
||||||
|
path: historyFile
|
||||||
|
watchChanges: true
|
||||||
|
onFileChanged: reload()
|
||||||
|
onAdapterUpdated: writeAdapter()
|
||||||
|
Component.onCompleted: reload()
|
||||||
|
onLoaded: loadFromHistory()
|
||||||
|
onLoadFailed: function (error) {
|
||||||
|
// Create file on first use
|
||||||
|
if (error.toString().includes("No such file") || error === 2) {
|
||||||
|
writeAdapter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonAdapter {
|
||||||
|
id: historyAdapter
|
||||||
|
property var history: []
|
||||||
|
property double timestamp: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Maximum visible notifications
|
// Maximum visible notifications
|
||||||
property int maxVisible: 5
|
property int maxVisible: 5
|
||||||
|
|
||||||
|
|
@ -84,6 +120,71 @@ QtObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add a simplified copy into persistent history
|
||||||
|
function addToHistory(notification) {
|
||||||
|
historyModel.insert(0, {
|
||||||
|
"summary": notification.summary,
|
||||||
|
"body": notification.body,
|
||||||
|
"appName": notification.appName,
|
||||||
|
"urgency": notification.urgency,
|
||||||
|
"timestamp": new Date()
|
||||||
|
})
|
||||||
|
while (historyModel.count > maxHistory) {
|
||||||
|
historyModel.remove(historyModel.count - 1)
|
||||||
|
}
|
||||||
|
saveHistory()
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearHistory() {
|
||||||
|
historyModel.clear()
|
||||||
|
saveHistory()
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadFromHistory() {
|
||||||
|
// Populate in-memory model from adapter
|
||||||
|
try {
|
||||||
|
historyModel.clear()
|
||||||
|
const items = historyAdapter.history || []
|
||||||
|
for (var i = 0; i < items.length; i++) {
|
||||||
|
const it = items[i]
|
||||||
|
historyModel.append({
|
||||||
|
"summary": it.summary || "",
|
||||||
|
"body": it.body || "",
|
||||||
|
"appName": it.appName || "",
|
||||||
|
"urgency": it.urgency,
|
||||||
|
"timestamp": it.timestamp ? new Date(it.timestamp) : new Date()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[Notifications] Failed to load history:", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveHistory() {
|
||||||
|
try {
|
||||||
|
// Serialize model back to adapter
|
||||||
|
var arr = []
|
||||||
|
for (var i = 0; i < historyModel.count; i++) {
|
||||||
|
const n = historyModel.get(i)
|
||||||
|
arr.push({
|
||||||
|
summary: n.summary,
|
||||||
|
body: n.body,
|
||||||
|
appName: n.appName,
|
||||||
|
urgency: n.urgency,
|
||||||
|
timestamp: (n.timestamp instanceof Date) ? n.timestamp.getTime() : n.timestamp
|
||||||
|
})
|
||||||
|
}
|
||||||
|
historyAdapter.history = arr
|
||||||
|
historyAdapter.timestamp = Time.timestamp
|
||||||
|
|
||||||
|
Qt.callLater(function () {
|
||||||
|
historyFileView.writeAdapter()
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[Notifications] Failed to save history:", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Signal to trigger animation before removal
|
// Signal to trigger animation before removal
|
||||||
signal animateAndRemove(var notification, int index)
|
signal animateAndRemove(var notification, int index)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue