diff --git a/Bar/Bar.qml b/Bar/Bar.qml index 5b127b6..b6e3945 100644 --- a/Bar/Bar.qml +++ b/Bar/Bar.qml @@ -82,6 +82,11 @@ Scope { anchors.rightMargin: 18 spacing: 12 + NotificationHistory { + id: notificationHistoryWin + anchors.verticalCenter: parent.verticalCenter + } + Brightness { id: widgetsBrightness anchors.verticalCenter: parent.verticalCenter diff --git a/Bar/Modules/NotificationHistory.qml b/Bar/Modules/NotificationHistory.qml new file mode 100644 index 0000000..0904e11 --- /dev/null +++ b/Bar/Modules/NotificationHistory.qml @@ -0,0 +1,403 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Io +import qs.Components +import qs.Settings + +// Notification Bell Icon as root +Item { + id: notificationBellButton + // Notification bell icon button (shows/hides history popup) + property alias bellVisible: bellBg.visible + signal bellClicked() + width: 22 + height: 22 + Rectangle { + id: bellBg + width: 22 + height: 22 + radius: 11 + color: mouseAreaBell.containsMouse ? Theme.accentPrimary : "transparent" + visible: mouseAreaBell.containsMouse + anchors.centerIn: parent + } + Text { + anchors.centerIn: parent + text: "notifications" + font.family: mouseAreaBell.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined" + font.pixelSize: 16 + color: mouseAreaBell.containsMouse ? Theme.backgroundPrimary : Theme.textPrimary + } + MouseArea { + id: mouseAreaBell + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: notificationHistoryWin.visible = !notificationHistoryWin.visible // Toggle popup + onEntered: if (!notificationHistoryWin.visible) bellTooltip.tooltipVisible = true + onExited: bellTooltip.tooltipVisible = false + } + StyledTooltip { + id: bellTooltip + text: "Notification History" + tooltipVisible: false + targetItem: notificationBellButton + delay: 200 + } + // Hide tooltip when popup is open + Connections { + target: notificationHistoryWin + function onVisibleChanged() { + if (notificationHistoryWin.visible) bellTooltip.tooltipVisible = false; + } + } + + // Notification history + PanelWindow { + id: notificationHistoryWin + width: 400 + height: 500 + color: "transparent" + visible: false + screen: Quickshell.primaryScreen + focusable: true + anchors.top: true + anchors.right: true + margins.top: 4 + margins.right: 4 + + property int maxHistory: 100 + property string configDir: Quickshell.configDir + property string historyFilePath: configDir + "/notification_history.json" + + ListModel { + id: historyModel // Holds notification objects + } + + FileView { + id: historyFileView + path: historyFilePath + blockLoading: true + printErrors: true + watchChanges: true + + JsonAdapter { + id: historyAdapter + property var notifications: [] // Array of notification objects + } + + onFileChanged: { + reload() // Reload if file changes on disk + } + + onLoaded: { + loadHistory() // Populate model after loading + } + + onLoadFailed: function(error) { + console.error("Failed to load history file:", error) + if (error.includes("No such file")) { + historyAdapter.notifications = [] // Create new file if missing + writeAdapter() + } + } + + onSaved: {} + onSaveFailed: function(error) { + console.error("Failed to save history:", error) + } + + Component.onCompleted: { + if (path) reload() + } + } + + function loadHistory() { + if (historyAdapter.notifications) { + historyModel.clear() + const notifications = historyAdapter.notifications + const count = Math.min(notifications.length, maxHistory) + for (let i = 0; i < count; i++) { + if (typeof notifications[i] === 'object' && notifications[i] !== null) { + historyModel.append(notifications[i]) + } + } + } + } + + function saveHistory() { + const historyArray = [] + const count = Math.min(historyModel.count, maxHistory) + for (let i = 0; i < count; ++i) { + let obj = historyModel.get(i) + if (typeof obj === 'object' && obj !== null) { + historyArray.push({ + id: obj.id, + appName: obj.appName, + summary: obj.summary, + body: obj.body, + timestamp: obj.timestamp + }) + } + } + historyAdapter.notifications = historyArray + Qt.callLater(function() { + historyFileView.writeAdapter() + }) + } + + function addToHistory(notification) { + if (!notification.id) notification.id = Date.now() + if (!notification.timestamp) notification.timestamp = new Date().toISOString() + for (let i = 0; i < historyModel.count; ++i) { + if (historyModel.get(i).id === notification.id) { + historyModel.remove(i) + break + } + } + historyModel.insert(0, notification) + if (historyModel.count > maxHistory) historyModel.remove(maxHistory) + saveHistory() + } + + function clearHistory() { + historyModel.clear() + historyAdapter.notifications = [] + historyFileView.writeAdapter() + } + + function formatTimestamp(ts) { + if (!ts) return ""; + var date = typeof ts === "number" ? new Date(ts) : new Date(Date.parse(ts)); + var y = date.getFullYear(); + var m = (date.getMonth()+1).toString().padStart(2,'0'); + var d = date.getDate().toString().padStart(2,'0'); + var h = date.getHours().toString().padStart(2,'0'); + var min = date.getMinutes().toString().padStart(2,'0'); + return `${y}-${m}-${d} ${h}:${min}`; + } + + Rectangle { + width: notificationHistoryWin.width + height: notificationHistoryWin.height + anchors.fill: parent + color: Theme.backgroundPrimary + radius: 20 + + Column { + anchors.fill: parent + anchors.margins: 16 + spacing: 8 + + Row { + width: parent.width + spacing: 8 + anchors.top: parent.top + anchors.topMargin: 16 + Text { + text: "Notification History" + font.pixelSize: 18 + font.bold: true + color: Theme.textPrimary + anchors.verticalCenter: parent.verticalCenter + } + Rectangle { + id: clearAllButton + width: 110 + height: 32 + radius: 20 + color: clearAllMouseArea.containsMouse ? Theme.accentPrimary : Theme.surfaceVariant + border.color: Theme.accentPrimary + border.width: 1 + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + Row { + anchors.centerIn: parent + spacing: 6 + Text { + text: "delete_sweep" + font.family: "Material Symbols Outlined" + font.pixelSize: 14 + color: clearAllMouseArea.containsMouse ? Theme.onAccent : Theme.accentPrimary + verticalAlignment: Text.AlignVCenter + } + Text { + text: "Clear All" + font.pixelSize: Theme.fontSizeSmall + color: clearAllMouseArea.containsMouse ? Theme.onAccent : Theme.accentPrimary + font.bold: true + verticalAlignment: Text.AlignVCenter + } + } + MouseArea { + id: clearAllMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: notificationHistoryWin.clearHistory() + } + } + } + + Rectangle { + width: parent.width + height: 0 + color: "transparent" + visible: true + } + + Rectangle { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 56 + anchors.bottom: parent.bottom + color: Theme.surfaceVariant + radius: 20 + + Rectangle { + anchors.fill: parent + color: Theme.backgroundPrimary + radius: 20 + border.width: 1 + border.color: Theme.surfaceVariant + z: 0 + } + Rectangle { + id: listContainer + anchors.fill: parent + anchors.topMargin: 12 + anchors.bottomMargin: 12 + color: "transparent" + clip: true + ListView { + id: historyList + anchors.fill: parent + spacing: 12 + model: historyModel + delegate: Item { + width: parent.width + height: notificationCard.implicitHeight + 12 + Rectangle { + id: notificationCard + width: parent.width - 24 + anchors.horizontalCenter: parent.horizontalCenter + color: Theme.backgroundPrimary + radius: 16 + border.color: Theme.outline + border.width: 1 + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.margins: 0 + implicitHeight: contentColumn.implicitHeight + 20 + Rectangle { + id: removeButton + width: 24 + height: 24 + radius: 12 + color: removeMouseArea.containsMouse ? Theme.error : Theme.surfaceVariant + border.color: Theme.error + border.width: 1 + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: 8 + anchors.rightMargin: 8 + z: 2 + Row { + anchors.centerIn: parent + spacing: 0 + Text { + text: "close" + font.family: "Material Symbols Outlined" + font.pixelSize: 16 + color: removeMouseArea.containsMouse ? Theme.onError : Theme.error + verticalAlignment: Text.AlignVCenter + } + } + MouseArea { + id: removeMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + historyModel.remove(index) + saveHistory() + } + } + } + Column { + id: contentColumn + anchors.fill: parent + anchors.margins: 14 + spacing: 6 + Row { + spacing: 8 + anchors.left: parent.left + anchors.right: parent.right + Rectangle { + id: iconBackground + width: 28 + height: 28 + radius: 20 + color: Theme.accentPrimary + border.color: Qt.darker(Theme.accentPrimary, 1.2) + border.width: 1.2 + anchors.verticalCenter: parent.verticalCenter + Text { + anchors.centerIn: parent + text: model.appName ? model.appName.charAt(0).toUpperCase() : "?" + font.family: Theme.fontFamily + font.pixelSize: 15 + font.bold: true + color: Theme.backgroundPrimary + } + } + Column { + spacing: 0 + anchors.verticalCenter: parent.verticalCenter + Text { + text: model.appName || "Unknown App" + font.bold: true + color: Theme.textPrimary + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeSmall + verticalAlignment: Text.AlignVCenter + } + Text { + text: formatTimestamp(model.timestamp) + color: Theme.textSecondary + font.family: Theme.fontFamily + font.pixelSize: 11 + verticalAlignment: Text.AlignVCenter + } + } + } + Text { + text: model.summary || "" + color: Theme.textSecondary + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeSmall + width: parent.width + wrapMode: Text.Wrap + } + Text { + text: model.body || "" + color: Theme.textDisabled + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeCaption + width: parent.width + wrapMode: Text.Wrap + } + } + } + } + } + } + } + + Rectangle { width: 1; height: 24; color: "transparent" } + } + } + } +} \ No newline at end of file diff --git a/Settings/Theme.json b/Settings/Theme.json index f00a3f5..c20e7b1 100644 --- a/Settings/Theme.json +++ b/Settings/Theme.json @@ -1,28 +1,28 @@ { - "backgroundPrimary": "#111211", - "backgroundSecondary": "#1D1E1D", - "backgroundTertiary": "#292A29", + "backgroundPrimary": "#0E0F10", + "backgroundSecondary": "#1A1B1C", + "backgroundTertiary": "#262728", - "surface": "#242524", - "surfaceVariant": "#353635", + "surface": "#212223", + "surfaceVariant": "#323334", - "textPrimary": "#F4E9E3", - "textSecondary": "#DCD2CC", - "textDisabled": "#928C88", + "textPrimary": "#F0F1E0", + "textSecondary": "#D8D9CA", + "textDisabled": "#909186", - "accentPrimary": "#7D8079", - "accentSecondary": "#979994", - "accentTertiary": "#646661", + "accentPrimary": "#A3A485", + "accentSecondary": "#B5B69D", + "accentTertiary": "#82836A", - "error": "#939849", - "warning": "#ABAF72", + "error": "#A5A9ED", + "warning": "#B9BCF1", - "highlight": "#B1B3AF", - "rippleEffect": "#8A8D86", + "highlight": "#C8C8B6", + "rippleEffect": "#ACAD91", - "onAccent": "#111211", - "outline": "#585958", + "onAccent": "#0E0F10", + "outline": "#565758", - "shadow": "#111211", - "overlay": "#111211" + "shadow": "#0E0F10", + "overlay": "#0E0F10" }