Extract notification center bar widget
This commit is contained in:
@@ -19,7 +19,7 @@ User-installed plugins live alongside these conceptually but on disk under
|
|||||||
| Clipboard mgr | `omarchy.clipboard` | `overlay` | `clipboard/Clipboard.qml` |
|
| Clipboard mgr | `omarchy.clipboard` | `overlay` | `clipboard/Clipboard.qml` |
|
||||||
| Reminders | `omarchy.reminders` | `overlay` | `reminders/ReminderFlow.qml` |
|
| Reminders | `omarchy.reminders` | `overlay` | `reminders/ReminderFlow.qml` |
|
||||||
| Omarchy menu | `omarchy.menu` | `menu`, `bar-widget` | `menu/Menu.qml`, `menu/BarWidget.qml` |
|
| Omarchy menu | `omarchy.menu` | `menu`, `bar-widget` | `menu/Menu.qml`, `menu/BarWidget.qml` |
|
||||||
| Notifications | `omarchy.notifications` | `service`, `bar-widget` | `notifications/Service.qml`, `notifications/BarWidget.qml` |
|
| Notifications | `omarchy.notifications` | `service` | `notifications/Service.qml` |
|
||||||
| Audio | `omarchy.audio` | `bar-widget` | `panels/audio/Panel.qml` |
|
| Audio | `omarchy.audio` | `bar-widget` | `panels/audio/Panel.qml` |
|
||||||
| Bluetooth | `omarchy.bluetooth` | `bar-widget` | `panels/bluetooth/Panel.qml` |
|
| Bluetooth | `omarchy.bluetooth` | `bar-widget` | `panels/bluetooth/Panel.qml` |
|
||||||
| Monitor | `omarchy.monitor` | `bar-widget` | `panels/monitor/Panel.qml` |
|
| Monitor | `omarchy.monitor` | `bar-widget` | `panels/monitor/Panel.qml` |
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ Example `shell.json` (bar subtree only shown):
|
|||||||
| `omarchy.clock` | Date/time label | left = alternate format · right = timezone selector |
|
| `omarchy.clock` | Date/time label | left = alternate format · right = timezone selector |
|
||||||
| `omarchy.media` | MPRIS now-playing — scrolling track + artist, cover-art popup | left = play/pause · middle = next · scroll = prev/next · right = popup |
|
| `omarchy.media` | MPRIS now-playing — scrolling track + artist, cover-art popup | left = play/pause · middle = next · scroll = prev/next · right = popup |
|
||||||
| `omarchy.indicators` | Manual state indicators | left = indicator action |
|
| `omarchy.indicators` | Manual state indicators | left = indicator action |
|
||||||
| `omarchy.notifications` | Bell with badge + popup with recent notifications, DND toggle | left = popup · right = toggle DND |
|
|
||||||
| `omarchy.system-update` | Available update indicator | left = update |
|
| `omarchy.system-update` | Available update indicator | left = update |
|
||||||
| `omarchy.tray` | System tray | hover = reveal drawer · right on chevron = manage |
|
| `omarchy.tray` | System tray | hover = reveal drawer · right on chevron = manage |
|
||||||
| `omarchy.weather` | Weather icon + popup with forecast | left = popup · right = full notification |
|
| `omarchy.weather` | Weather icon + popup with forecast | left = popup · right = full notification |
|
||||||
@@ -169,8 +168,8 @@ Widgets receive `bar` (the shell root), `moduleName` (string), and `settings` (o
|
|||||||
First-party bar widgets are manifest-backed just like third-party widgets.
|
First-party bar widgets are manifest-backed just like third-party widgets.
|
||||||
Simple widgets carry sibling manifests such as `widgets/Clock.manifest.json`;
|
Simple widgets carry sibling manifests such as `widgets/Clock.manifest.json`;
|
||||||
richer popup plugins live in feature directories such as `../panels/audio/`,
|
richer popup plugins live in feature directories such as `../panels/audio/`,
|
||||||
`../panels/network/`, and `../model-usage/`; and feature plugins such as `omarchy.menu`, `omarchy.media`, and
|
`../panels/network/`, and `../model-usage/`; and feature plugins such as
|
||||||
`omarchy.notifications` declare their bar-widget entry points in their own
|
`omarchy.menu` and `omarchy.media` declare their bar-widget entry points in their own
|
||||||
`manifest.json`. Bar layout ids are namespaced, e.g. `omarchy.audio`,
|
`manifest.json`. Bar layout ids are namespaced, e.g. `omarchy.audio`,
|
||||||
`omarchy.network`, and `omarchy.clock`. Older UpperCamelCase ids such as
|
`omarchy.network`, and `omarchy.clock`. Older UpperCamelCase ids such as
|
||||||
`AudioPanel` and `Clock` are migrated forward; new configs should use the
|
`AudioPanel` and `Clock` are migrated forward; new configs should use the
|
||||||
|
|||||||
@@ -1,412 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import Quickshell
|
|
||||||
import qs.Commons
|
|
||||||
import qs.Ui
|
|
||||||
import "NotificationLogic.js" as NotificationLogic
|
|
||||||
|
|
||||||
BarWidget {
|
|
||||||
id: root
|
|
||||||
moduleName: "omarchy.notifications"
|
|
||||||
|
|
||||||
|
|
||||||
property bool popupOpen: false
|
|
||||||
function close() { popupOpen = false }
|
|
||||||
|
|
||||||
// Always default to the pending tab when there's anything unseen, no
|
|
||||||
// matter how the popup was opened (click, keybind/IPC, or the close
|
|
||||||
// path). Keeps the spec from drifting based on the user's last manual
|
|
||||||
// tab selection.
|
|
||||||
onPopupOpenChanged: {
|
|
||||||
if (popupOpen) {
|
|
||||||
activeTab = pendingCount > 0 ? "pending" : "past"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look up the long-running notifications service through the shell host.
|
|
||||||
readonly property var hostShell: bar && bar.shell ? bar.shell : null
|
|
||||||
readonly property var notificationService: hostShell?.firstPartyServiceFor("omarchy.notifications")
|
|
||||||
|
|
||||||
function isChromiumDerived(app, appIcon) {
|
|
||||||
return NotificationLogic.isChromiumDerived(app, appIcon)
|
|
||||||
}
|
|
||||||
|
|
||||||
function sanitizeBody(s, app, appIcon) {
|
|
||||||
return NotificationLogic.sanitizeBody(s, app, appIcon)
|
|
||||||
}
|
|
||||||
|
|
||||||
function notificationIconSource(icon) {
|
|
||||||
var value = String(icon || "")
|
|
||||||
if (value.length === 0) return ""
|
|
||||||
if (value.indexOf("file://") === 0 || value.indexOf("image://") === 0) return value
|
|
||||||
if (value.charAt(0) === "/") return Util.fileUrl(value)
|
|
||||||
return Quickshell.iconPath(value, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property int pendingCount: notificationService ? notificationService.pendingModel.count : 0
|
|
||||||
readonly property int pastCount: notificationService ? notificationService.pastModel.count : 0
|
|
||||||
readonly property bool dnd: notificationService ? notificationService.doNotDisturb : false
|
|
||||||
|
|
||||||
// Which tab is active in the popup. Auto-selects pending when there's
|
|
||||||
// something unseen; otherwise opens past.
|
|
||||||
property string activeTab: "pending"
|
|
||||||
|
|
||||||
readonly property string icon: {
|
|
||||||
if (dnd) return ""
|
|
||||||
if (pendingCount > 0) return ""
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// Theme palette (mirrors HistoryPanel's tokens so the popup matches the
|
|
||||||
// rest of the notification stack).
|
|
||||||
readonly property color colForeground: Color.foreground
|
|
||||||
readonly property color colDim: Qt.darker(Color.foreground, 1.4)
|
|
||||||
readonly property color colBorder: Style.normalBorderFor(Color.foreground, Color.accent)
|
|
||||||
readonly property color colSurface: Style.normalFillFor(Color.foreground, Color.accent)
|
|
||||||
readonly property color colAccent: Color.accent
|
|
||||||
readonly property int cardRadius: notificationService ? notificationService.cornerRadius : 0
|
|
||||||
|
|
||||||
implicitWidth: button.implicitWidth
|
|
||||||
implicitHeight: button.implicitHeight
|
|
||||||
|
|
||||||
WidgetButton {
|
|
||||||
id: button
|
|
||||||
anchors.fill: parent
|
|
||||||
bar: root.bar
|
|
||||||
text: root.icon
|
|
||||||
active: root.pendingCount > 0 && !root.dnd
|
|
||||||
tooltipText: root.dnd ? "Do Not Disturb"
|
|
||||||
: (root.pendingCount > 0 ? root.pendingCount + " pending" : "No notifications")
|
|
||||||
|
|
||||||
onPressed: function(b) {
|
|
||||||
if (b === Qt.RightButton) {
|
|
||||||
if (root.notificationService) {
|
|
||||||
root.notificationService.setDoNotDisturb(!root.notificationService.doNotDisturb)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
root.popupOpen = !root.popupOpen
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PopupCard {
|
|
||||||
id: popup
|
|
||||||
anchorItem: button
|
|
||||||
bar: root.bar
|
|
||||||
owner: root
|
|
||||||
open: root.popupOpen
|
|
||||||
contentWidth: popup.fittedContentWidth(Style.space(440))
|
|
||||||
contentHeight: popup.cappedContentHeight(Style.space(540))
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
spacing: Style.space(10)
|
|
||||||
|
|
||||||
// ----------------------------------------- header
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: Style.space(8)
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "Notifications"
|
|
||||||
font.family: root.bar ? root.bar.fontFamily : ""
|
|
||||||
color: root.colForeground
|
|
||||||
font.pixelSize: Style.font.title
|
|
||||||
font.bold: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Item { Layout.fillWidth: true }
|
|
||||||
|
|
||||||
BorderSurface {
|
|
||||||
id: dndPill
|
|
||||||
Layout.preferredHeight: Math.max(Style.space(24), Style.font.bodySmall + Style.spacing.controlPaddingY * 2)
|
|
||||||
Layout.preferredWidth: dndLabel.implicitWidth + dndGlyph.implicitWidth + Style.space(18)
|
|
||||||
radius: Math.min(Style.space(12), root.cardRadius + Style.space(6))
|
|
||||||
color: dndOn ? root.colAccent : root.colSurface
|
|
||||||
borderSpec: Border.flat(dndOn ? root.colAccent : root.colBorder, Style.normalBorderWidth)
|
|
||||||
|
|
||||||
readonly property bool dndOn: !!root.notificationService && root.notificationService.doNotDisturb
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Style.space(4)
|
|
||||||
|
|
||||||
Text {
|
|
||||||
id: dndGlyph
|
|
||||||
text: dndPill.dndOn ? "" : ""
|
|
||||||
font.family: root.bar ? root.bar.fontFamily : ""
|
|
||||||
color: dndPill.dndOn ? Color.background : root.colDim
|
|
||||||
font.pixelSize: Style.font.body
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
id: dndLabel
|
|
||||||
text: dndPill.dndOn ? "DND on" : "DND off"
|
|
||||||
font.family: root.bar ? root.bar.fontFamily : ""
|
|
||||||
color: dndPill.dndOn ? Color.background : root.colDim
|
|
||||||
font.pixelSize: Style.font.caption
|
|
||||||
font.bold: true
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: if (root.notificationService) root.notificationService.setDoNotDisturb(!dndPill.dndOn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------- tabs
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: [
|
|
||||||
{ key: "pending", label: "Pending", count: root.pendingCount },
|
|
||||||
{ key: "past", label: "Recently", count: root.pastCount }
|
|
||||||
]
|
|
||||||
delegate: Rectangle {
|
|
||||||
required property var modelData
|
|
||||||
readonly property bool isActive: root.activeTab === modelData.key
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: Math.max(Style.space(30), Style.font.body + Style.spacing.controlPaddingY * 2)
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: modelData.label + (modelData.count > 0 ? " " + modelData.count : "")
|
|
||||||
font.family: root.bar ? root.bar.fontFamily : ""
|
|
||||||
color: parent.isActive ? root.colForeground : root.colDim
|
|
||||||
font.pixelSize: Style.font.body
|
|
||||||
font.bold: parent.isActive
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
height: Math.max(1, Style.space(2))
|
|
||||||
color: parent.isActive ? root.colAccent : root.colBorder
|
|
||||||
opacity: parent.isActive ? 1 : 0.4
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: root.activeTab = modelData.key
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------- action row
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
visible: (root.activeTab === "pending" && root.pendingCount > 0)
|
|
||||||
|| (root.activeTab === "past" && root.pastCount > 0)
|
|
||||||
spacing: Style.space(8)
|
|
||||||
|
|
||||||
Item { Layout.fillWidth: true }
|
|
||||||
|
|
||||||
BorderSurface {
|
|
||||||
Layout.preferredWidth: actionLabel.implicitWidth + Style.space(16)
|
|
||||||
Layout.preferredHeight: Math.max(Style.space(22), Style.font.bodySmall + Style.spacing.controlPaddingY * 2)
|
|
||||||
radius: Math.min(Style.space(6), root.cardRadius)
|
|
||||||
color: actionArea.containsMouse ? root.colBorder : "transparent"
|
|
||||||
borderSpec: Border.flat(root.colBorder, Style.normalBorderWidth)
|
|
||||||
|
|
||||||
Text {
|
|
||||||
id: actionLabel
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: root.activeTab === "pending" ? "Mark all as seen" : "Clear recent"
|
|
||||||
font.family: root.bar ? root.bar.fontFamily : ""
|
|
||||||
color: root.colForeground
|
|
||||||
font.pixelSize: Style.font.caption
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: actionArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (!root.notificationService) return
|
|
||||||
if (root.activeTab === "pending") root.notificationService.markAllSeen()
|
|
||||||
else root.notificationService.clearPast()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------- list
|
|
||||||
ListView {
|
|
||||||
id: listView
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
clip: true
|
|
||||||
spacing: Style.space(8)
|
|
||||||
|
|
||||||
readonly property bool onPending: root.activeTab === "pending"
|
|
||||||
model: !root.notificationService ? null
|
|
||||||
: (onPending ? root.notificationService.pendingModel : root.notificationService.pastModel)
|
|
||||||
visible: count > 0
|
|
||||||
|
|
||||||
delegate: BorderSurface {
|
|
||||||
id: rowCard
|
|
||||||
required property int index
|
|
||||||
required property string app
|
|
||||||
required property string appIcon
|
|
||||||
required property string summary
|
|
||||||
required property string body
|
|
||||||
required property string image
|
|
||||||
required property int urgency
|
|
||||||
required property double timestamp
|
|
||||||
|
|
||||||
readonly property bool hasMedia: image.length > 0 && (
|
|
||||||
image.indexOf("image://icon//") === 0 || image.indexOf("file://") === 0)
|
|
||||||
readonly property string smallIconSource: image.length > 0 ? image : root.notificationIconSource(appIcon)
|
|
||||||
readonly property bool hasIcon: !hasMedia && smallIconSource.length > 0
|
|
||||||
readonly property string sanitizedBody: root.sanitizeBody(body, app, appIcon)
|
|
||||||
|
|
||||||
width: listView.width
|
|
||||||
implicitHeight: rowContent.implicitHeight + Style.spacing.panelGap
|
|
||||||
radius: root.cardRadius
|
|
||||||
color: "transparent"
|
|
||||||
borderSpec: Border.flat(root.colBorder, Style.normalBorderWidth)
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: { /* no-op */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
id: rowContent
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.leftMargin: rowCard.borderLeft + Style.space(12)
|
|
||||||
anchors.rightMargin: rowCard.borderRight + Style.space(12)
|
|
||||||
spacing: Style.space(10)
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: imageSlot
|
|
||||||
Layout.preferredWidth: Style.space(32)
|
|
||||||
Layout.preferredHeight: Style.space(32)
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
// Hide on icon load failure so unresolved themed-icon names
|
|
||||||
// don't render Qt's broken-image placeholder.
|
|
||||||
visible: (rowCard.hasIcon || rowCard.hasMedia) && rowIconImage.status !== Image.Error
|
|
||||||
|
|
||||||
Image {
|
|
||||||
id: rowIconImage
|
|
||||||
anchors.fill: parent
|
|
||||||
source: rowCard.hasMedia ? rowCard.image : rowCard.smallIconSource
|
|
||||||
fillMode: rowCard.hasMedia ? Image.PreserveAspectCrop : Image.PreserveAspectFit
|
|
||||||
sourceSize.width: imageSlot.width * Screen.devicePixelRatio
|
|
||||||
sourceSize.height: imageSlot.height * Screen.devicePixelRatio
|
|
||||||
asynchronous: true
|
|
||||||
smooth: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: Style.space(2)
|
|
||||||
|
|
||||||
Text {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
visible: rowCard.summary.length > 0
|
|
||||||
text: rowCard.summary
|
|
||||||
font.family: root.bar ? root.bar.fontFamily : ""
|
|
||||||
color: root.colForeground
|
|
||||||
font.pixelSize: Style.font.subtitle
|
|
||||||
font.bold: true
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
elide: Text.ElideRight
|
|
||||||
maximumLineCount: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
visible: rowCard.sanitizedBody.length > 0
|
|
||||||
text: rowCard.sanitizedBody
|
|
||||||
font.family: root.bar ? root.bar.fontFamily : ""
|
|
||||||
textFormat: Text.PlainText
|
|
||||||
color: root.colDim
|
|
||||||
font.pixelSize: Style.font.bodySmall
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
elide: Text.ElideRight
|
|
||||||
maximumLineCount: 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.preferredWidth: Style.space(18)
|
|
||||||
Layout.preferredHeight: Style.space(18)
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
radius: Math.min(4, root.cardRadius)
|
|
||||||
color: rowCloseArea.containsMouse ? root.colBorder : "transparent"
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: "✕"
|
|
||||||
font.family: root.bar ? root.bar.fontFamily : ""
|
|
||||||
color: root.colDim
|
|
||||||
font.pixelSize: Style.font.bodySmall
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: rowCloseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (!root.notificationService) return
|
|
||||||
if (listView.onPending) root.notificationService.dismissPending(rowCard.index)
|
|
||||||
else root.notificationService.dismissPast(rowCard.index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------- empty state
|
|
||||||
Item {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
visible: listView.count === 0
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Style.space(6)
|
|
||||||
|
|
||||||
Text {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
text: ""
|
|
||||||
font.family: root.bar ? root.bar.fontFamily : ""
|
|
||||||
color: root.colBorder
|
|
||||||
font.pixelSize: Style.font.displayLarge
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
text: root.activeTab === "pending"
|
|
||||||
? "Nothing waiting for you"
|
|
||||||
: "Nothing recent"
|
|
||||||
font.family: root.bar ? root.bar.fontFamily : ""
|
|
||||||
color: root.colDim
|
|
||||||
font.pixelSize: Style.font.body
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,20 +4,12 @@
|
|||||||
"name": "Notifications",
|
"name": "Notifications",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"author": "Omarchy",
|
"author": "Omarchy",
|
||||||
"description": "Notification daemon, popups, and history",
|
"description": "Notification daemon, popups, DND, and history",
|
||||||
"kinds": [
|
"kinds": [
|
||||||
"service",
|
"service"
|
||||||
"bar-widget"
|
|
||||||
],
|
],
|
||||||
"keepLoaded": true,
|
"keepLoaded": true,
|
||||||
"entryPoints": {
|
"entryPoints": {
|
||||||
"service": "Service.qml",
|
"service": "Service.qml"
|
||||||
"barWidget": "BarWidget.qml"
|
|
||||||
},
|
|
||||||
"barWidget": {
|
|
||||||
"displayName": "Notification center",
|
|
||||||
"description": "Recent notifications + DND",
|
|
||||||
"category": "Status",
|
|
||||||
"allowMultiple": false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,24 +110,13 @@ ShellRoot {
|
|||||||
|
|
||||||
Item { id: host }
|
Item { id: host }
|
||||||
|
|
||||||
QtObject {
|
|
||||||
id: mockNotificationService
|
|
||||||
property bool doNotDisturb: false
|
|
||||||
property ListModel pendingModel: ListModel {}
|
|
||||||
property ListModel pastModel: ListModel {}
|
|
||||||
function setDoNotDisturb(value) { doNotDisturb = !!value }
|
|
||||||
}
|
|
||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
id: mockShell
|
id: mockShell
|
||||||
property var bar: fakeBar
|
property var bar: fakeBar
|
||||||
property var barConfig: ({ position: "top" })
|
property var barConfig: ({ position: "top" })
|
||||||
property var shellConfig: ({ version: 1, idle: {}, plugins: [], bar: { layout: { left: [], center: [], right: [] } } })
|
property var shellConfig: ({ version: 1, idle: {}, plugins: [], bar: { layout: { left: [], center: [], right: [] } } })
|
||||||
function firstPartyServiceFor(id) {
|
function firstPartyServiceFor(id) { return null }
|
||||||
if (id === "omarchy.notifications") return mockNotificationService
|
function serviceFor(id) { return null }
|
||||||
return null
|
|
||||||
}
|
|
||||||
function serviceFor(id) { return firstPartyServiceFor(id) }
|
|
||||||
function summon(id, payloadJson) { return true }
|
function summon(id, payloadJson) { return true }
|
||||||
function hide(id) { return true }
|
function hide(id) { return true }
|
||||||
function toggle(id, payloadJson) { return true }
|
function toggle(id, payloadJson) { return true }
|
||||||
|
|||||||
@@ -137,24 +137,13 @@ ShellRoot {
|
|||||||
function rescan() {}
|
function rescan() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
QtObject {
|
|
||||||
id: mockNotificationService
|
|
||||||
property bool doNotDisturb: false
|
|
||||||
property ListModel pendingModel: ListModel {}
|
|
||||||
property ListModel pastModel: ListModel {}
|
|
||||||
function setDoNotDisturb(value) { doNotDisturb = !!value }
|
|
||||||
}
|
|
||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
id: mockShell
|
id: mockShell
|
||||||
property var bar: fakeBar
|
property var bar: fakeBar
|
||||||
property var barConfig: ({ position: "top" })
|
property var barConfig: ({ position: "top" })
|
||||||
property var shellConfig: ({ version: 1, idle: {}, plugins: [], bar: { layout: { left: [], center: [], right: [] } } })
|
property var shellConfig: ({ version: 1, idle: {}, plugins: [], bar: { layout: { left: [], center: [], right: [] } } })
|
||||||
function firstPartyServiceFor(id) {
|
function firstPartyServiceFor(id) { return null }
|
||||||
if (id === "omarchy.notifications") return mockNotificationService
|
function serviceFor(id) { return null }
|
||||||
return null
|
|
||||||
}
|
|
||||||
function serviceFor(id) { return firstPartyServiceFor(id) }
|
|
||||||
function summon(id, payloadJson) { return true }
|
function summon(id, payloadJson) { return true }
|
||||||
function hide(id) { return true }
|
function hide(id) { return true }
|
||||||
function toggle(id, payloadJson) { return true }
|
function toggle(id, payloadJson) { return true }
|
||||||
|
|||||||
@@ -188,7 +188,6 @@ assertEqual(notifications.imageExtension('/tmp/no-extension'), 'png', 'notificat
|
|||||||
assertEqual(notifications.imageExtension('/tmp/archive.reallylong'), 'png', 'notifications reject suspicious image extensions')
|
assertEqual(notifications.imageExtension('/tmp/archive.reallylong'), 'png', 'notifications reject suspicious image extensions')
|
||||||
|
|
||||||
const serviceQml = fs.readFileSync(path.join(root, 'shell/plugins/notifications/Service.qml'), 'utf8')
|
const serviceQml = fs.readFileSync(path.join(root, 'shell/plugins/notifications/Service.qml'), 'utf8')
|
||||||
const barWidgetQml = fs.readFileSync(path.join(root, 'shell/plugins/notifications/BarWidget.qml'), 'utf8')
|
|
||||||
assert(
|
assert(
|
||||||
/readonly property int historyReplayLimit: 5/.test(serviceQml),
|
/readonly property int historyReplayLimit: 5/.test(serviceQml),
|
||||||
'notifications service limits history replay to five rows'
|
'notifications service limits history replay to five rows'
|
||||||
@@ -197,8 +196,4 @@ assert(
|
|||||||
/function showHistory\(\): string \{\s*return service\.showRecentHistory\(\)\s*\}/.test(serviceQml),
|
/function showHistory\(\): string \{\s*return service\.showRecentHistory\(\)\s*\}/.test(serviceQml),
|
||||||
'notifications history IPC replays recent notifications'
|
'notifications history IPC replays recent notifications'
|
||||||
)
|
)
|
||||||
assert(
|
|
||||||
!barWidgetQml.includes('historyOpenRequested'),
|
|
||||||
'notifications history IPC does not depend on the bar widget'
|
|
||||||
)
|
|
||||||
JS
|
JS
|
||||||
|
|||||||
Reference in New Issue
Block a user