Replace mako with quickshell-backed notification daemon
Adds first-party omarchy.notifications service plugin that hosts a freedesktop notification server and renders popups + a history popup inside the shell. Uninstalls mako and retargets every helper, keybind, indicator, and migration entry to the new daemon. Plugin (default/quickshell/omarchy-shell/plugins/notifications/): - Service.qml: NotificationServer, popupModel + pendingModel + pastModel (two-tier history, see below), DND via PersistentProperties + cache-file backstop, image cache for /tmp screenshots, IpcHandler with toggleDnd/setDnd/isDnd/showHistory/clear/clearPending/markAllSeen/ dismissAll/dismissOne/invokeLast/dismiss, per-theme override file ~/.config/omarchy/current/theme/notifications.json honoring borderColor/backgroundColor/textColor/countdownColor. - components/NotificationCard.qml: theme-driven card (Color.foreground/ background/border tokens from Commons/Color.qml), 32x32 icon slot, Nerd Font glyph fallback via omarchy-glyph hint, hero image strip for screenshot/image-path notifications, hover-pause progress bar, uses bar.fontFamily so all surfaces share one font. Filtering and DND: - transient hint and CLI-style senders (app_name in notify-send / omarchy-action) bypass history but still pop. - DND only allows omarchy-action toasts and notify-send -u critical through; real-app urgency=critical (Discord, Slack, Vesktop) is silenced and lands in pending instead. - Pending vs past split surfaced via tabs in the bar widget popup; past tab is auto-pruned at the 15-minute mark. - Click-to-jump: notifications without a libnotify default action focus the matching Hyprland window via class lookup. Shell host: - shell.qml: generic first-party service loader (mirrors the existing noctalia-compat path) and an alias for the bar so plugins can read barSize / barHidden / position for anchoring. - Commons/Color.qml: parses the theme's hyprland.conf for $activeBorderColor so notifications match Hyprland window borders; picks the explicit accent= key over the color4= alias. Bar widget rebase (plugins/bar/widgets/notificationCenter.qml): - Drops the chunk-1 stub server, binds count/dnd state to the service, hosts the history popup via PopupCard so it drops down from the notification glyph the same way Quick Settings does. - Pending/Past tabs, dismiss-individual close X, mark-all-as-seen and clear-recent action buttons, theme-driven palette. Quick Settings rework (plugins/bar/widgets/controlCenter.qml): - DND tile binds directly to service.doNotDisturb for instant feedback. - Drops the volume slider (already in audioPanel) and the no-op Theme tile; adds a Bluetooth toggle bound to Quickshell.Bluetooth. - Bigger 44x44 wallet was scaled back to 32x32 for tighter rows. Notification scripts (bin/omarchy-*): - omarchy-notification-send: passes glyph as a custom hint instead of prepending to the summary; adds -a omarchy-action and -u urgency automatically; supports -e/--transient passthrough. - User-action toasts in the capture / toggle / hyprland / default-* scripts and bindings/utilities.lua now tag themselves -a omarchy-action so DND treats them as intent-based bypass. - omarchy-toggle-notification-silencing, omarchy-notification-dismiss, default/waybar/indicators/notification-silencing.sh, and the Hyprland comma-keybinds all route through omarchy-shell-ipc notifications. - omarchy-capture-screenshot / -screenrecording set the image-path hint properly so the hero-image rendering kicks in. Mako removal (migrations/1778743515.sh): - pkill -x mako, systemctl --user stop mako.service, pacman -Rns mako (uninstalling deletes /usr/lib/systemd/user/mako.service so D-Bus activation can't respawn it). Removes ~/.config/mako/ and the legacy toggle file. Restarts quickshell so it claims the bus name. - Drops mako from install/omarchy-base.packages, autostart.lua, install/config/theme.sh + toggles.sh, default/themed/mako.ini.tpl, default/mako/, the omarchy-menu Mako restart row, bin/omarchy GROUP_DESCRIPTIONS, the settings panel catalogue, and the default/omarchy-skill paths table. - Removed scripts: bin/omarchy-restart-mako, bin/omarchy-style-corners-mako. - bin/omarchy-style-corners summary updated; corner radius for the notification card reads ~/.local/state/omarchy/toggles/quickshell-menu.json alongside the rest of the shell.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Bluetooth
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Pipewire
|
||||
import Quickshell.Services.UPower
|
||||
@@ -25,15 +26,22 @@ Item {
|
||||
return value === undefined || value === null ? fallback : value
|
||||
}
|
||||
|
||||
readonly property var sink: Pipewire.defaultAudioSink
|
||||
readonly property real currentVolume: sink && sink.audio && isFinite(sink.audio.volume) ? sink.audio.volume : 0
|
||||
readonly property bool sinkMuted: sink && sink.audio ? sink.audio.muted : false
|
||||
PwObjectTracker { objects: root.sink ? [root.sink] : [] }
|
||||
// Volume controls live in the audioPanel bar widget, so the quick-settings
|
||||
// popup just hosts brightness + toggles. Bluetooth adapter status is
|
||||
// exposed via Quickshell.Bluetooth.
|
||||
readonly property var btAdapter: Bluetooth.defaultAdapter
|
||||
readonly property bool btEnabled: btAdapter ? btAdapter.enabled : false
|
||||
|
||||
property int currentBrightness: -1
|
||||
property int pendingBrightness: -1
|
||||
|
||||
property bool dndActive: false
|
||||
// DND state is bound straight to the notifications service so toggling
|
||||
// from the quick-settings tile or the bar widget reflects instantly.
|
||||
readonly property var hostShell: bar && bar.shell ? bar.shell : null
|
||||
readonly property var notificationService: hostShell && typeof hostShell.firstPartyServiceFor === "function"
|
||||
? hostShell.firstPartyServiceFor("omarchy.notifications")
|
||||
: null
|
||||
readonly property bool dndActive: notificationService ? notificationService.doNotDisturb : false
|
||||
property bool idleInhibited: false
|
||||
property bool nightLightActive: false
|
||||
property bool nightLightAvailable: false
|
||||
@@ -42,16 +50,6 @@ Item {
|
||||
readonly property bool powerProfileAvailable: PowerProfiles.hasPerformanceProfile || PowerProfiles.profile === PowerProfile.PowerSaver || PowerProfiles.profile === PowerProfile.Balanced
|
||||
readonly property int currentProfile: PowerProfiles.profile
|
||||
|
||||
function setVolume(value) {
|
||||
if (!sink || !sink.audio) return
|
||||
sink.audio.volume = Math.max(0, Math.min(1, value))
|
||||
}
|
||||
|
||||
function toggleMute() {
|
||||
if (!sink || !sink.audio) return
|
||||
sink.audio.muted = !sink.audio.muted
|
||||
}
|
||||
|
||||
function setBrightness(percent) {
|
||||
var clamped = Math.max(1, Math.min(100, Math.round(percent)))
|
||||
currentBrightness = clamped
|
||||
@@ -61,7 +59,6 @@ Item {
|
||||
|
||||
function refresh() {
|
||||
if (!brightnessProc.running) brightnessProc.running = true
|
||||
if (!dndProc.running) dndProc.running = true
|
||||
if (!idleProc.running) idleProc.running = true
|
||||
if (!nightLightProc.running) nightLightProc.running = true
|
||||
if (!themeProc.running) themeProc.running = true
|
||||
@@ -98,15 +95,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: dndProc
|
||||
command: ["bash", "-lc", "if command -v makoctl >/dev/null && makoctl mode 2>/dev/null | grep -q '^do-not-disturb$'; then echo on; else echo off; fi"]
|
||||
stdout: StdioCollector {
|
||||
waitForEnd: true
|
||||
onStreamFinished: root.dndActive = String(text || "").trim() === "on"
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: idleProc
|
||||
command: ["bash", "-lc", "if pgrep -x hypridle >/dev/null 2>&1; then echo running; else echo inhibited; fi"]
|
||||
@@ -190,49 +178,12 @@ Item {
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 10
|
||||
visible: root.sink !== null
|
||||
visible: root.currentBrightness >= 0
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Common.PillButton {
|
||||
iconText: root.sinkMuted ? "" : ""
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 8
|
||||
verticalPadding: 6
|
||||
iconSize: 16
|
||||
onClicked: root.toggleMute()
|
||||
}
|
||||
|
||||
Common.Slider {
|
||||
bar: root.bar
|
||||
width: parent.width - 90
|
||||
value: root.currentVolume
|
||||
minimum: 0
|
||||
maximum: 1
|
||||
step: 0.05
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onMoved: function(v) { root.setVolume(v) }
|
||||
onReleased: function(v) { root.setVolume(v) }
|
||||
}
|
||||
|
||||
Text {
|
||||
text: Math.round(root.currentVolume * 100) + "%"
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 32
|
||||
horizontalAlignment: Text.AlignRight
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
visible: root.currentBrightness >= 0
|
||||
|
||||
Common.PillButton {
|
||||
iconText: ""
|
||||
foreground: root.bar.foreground
|
||||
@@ -284,7 +235,10 @@ Item {
|
||||
title: "Do Not Disturb"
|
||||
subtitle: root.dndActive ? "On" : "Off"
|
||||
active: root.dndActive
|
||||
onClicked: { root.run("omarchy-toggle-notification-silencing"); dndProc.running = true }
|
||||
onClicked: {
|
||||
if (!root.notificationService) return
|
||||
root.notificationService.setDoNotDisturb(!root.notificationService.doNotDisturb)
|
||||
}
|
||||
}
|
||||
|
||||
Tile {
|
||||
@@ -308,10 +262,15 @@ Item {
|
||||
|
||||
Tile {
|
||||
width: (tileGrid.width - tileGrid.columnSpacing) / 2
|
||||
glyph: ""
|
||||
title: "Theme"
|
||||
subtitle: root.themeName || "—"
|
||||
onClicked: { root.run("omarchy-menu themes"); root.popupOpen = false }
|
||||
glyph: root.btEnabled ? "" : ""
|
||||
title: "Bluetooth"
|
||||
subtitle: !root.btAdapter ? "—" : (root.btEnabled ? "On" : "Off")
|
||||
active: root.btEnabled
|
||||
tileEnabled: root.btAdapter !== null
|
||||
onClicked: {
|
||||
if (!root.btAdapter) return
|
||||
root.btAdapter.enabled = !root.btAdapter.enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
import qs.Commons
|
||||
import "../common" as Common
|
||||
|
||||
Item {
|
||||
@@ -11,80 +12,46 @@ Item {
|
||||
property var settings: ({})
|
||||
|
||||
property bool popupOpen: false
|
||||
|
||||
function closePopout() { popupOpen = false }
|
||||
property var stored: []
|
||||
property bool dnd: false
|
||||
|
||||
readonly property int count: stored.length
|
||||
// Always default to the pending tab when there's anything unseen, no
|
||||
// matter how the popup was opened (click, keybind/IPC, or the closePopout
|
||||
// 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 && typeof hostShell.firstPartyServiceFor === "function"
|
||||
? hostShell.firstPartyServiceFor("omarchy.notifications")
|
||||
: null
|
||||
|
||||
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 (count > 0) return ""
|
||||
if (pendingCount > 0) return ""
|
||||
return ""
|
||||
}
|
||||
|
||||
property bool replaceMako: settings && settings.replaceMako === true
|
||||
|
||||
Loader {
|
||||
active: root.replaceMako
|
||||
sourceComponent: serverComponent
|
||||
}
|
||||
|
||||
Component {
|
||||
id: serverComponent
|
||||
|
||||
NotificationServer {
|
||||
id: server
|
||||
keepOnReload: false
|
||||
bodySupported: true
|
||||
actionsSupported: true
|
||||
imageSupported: true
|
||||
|
||||
onNotification: function(notification) {
|
||||
if (root.dnd) {
|
||||
notification.expire()
|
||||
return
|
||||
}
|
||||
notification.tracked = true
|
||||
var snapshot = {
|
||||
id: notification.id,
|
||||
app: notification.appName,
|
||||
summary: notification.summary,
|
||||
body: notification.body,
|
||||
time: new Date(),
|
||||
ref: notification
|
||||
}
|
||||
var next = root.stored.slice()
|
||||
next.unshift(snapshot)
|
||||
if (next.length > 30) next.pop()
|
||||
root.stored = next
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function dismiss(index) {
|
||||
var item = stored[index]
|
||||
if (item && item.ref && !item.ref.closed) item.ref.dismiss()
|
||||
var next = stored.slice()
|
||||
next.splice(index, 1)
|
||||
stored = next
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
for (var i = 0; i < stored.length; i++) {
|
||||
if (stored[i].ref && !stored[i].ref.closed) stored[i].ref.dismiss()
|
||||
}
|
||||
stored = []
|
||||
}
|
||||
|
||||
function relativeTime(date) {
|
||||
if (!date) return ""
|
||||
var diff = (Date.now() - date.getTime()) / 1000
|
||||
if (diff < 60) return "just now"
|
||||
if (diff < 3600) return Math.floor(diff / 60) + "m"
|
||||
if (diff < 86400) return Math.floor(diff / 3600) + "h"
|
||||
return Math.floor(diff / 86400) + "d"
|
||||
}
|
||||
// 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: Qt.rgba(Color.foreground.r, Color.foreground.g, Color.foreground.b, 0.18)
|
||||
readonly property color colSurface: Qt.rgba(Color.foreground.r, Color.foreground.g, Color.foreground.b, 0.06)
|
||||
readonly property color colAccent: Color.accent
|
||||
readonly property int cardRadius: notificationService ? notificationService.cornerRadius : 10
|
||||
|
||||
implicitWidth: button.implicitWidth
|
||||
implicitHeight: button.implicitHeight
|
||||
@@ -94,148 +61,315 @@ Item {
|
||||
anchors.fill: parent
|
||||
bar: root.bar
|
||||
text: root.icon
|
||||
active: root.count > 0 && !root.dnd
|
||||
tooltipText: root.dnd ? "Do Not Disturb" : (root.count > 0 ? root.count + " notifications" : "No notifications")
|
||||
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) root.dnd = !root.dnd
|
||||
else root.popupOpen = !root.popupOpen
|
||||
if (b === Qt.RightButton) {
|
||||
if (root.notificationService) {
|
||||
root.notificationService.setDoNotDisturb(!root.notificationService.doNotDisturb)
|
||||
}
|
||||
} else {
|
||||
root.popupOpen = !root.popupOpen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Service-side IPC (omarchy-shell-ipc notifications showHistory) flips
|
||||
// historyOpenRequested; we toggle our local popup state from here so the
|
||||
// keybind path lands in the same PopupCard the click path uses.
|
||||
Connections {
|
||||
target: root.notificationService
|
||||
ignoreUnknownSignals: true
|
||||
function onHistoryOpenRequested() {
|
||||
root.popupOpen = true
|
||||
}
|
||||
}
|
||||
|
||||
Common.PopupCard {
|
||||
id: popup
|
||||
anchorItem: button
|
||||
owner: root
|
||||
bar: root.bar
|
||||
owner: root
|
||||
open: root.popupOpen
|
||||
contentWidth: 340
|
||||
contentHeight: Math.min(420, listColumn.implicitHeight + 60)
|
||||
contentWidth: 440
|
||||
contentHeight: 540
|
||||
|
||||
Column {
|
||||
id: listColumn
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 8
|
||||
spacing: 10
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
// ----------------------------------------- header
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: "Notifications"
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 13
|
||||
font.family: root.bar ? root.bar.fontFamily : ""
|
||||
color: root.colForeground
|
||||
font.pixelSize: 14
|
||||
font.bold: true
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item { width: parent.width - 220; height: 1 }
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Common.PillButton {
|
||||
iconText: root.dnd ? "" : ""
|
||||
text: root.dnd ? "DND" : ""
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 8
|
||||
verticalPadding: 4
|
||||
active: root.dnd
|
||||
onClicked: root.dnd = !root.dnd
|
||||
}
|
||||
Rectangle {
|
||||
id: dndPill
|
||||
Layout.preferredHeight: 24
|
||||
Layout.preferredWidth: dndLabel.implicitWidth + dndGlyph.implicitWidth + 18
|
||||
radius: Math.min(12, root.cardRadius + 6)
|
||||
color: dndOn ? root.colAccent : root.colSurface
|
||||
border.color: dndOn ? root.colAccent : root.colBorder
|
||||
border.width: 1
|
||||
|
||||
Common.PillButton {
|
||||
iconText: ""
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 8
|
||||
verticalPadding: 4
|
||||
enabled: root.count > 0
|
||||
opacity: enabled ? 1 : 0.4
|
||||
onClicked: root.clearAll()
|
||||
readonly property bool dndOn: !!root.notificationService && root.notificationService.doNotDisturb
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
id: dndGlyph
|
||||
text: dndPill.dndOn ? "" : ""
|
||||
font.family: root.bar ? root.bar.fontFamily : ""
|
||||
color: dndPill.dndOn ? Color.background : root.colDim
|
||||
font.pixelSize: 12
|
||||
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: 10
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Flickable {
|
||||
width: parent.width
|
||||
height: Math.min(320, contentHeight)
|
||||
contentHeight: feedColumn.implicitHeight
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
// ----------------------------------------- tabs
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
Column {
|
||||
id: feedColumn
|
||||
width: parent.width
|
||||
spacing: 4
|
||||
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
|
||||
|
||||
Repeater {
|
||||
model: root.stored
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 30
|
||||
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: 12
|
||||
font.bold: parent.isActive
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
required property var modelData
|
||||
required property int index
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: 2
|
||||
color: parent.isActive ? root.colAccent : root.colBorder
|
||||
opacity: parent.isActive ? 1 : 0.4
|
||||
}
|
||||
|
||||
width: feedColumn.width
|
||||
radius: 4
|
||||
color: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.06)
|
||||
implicitHeight: notifContent.implicitHeight + 16
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.activeTab = modelData.key
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: notifContent
|
||||
// ----------------------------------------- action row
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
visible: (root.activeTab === "pending" && root.pendingCount > 0)
|
||||
|| (root.activeTab === "past" && root.pastCount > 0)
|
||||
spacing: 8
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: actionLabel.implicitWidth + 16
|
||||
Layout.preferredHeight: 22
|
||||
radius: Math.min(6, root.cardRadius)
|
||||
color: actionArea.containsMouse ? root.colBorder : "transparent"
|
||||
border.color: root.colBorder
|
||||
border.width: 1
|
||||
|
||||
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: 10
|
||||
}
|
||||
|
||||
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: 8
|
||||
|
||||
readonly property bool onPending: root.activeTab === "pending"
|
||||
model: !root.notificationService ? null
|
||||
: (onPending ? root.notificationService.pendingModel : root.notificationService.pastModel)
|
||||
visible: count > 0
|
||||
|
||||
delegate: Rectangle {
|
||||
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 : appIcon
|
||||
readonly property bool hasIcon: !hasMedia && smallIconSource.length > 0
|
||||
|
||||
width: listView.width
|
||||
implicitHeight: rowContent.implicitHeight + 20
|
||||
radius: root.cardRadius
|
||||
color: "transparent"
|
||||
border.color: root.colBorder
|
||||
border.width: 1
|
||||
|
||||
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: 12
|
||||
anchors.rightMargin: 12
|
||||
spacing: 10
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: 32
|
||||
Layout.preferredHeight: 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
|
||||
anchors.margins: 8
|
||||
spacing: 2
|
||||
source: rowCard.hasMedia ? rowCard.image : rowCard.smallIconSource
|
||||
fillMode: rowCard.hasMedia ? Image.PreserveAspectCrop : Image.PreserveAspectFit
|
||||
sourceSize.width: 32 * Screen.devicePixelRatio
|
||||
sourceSize.height: 32 * Screen.devicePixelRatio
|
||||
asynchronous: true
|
||||
smooth: true
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
|
||||
Text {
|
||||
text: modelData ? (modelData.app || "App") : ""
|
||||
color: Qt.darker(root.bar.foreground, 1.4)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 10
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
width: parent.width - timeText.implicitWidth - dismissBtn.width - 12
|
||||
}
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
visible: rowCard.summary.length > 0
|
||||
text: rowCard.summary
|
||||
font.family: root.bar ? root.bar.fontFamily : ""
|
||||
color: root.colForeground
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
wrapMode: Text.WordWrap
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
|
||||
Text {
|
||||
id: timeText
|
||||
text: modelData ? root.relativeTime(modelData.time) : ""
|
||||
color: Qt.darker(root.bar.foreground, 1.6)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 10
|
||||
}
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
visible: rowCard.body.length > 0
|
||||
text: String(rowCard.body).replace(/<img[^>]*>/gi, "")
|
||||
font.family: root.bar ? root.bar.fontFamily : ""
|
||||
textFormat: Text.PlainText
|
||||
color: root.colDim
|
||||
font.pixelSize: 11
|
||||
wrapMode: Text.WordWrap
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 2
|
||||
}
|
||||
}
|
||||
|
||||
Item { width: 6; height: 1 }
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 18
|
||||
Layout.preferredHeight: 18
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
radius: Math.min(4, root.cardRadius)
|
||||
color: rowCloseArea.containsMouse ? root.colBorder : "transparent"
|
||||
|
||||
Common.PillButton {
|
||||
id: dismissBtn
|
||||
iconText: ""
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 4
|
||||
verticalPadding: 0
|
||||
iconSize: 10
|
||||
onClicked: root.dismiss(index)
|
||||
}
|
||||
}
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "✕"
|
||||
font.family: root.bar ? root.bar.fontFamily : ""
|
||||
color: root.colDim
|
||||
font.pixelSize: 11
|
||||
}
|
||||
|
||||
Text {
|
||||
text: modelData ? (modelData.summary || "") : ""
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: modelData && modelData.body !== ""
|
||||
text: modelData ? (modelData.body || "") : ""
|
||||
color: Qt.darker(root.bar.foreground, 1.2)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 10
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
maximumLineCount: 3
|
||||
elide: Text.ElideRight
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -243,12 +377,36 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: root.stored.length === 0
|
||||
text: root.dnd ? "Do Not Disturb is on" : "Nothing new"
|
||||
color: Qt.darker(root.bar.foreground, 1.5)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
// ----------------------------------------- empty state
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
visible: listView.count === 0
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: 6
|
||||
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: ""
|
||||
font.family: root.bar ? root.bar.fontFamily : ""
|
||||
color: root.colBorder
|
||||
font.pixelSize: 36
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: root.activeTab === "pending"
|
||||
? "Nothing waiting for you"
|
||||
: "Nothing recent"
|
||||
font.family: root.bar ? root.bar.fontFamily : ""
|
||||
? "Nothing waiting for you"
|
||||
: "No past notifications"
|
||||
color: root.colDim
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user