Add notification history (NotificationHistory.qml)
This commit is contained in:
parent
b803b097a5
commit
e55cbb934b
3 changed files with 427 additions and 19 deletions
|
|
@ -82,6 +82,11 @@ Scope {
|
||||||
anchors.rightMargin: 18
|
anchors.rightMargin: 18
|
||||||
spacing: 12
|
spacing: 12
|
||||||
|
|
||||||
|
NotificationHistory {
|
||||||
|
id: notificationHistoryWin
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
Brightness {
|
Brightness {
|
||||||
id: widgetsBrightness
|
id: widgetsBrightness
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
|
||||||
403
Bar/Modules/NotificationHistory.qml
Normal file
403
Bar/Modules/NotificationHistory.qml
Normal file
|
|
@ -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" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,28 +1,28 @@
|
||||||
{
|
{
|
||||||
"backgroundPrimary": "#111211",
|
"backgroundPrimary": "#0E0F10",
|
||||||
"backgroundSecondary": "#1D1E1D",
|
"backgroundSecondary": "#1A1B1C",
|
||||||
"backgroundTertiary": "#292A29",
|
"backgroundTertiary": "#262728",
|
||||||
|
|
||||||
"surface": "#242524",
|
"surface": "#212223",
|
||||||
"surfaceVariant": "#353635",
|
"surfaceVariant": "#323334",
|
||||||
|
|
||||||
"textPrimary": "#F4E9E3",
|
"textPrimary": "#F0F1E0",
|
||||||
"textSecondary": "#DCD2CC",
|
"textSecondary": "#D8D9CA",
|
||||||
"textDisabled": "#928C88",
|
"textDisabled": "#909186",
|
||||||
|
|
||||||
"accentPrimary": "#7D8079",
|
"accentPrimary": "#A3A485",
|
||||||
"accentSecondary": "#979994",
|
"accentSecondary": "#B5B69D",
|
||||||
"accentTertiary": "#646661",
|
"accentTertiary": "#82836A",
|
||||||
|
|
||||||
"error": "#939849",
|
"error": "#A5A9ED",
|
||||||
"warning": "#ABAF72",
|
"warning": "#B9BCF1",
|
||||||
|
|
||||||
"highlight": "#B1B3AF",
|
"highlight": "#C8C8B6",
|
||||||
"rippleEffect": "#8A8D86",
|
"rippleEffect": "#ACAD91",
|
||||||
|
|
||||||
"onAccent": "#111211",
|
"onAccent": "#0E0F10",
|
||||||
"outline": "#585958",
|
"outline": "#565758",
|
||||||
|
|
||||||
"shadow": "#111211",
|
"shadow": "#0E0F10",
|
||||||
"overlay": "#111211"
|
"overlay": "#0E0F10"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue