Port the AGS widgets into the shell as plugins

This commit is contained in:
2026-09-20 00:09:41 -04:00
parent c5434026a8
commit 0c422e7345
31 changed files with 1740 additions and 17 deletions
@@ -0,0 +1,27 @@
.pragma library
// Each history file is one line of JSON, so the reader concatenates the
// directory and parses line by line. Newest first, by timestamp.
function parseHistory(raw) {
var lines = String(raw || "").split("\n")
var entries = []
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim()
if (!line) continue
try {
var value = JSON.parse(line)
if (!value || typeof value !== "object") continue
entries.push({
id: value.id || 0,
app: value.app || "",
summary: value.summary || "",
body: value.body || "",
timestamp: Number(value.timestamp) || 0
})
} catch (e) {
continue
}
}
entries.sort(function(a, b) { return b.timestamp - a.timestamp })
return entries
}
@@ -0,0 +1,198 @@
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import qs.Commons
import qs.Ui
import "HistoryModel.js" as HistoryModel
Item {
id: root
property var shell: null
property var manifest: null
property bool opened: false
readonly property string pluginId: (manifest && manifest.id) || "blob.notification-center"
readonly property string historyDir: Quickshell.env("HOME") + "/.local/state/blob/notifications/history"
property var entries: []
property bool doNotDisturb: false
readonly property bool empty: root.entries.length === 0
function open(payloadJson) {
root.opened = true
root.refresh()
}
function close() {
root.opened = false
}
function dismiss() {
root.opened = false
if (root.shell && typeof root.shell.hide === "function")
root.shell.hide(root.pluginId)
}
function refresh() {
if (!historyProbe.running) historyProbe.running = true
if (!dndProbe.running) dndProbe.running = true
}
function run(command) {
Quickshell.execDetached(["bash", "-c", command])
}
function clearAll() {
root.run("blob-shell notifications clearHistory")
root.entries = []
}
function toggleSilence() {
root.run("blob-shell notifications toggleDnd")
refreshTimer.restart()
}
function dismissEntry(index) {
var entry = root.entries[index]
if (!entry) return
root.run("blob-notify-dismiss " + entry.id)
var next = []
for (var i = 0; i < root.entries.length; i++)
if (i !== index) next.push(root.entries[i])
root.entries = next
}
Process {
id: historyProbe
command: ["bash", "-c", "cat " + root.historyDir + "/*.json 2>/dev/null || true"]
stdout: StdioCollector {
onStreamFinished: root.entries = HistoryModel.parseHistory(text)
}
}
Process {
id: dndProbe
command: ["bash", "-c", "blob-shell notifications dndState 2>/dev/null"]
stdout: StdioCollector {
onStreamFinished: root.doNotDisturb = String(text).trim() === "on"
}
}
Timer {
id: refreshTimer
interval: 250
onTriggered: root.refresh()
}
Timer {
interval: 5000
running: root.opened
repeat: true
onTriggered: root.refresh()
}
PanelWindow {
visible: root.opened
anchors { top: true; bottom: true; left: true; right: true }
color: "transparent"
WlrLayershell.namespace: "blob-notification-center"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
exclusionMode: ExclusionMode.Ignore
MouseArea {
anchors.fill: parent
onClicked: root.dismiss()
}
Card {
anchors.top: parent.top
anchors.right: parent.right
anchors.topMargin: Style.spaceReal(10)
anchors.rightMargin: Style.spaceReal(10)
implicitWidth: Style.spaceReal(440)
implicitHeight: Style.spaceReal(420)
fillColor: Util.alpha(Color.background, Style.panelFillAlpha)
spacing: Style.spaceReal(10)
Item {
width: parent.width
implicitHeight: titleText.implicitHeight
Text {
id: titleText
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: "Notifications"
textFormat: Text.PlainText
color: Color.foreground
font.family: Style.font.family
font.pixelSize: Style.font.body
font.bold: true
}
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Style.spaceReal(4)
CardIconButton {
icon: root.doNotDisturb ? "" : ""
active: root.doNotDisturb
onActivated: root.toggleSilence()
}
CardIconButton {
icon: ""
onActivated: root.clearAll()
}
}
}
ListView {
width: parent.width
height: Style.spaceReal(340)
clip: true
spacing: Style.spaceReal(8)
visible: !root.empty
model: root.entries
delegate: NotificationItem {
required property var modelData
required property int index
width: ListView.view.width
app: modelData.app
summary: modelData.summary
body: modelData.body
onDismissRequested: root.dismissEntry(index)
}
}
Column {
width: parent.width
visible: root.empty
spacing: Style.spaceReal(8)
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: ""
textFormat: Text.PlainText
color: Util.alpha(Color.blue, 0.6)
font.family: Style.font.family
font.pixelSize: Style.font.heading
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "No notifications"
textFormat: Text.PlainText
color: Util.alpha(Color.foreground, 0.6)
font.family: Style.font.family
font.pixelSize: Style.font.bodySmall
}
}
}
}
}
@@ -0,0 +1,91 @@
import QtQuick
import qs.Commons
Rectangle {
id: root
property string app: ""
property string summary: ""
property string body: ""
signal dismissRequested()
radius: Style.cardRadius
color: Util.alpha(Color.background, Style.cardFillAlpha)
implicitHeight: layout.implicitHeight + Style.spaceReal(20)
Rectangle {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: Style.spaceReal(3)
color: Color.accent
}
Column {
id: layout
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.leftMargin: Style.spaceReal(12)
anchors.rightMargin: Style.spaceReal(12)
anchors.topMargin: Style.spaceReal(10)
spacing: Style.spaceReal(2)
Item {
width: parent.width
implicitHeight: appText.implicitHeight
Text {
id: appText
anchors.left: parent.left
text: root.app.length > 0 ? root.app : "Notification"
textFormat: Text.PlainText
color: Color.accent
font.family: Style.font.family
font.pixelSize: Style.font.bodySmall
font.bold: true
}
Text {
anchors.right: parent.right
text: ""
textFormat: Text.PlainText
color: closeHover.hovered ? Color.urgent : Color.magenta
font.family: Style.font.family
font.pixelSize: Style.font.bodySmall
HoverHandler {
id: closeHover
}
TapHandler {
onTapped: root.dismissRequested()
}
}
}
Text {
width: parent.width
text: root.summary
textFormat: Text.PlainText
wrapMode: Text.WordWrap
color: Color.foreground
font.family: Style.font.family
font.pixelSize: Style.font.bodySmall
font.bold: true
}
Text {
width: parent.width
visible: root.body.length > 0
text: root.body
textFormat: Text.PlainText
wrapMode: Text.WordWrap
maximumLineCount: 4
elide: Text.ElideRight
color: Util.alpha(Color.foreground, 0.8)
font.family: Style.font.family
font.pixelSize: Style.font.bodySmall
}
}
}
@@ -0,0 +1,15 @@
{
"schemaVersion": 1,
"name": "Notification centre",
"version": "1.0.0",
"author": "Blob",
"description": "List of recent notifications with clear-all and silence controls",
"id": "blob.notification-center",
"kinds": [
"overlay"
],
"keepLoaded": true,
"entryPoints": {
"overlay": "NotificationCenter.qml"
}
}