NotificationHistory Updates

Add read/unread bell icon
Edit style of the Panel
Small fixes
This commit is contained in:
ly-sec 2025-07-17 16:56:51 +02:00
parent cf26fe52d9
commit dbb5a9160c
6 changed files with 416 additions and 409 deletions

View file

@ -9,11 +9,11 @@ import qs.Settings
import qs.Services
import qs.Components
import qs.Widgets
import qs.Widgets.Notification
import qs.Widgets.Sidebar
import qs.Widgets.Sidebar.Panel
import qs.Helpers
import QtQuick.Controls
import qs.Widgets.Notification
Scope {
id: rootScope
@ -83,40 +83,9 @@ Scope {
anchors.rightMargin: 18
spacing: 12
Item {
id: notificationBellButton
width: 22
height: 22
anchors.verticalCenter: parent.verticalCenter
z: 1
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
}
}
NotificationHistory {
id: notificationHistoryWin
anchors.verticalCenter: parent.verticalCenter
}
Brightness {
@ -158,28 +127,25 @@ Scope {
}
}
Background {}
Overview {}
}
PanelWindow {
id: topLeftPanel
id: topCornerPanel
anchors.top: true
anchors.left: true
anchors.right: true
color: "transparent"
screen: modelData
margins.top: 36
WlrLayershell.exclusionMode: ExclusionMode.Ignore
visible: true
WlrLayershell.layer: WlrLayer.Background
aboveWindows: false
WlrLayershell.namespace: "swww-daemon"
implicitHeight: 24
Corners {
id: topLeftCorner
id: topleftCorner
position: "bottomleft"
size: 1.3
fillColor: (Theme.backgroundPrimary !== undefined && Theme.backgroundPrimary !== null) ? Theme.backgroundPrimary : "#222"
@ -187,25 +153,9 @@ Scope {
offsetY: 0
anchors.top: parent.top
}
}
PanelWindow {
id: topRightPanel
anchors.top: true
anchors.right: true
color: "transparent"
screen: modelData
margins.top: 36
WlrLayershell.exclusionMode: ExclusionMode.Ignore
visible: true
WlrLayershell.layer: WlrLayer.Background
aboveWindows: false
WlrLayershell.namespace: "swww-daemon"
implicitHeight: 24
Corners {
id: topRightCorner
id: toprightCorner
position: "bottomright"
size: 1.3
fillColor: (Theme.backgroundPrimary !== undefined && Theme.backgroundPrimary !== null) ? Theme.backgroundPrimary : "#222"
@ -223,9 +173,6 @@ Scope {
screen: modelData
WlrLayershell.exclusionMode: ExclusionMode.Ignore
visible: true
WlrLayershell.layer: WlrLayer.Background
aboveWindows: false
WlrLayershell.namespace: "swww-daemon"
implicitHeight: 24
@ -241,16 +188,13 @@ Scope {
}
PanelWindow {
id: bottomRightPanel
id: bottomRightCornerPanel
anchors.bottom: true
anchors.right: true
color: "transparent"
screen: modelData
WlrLayershell.exclusionMode: ExclusionMode.Ignore
visible: true
WlrLayershell.layer: WlrLayer.Background
aboveWindows: false
WlrLayershell.namespace: "swww-daemon"
implicitHeight: 24
@ -264,6 +208,10 @@ Scope {
anchors.top: parent.top
}
}
Loader {
id: tabViewerLoader
}
}
}

View file

@ -3,6 +3,7 @@ import Quickshell.Io
IpcHandler {
property var appLauncherPanel
property var lockScreen
property var tabViewer
target: "globalIPC"

View file

@ -89,7 +89,7 @@ Singleton {
id: i,
idx: ws.id,
name: ws.name || "",
output: ws.monitor?.name || "",
output: ws.monitor.name || "",
isActive: ws.active === true,
isFocused: ws.focused === true,
isUrgent: ws.urgent === true

View file

@ -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"
}

View file

@ -1,15 +1,48 @@
import QtQuick
import QtQuick.Controls
import QtQuick 2.15
import QtQuick.Controls 2.15
import Quickshell
import Quickshell.Io
import qs.Components
import qs.Settings
import QtQuick.Layouts
import QtQuick.Layouts 1.15
Item {
id: root
property string configDir: Quickshell.configDir
property string historyFilePath: configDir + "/notification_history.json"
property bool hasUnread: notificationHistoryWin.hasUnread && !notificationHistoryWin.visible
function addToHistory(notification) { notificationHistoryWin.addToHistory(notification) }
width: 22; height: 22
// Bell icon/button
Item {
id: bell
width: 22; height: 22
Text {
anchors.centerIn: parent
text: root.hasUnread ? "notifications_unread" : "notifications"
font.family: mouseAreaBell.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined"
font.pixelSize: 16
font.weight: root.hasUnread ? Font.Bold : Font.Normal
color: mouseAreaBell.containsMouse ? Theme.accentPrimary : (root.hasUnread ? Theme.accentPrimary : Theme.textDisabled)
}
MouseArea {
id: mouseAreaBell
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: notificationHistoryWin.visible = !notificationHistoryWin.visible
}
}
// The popup window
PanelWindow {
id: notificationHistoryWin
width: 400
height: 500
property int maxPopupHeight: 500
property int minPopupHeight: 230
property int contentHeight: headerRow.height + historyList.contentHeight + 56
height: Math.max(Math.min(contentHeight, maxPopupHeight), minPopupHeight)
color: "transparent"
visible: false
screen: Quickshell.primaryScreen
@ -20,48 +53,47 @@ PanelWindow {
margins.right: 4
property int maxHistory: 100
property string configDir: Quickshell.configDir
property string historyFilePath: configDir + "/notification_history.json"
property bool hasUnread: false
signal unreadChanged(bool hasUnread)
ListModel {
id: historyModel // Holds notification objects
id: historyModel
}
FileView {
id: historyFileView
path: historyFilePath
path: root.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
property var notifications: []
}
onFileChanged: historyFileView.reload()
onLoaded: notificationHistoryWin.loadHistory()
onLoadFailed: function(error) {
console.error("Failed to load history file:", error)
if (error.includes("No such file")) {
historyAdapter.notifications = [] // Create new file if missing
historyAdapter.notifications = []
writeAdapter()
}
}
onSaved: {}
onSaveFailed: function(error) {
console.error("Failed to save history:", error)
Component.onCompleted: if (path) reload()
}
Component.onCompleted: {
if (path) reload()
function updateHasUnread() {
var unread = false;
for (let i = 0; i < historyModel.count; ++i) {
if (historyModel.get(i).read === false) {
unread = true;
break;
}
}
if (hasUnread !== unread) {
hasUnread = unread;
unreadChanged(hasUnread);
}
}
@ -71,10 +103,15 @@ PanelWindow {
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])
let n = notifications[i]
if (typeof n === 'object' && n !== null) {
if (n.read === undefined) n.read = false;
// Mark as read if window is open
if (visible) n.read = true;
historyModel.append(n)
}
}
updateHasUnread();
}
}
@ -89,7 +126,8 @@ PanelWindow {
appName: obj.appName,
summary: obj.summary,
body: obj.body,
timestamp: obj.timestamp
timestamp: obj.timestamp,
read: obj.read === undefined ? false : obj.read
})
}
}
@ -97,18 +135,26 @@ PanelWindow {
Qt.callLater(function() {
historyFileView.writeAdapter()
})
updateHasUnread();
}
function addToHistory(notification) {
if (!notification.id) notification.id = Date.now()
if (!notification.timestamp) notification.timestamp = new Date().toISOString()
// Mark as read if window is open
notification.read = visible
// Remove duplicate by id
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()
}
@ -130,6 +176,20 @@ PanelWindow {
return `${y}-${m}-${d} ${h}:${min}`;
}
onVisibleChanged: {
if (visible) {
// Mark all as read when popup is opened
let changed = false;
for (let i = 0; i < historyModel.count; ++i) {
if (historyModel.get(i).read === false) {
historyModel.setProperty(i, 'read', true);
changed = true;
}
}
if (changed) saveHistory();
}
}
Rectangle {
width: notificationHistoryWin.width
height: notificationHistoryWin.height
@ -143,9 +203,10 @@ PanelWindow {
spacing: 8
RowLayout {
id: headerRow
spacing: 4
anchors.top: parent.top
anchors.topMargin: 16
anchors.topMargin: 4
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 16
@ -162,7 +223,7 @@ PanelWindow {
id: clearAllButton
width: 90
height: 32
radius: 20
radius: 16
color: clearAllMouseArea.containsMouse ? Theme.accentPrimary : Theme.surfaceVariant
border.color: Theme.accentPrimary
border.width: 1
@ -213,10 +274,8 @@ PanelWindow {
Rectangle {
anchors.fill: parent
color: Theme.backgroundPrimary
color: Theme.surface
radius: 20
border.width: 1
border.color: Theme.surfaceVariant
z: 0
}
Rectangle {
@ -226,12 +285,19 @@ PanelWindow {
anchors.bottomMargin: 12
color: "transparent"
clip: true
Column {
anchors.fill: parent
spacing: 0
Item { id: topSpacer; height: (parent.height - historyList.height) / 2 }
ListView {
id: historyList
anchors.fill: parent
width: parent.width
height: Math.min(contentHeight, parent.height)
spacing: 12
model: historyModel
model: historyModel.count > 0 ? historyModel : placeholderModel
clip: true
delegate: Item {
width: parent.width
height: notificationCard.implicitHeight + 12
Rectangle {
id: notificationCard
@ -239,8 +305,6 @@ PanelWindow {
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
@ -253,9 +317,6 @@ PanelWindow {
RowLayout {
id: headerRow
spacing: 8
anchors.left: parent.left
anchors.right: parent.right
anchors.rightMargin: 0
Rectangle {
id: iconBackground
width: 28
@ -279,7 +340,7 @@ PanelWindow {
spacing: 0
Layout.alignment: Qt.AlignVCenter
Text {
text: model.appName || "Unknown App"
text: model.appName || "No Notifications"
font.bold: true
color: Theme.textPrimary
font.family: Theme.fontFamily
@ -287,49 +348,18 @@ PanelWindow {
verticalAlignment: Text.AlignVCenter
}
Text {
text: formatTimestamp(model.timestamp)
color: Theme.textSecondary
visible: !model.isPlaceholder
text: model.timestamp ? notificationHistoryWin.formatTimestamp(model.timestamp) : ""
color: Theme.textDisabled
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeCaption
verticalAlignment: Text.AlignVCenter
}
}
Item { Layout.fillWidth: true }
Rectangle {
id: deleteButton
width: 24
height: 24
radius: 12
color: deleteMouseArea.containsMouse ? Theme.accentPrimary : Theme.surfaceVariant
border.color: Theme.accentPrimary
border.width: 1
Layout.alignment: Qt.AlignVCenter
z: 2
Row {
anchors.centerIn: parent
spacing: 0
Text {
text: "close"
font.family: "Material Symbols Outlined"
font.pixelSize: 16
color: deleteMouseArea.containsMouse ? Theme.onAccent : Theme.error
verticalAlignment: Text.AlignVCenter
}
}
MouseArea {
id: deleteMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
historyModel.remove(index)
saveHistory()
}
}
}
}
Text {
text: model.summary || ""
text: model.summary || (model.isPlaceholder ? "You're all caught up!" : "")
color: Theme.textSecondary
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeBody
@ -337,7 +367,7 @@ PanelWindow {
wrapMode: Text.Wrap
}
Text {
text: model.body || ""
text: model.body || (model.isPlaceholder ? "No notifications to show." : "")
color: Theme.textDisabled
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeBody
@ -350,8 +380,20 @@ PanelWindow {
}
}
}
}
Rectangle { width: 1; height: 24; color: "transparent" }
ListModel {
id: placeholderModel
ListElement {
appName: ""
summary: ""
body: ""
isPlaceholder: true
}
}
}
}
}
}

View file

@ -15,6 +15,7 @@ Scope {
id: root
property alias appLauncherPanel: appLauncherPanel
property var notificationHistoryWin: notificationHistoryWin
function updateVolume(vol) {
volume = vol;
@ -30,6 +31,7 @@ Scope {
Bar {
id: bar
shell: root
property var notificationHistoryWin: notificationHistoryWin
}
Applauncher {
@ -47,6 +49,15 @@ Scope {
console.log("Notification received:", notification.appName);
notification.tracked = true;
notificationPopup.addNotification(notification);
if (notificationHistoryWin) {
notificationHistoryWin.addToHistory({
id: notification.id,
appName: notification.appName || "Notification",
summary: notification.summary || "",
body: notification.body || "",
timestamp: Date.now()
});
}
}
}
@ -55,6 +66,11 @@ Scope {
barVisible: bar.visible
}
// Notification History Window
NotificationHistory {
id: notificationHistoryWin
}
property var defaultAudioSink: Pipewire.defaultAudioSink
property int volume: defaultAudioSink && defaultAudioSink.audio && defaultAudioSink.audio.volume ? Math.round(defaultAudioSink.audio.volume * 100) : 0