repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
quickshell/nuguland/NotificationGroup.qml
Rawpragma ComponentBehavior: Bound
import QtQuick
import "Util.js" as Util
Column {
id: root
required property Theme theme
readonly property Flickable scrollView: scroll
required property Notifications notificationService
property var panelState: notificationService ? Util.parseJson(notificationService.stateJson(), {
"groups": [],
"dnd": false,
"activeCount": 0
}) : ({
"groups": [],
"dnd": false,
"activeCount": 0
})
readonly property var groups: panelState.groups || []
// mutators trigger side-effects only; never assign to `state` — that
// would destroy the binding above and freeze the open popup, so
// externally-arriving notifications would stop appearing until reopen.
// stateJson() reads reactive props (notifications/dnd/expandedGroups/…)
// so the binding re-evaluates on its own after every mutation.
function runAction(id, actionId) {
if (notificationService)
notificationService.runAction(id, actionId);
}
function editReply(id, text) {
if (notificationService)
notificationService.setReplyDraft(id, text);
}
function sendReply(id, text) {
if (notificationService)
notificationService.sendReply(id, text);
}
function toggleGroup(appName) {
if (notificationService)
notificationService.toggleGroup(appName);
}
function toggleDnd() {
if (notificationService)
notificationService.toggleDnd();
}
function clearAll() {
if (notificationService)
notificationService.discardAllNotifications();
}
spacing: 8
width: parent ? parent.width : 0
StableListModel {
id: groupRows
rows: root.groups
}
Item {
width: parent.width
height: 28
NuguText {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
theme: root.theme
variant: "title"
text: `notifications${root.panelState.activeCount ? " · " + root.panelState.activeCount : ""}`
}
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 8
PopupButton {
theme: root.theme
height: 24
text: root.panelState.dnd ? "dnd on" : "dnd"
checked: root.panelState.dnd
onClicked: root.toggleDnd()
}
PopupButton {
visible: (root.panelState.activeCount || 0) > 0
theme: root.theme
height: 24
text: "clear all"
role: "destructive"
onClicked: root.clearAll()
}
}
}
// History can outgrow the popup; the header stays fixed and everything
// below scrolls. The Loader hosting this Column forces a concrete
// root.height, so the viewport can bound on it.
Item {
id: scrollArea
width: parent.width
height: Math.max(0, root.height - 28 - root.spacing)
Flickable {
id: scroll
anchors.fill: parent
contentHeight: scrollColumn.height
clip: true
interactive: contentHeight > height
boundsBehavior: Flickable.StopAtBounds
flickableDirection: Flickable.VerticalFlick
Column {
id: scrollColumn
width: scroll.width
spacing: 8
Item {
width: parent.width
height: 220
visible: root.groups.length === 0
Column {
anchors.centerIn: parent
spacing: 8
NuguText {
anchors.horizontalCenter: parent.horizontalCenter
theme: root.theme
variant: "title"
text: "no notifications"
}
NuguText {
anchors.horizontalCenter: parent.horizontalCenter
theme: root.theme
variant: "meta"
text: "you're all caught up"
}
}
}
Repeater {
model: groupRows
delegate: Column {
id: groupDelegate
required property var rowData
width: root.width
spacing: 6
PopupButton {
id: groupHeader
readonly property int hiddenCount: Math.max(1, (groupDelegate.rowData.count || 1) - (groupDelegate.rowData.previewCount || 1))
theme: root.theme
width: parent.width
height: (groupDelegate.rowData.count || 0) > 1 ? 30 : 0
visible: (groupDelegate.rowData.count || 0) > 1
text: groupDelegate.rowData.expanded ? `${groupDelegate.rowData.appName || "app"} · ${groupDelegate.rowData.count} — show less` : `${groupDelegate.rowData.appName || "app"} · ${groupDelegate.rowData.count} — show ${hiddenCount} more`
checked: groupDelegate.rowData.expanded
onClicked: root.toggleGroup(groupDelegate.rowData.appName || "")
}
StableListModel {
id: notificationRows
rows: groupDelegate.rowData.visibleNotifications || []
}
Repeater {
model: notificationRows
delegate: Item {
id: cardWrap
required property var rowData
width: root.width
implicitHeight: cardInner.implicitHeight
height: implicitHeight
NotificationCard {
id: cardInner
width: parent.width
theme: root.theme
modelData: cardWrap.rowData
replyDraft: cardWrap.rowData.replyDraft || ""
replyError: cardWrap.rowData.replyError || ""
onDismiss: id => root.runAction(id, "close")
onInvokeAction: (id, actionId) => root.runAction(id, actionId)
onReplyEdited: (id, text) => root.editReply(id, text)
onSendReply: (id, text) => root.sendReply(id, text)
}
}
}
}
}
}
}
// slim scroll indicator, overlaid (outside the Flickable so it
// doesn't scroll with content); shown only when content overflows.
Rectangle {
anchors.right: parent.right
width: 3
radius: 2
visible: scroll.contentHeight > scroll.height + 1
color: root.theme ? root.theme.withAlpha(root.theme.ui.minor, 0.5) : "transparent"
height: Math.max(24, scrollArea.height * scrollArea.height / Math.max(1, scroll.contentHeight))
y: scroll.contentY * scrollArea.height / Math.max(1, scroll.contentHeight)
}
}
}