Files
dotfiles/dot_config/quickshell/bar/popouts/NotificationCenter.qml
s0wlz (Matthias Puchstein) c5f7162ebb quickshell: add initial bar config with per-monitor workspaces
- Vertical bar on DP-2 with rounded-square pills throughout
- Per-monitor workspace groups sorted by screen x position, with
  Nerd Font icons for named workspaces and apex-neon red active indicator
- Bar layout: datetime+weather top, workspaces centered, gamemode+media+notif+system bottom
- Popouts anchor to triggering icon (top-right for datetime/weather, bottom-right for media/notif/system)
- Lock command switched from hyprlock to swaylock
- Hyprland blur/ignore_alpha layerrules for quickshell namespace

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 20:00:54 +02:00

353 lines
14 KiB
QML

import Quickshell
import Quickshell.Services.Notifications
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
import "../../shared" as Shared
Item {
id: root
required property var trackedNotifications
required property var daemon
function relativeTime(id) {
let ts = root.daemon?.timestamps?.[id];
if (!ts) return "";
let diff = Math.floor((Date.now() - ts) / 1000);
if (diff < 60) return "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";
}
implicitWidth: Shared.Theme.popoutWidth
implicitHeight: Math.min(600, col.implicitHeight + Shared.Theme.popoutPadding * 2)
PopoutBackground { anchors.fill: parent }
MouseArea { anchors.fill: parent }
// Drives relative timestamp re-evaluation
Timer { id: tsRefresh; property int tick: 0; interval: 30000; running: true; repeat: true; onTriggered: tick++ }
// DnD auto-off timer
Timer {
id: dndTimer
property int remaining: 0 // seconds, 0 = indefinite
interval: 1000
running: false
repeat: true
onTriggered: {
remaining--;
if (remaining <= 0) {
running = false;
if (root.daemon) root.daemon.dnd = false;
}
}
}
ColumnLayout {
id: col
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Shared.Theme.popoutPadding
spacing: 8
// Header
RowLayout {
Layout.fillWidth: true
spacing: 8
Text {
text: "Notifications"
color: Shared.Theme.text
font.pixelSize: Shared.Theme.fontLarge
font.family: Shared.Theme.fontFamily
font.bold: true
Layout.fillWidth: true
}
// DnD toggle
Rectangle {
implicitWidth: dndRow.implicitWidth + 12
implicitHeight: 24
radius: 12
color: root.daemon?.dnd ? Qt.alpha(Shared.Theme.danger, Shared.Theme.opacityLight) : (dndMouse.containsMouse ? Shared.Theme.surface1 : Shared.Theme.surface0)
border.width: root.daemon?.dnd ? 1 : 0
border.color: Qt.alpha(Shared.Theme.danger, Shared.Theme.opacityMedium)
Behavior on color { ColorAnimation { duration: 100 } }
RowLayout {
id: dndRow
anchors.centerIn: parent
spacing: 4
Text { text: root.daemon?.dnd ? "\u{f009b}" : "\u{f009a}"; color: root.daemon?.dnd ? Shared.Theme.danger : Shared.Theme.overlay0; font.pixelSize: 12; font.family: Shared.Theme.iconFont }
Text { text: "DnD"; color: root.daemon?.dnd ? Shared.Theme.danger : Shared.Theme.overlay0; font.pixelSize: Shared.Theme.fontSmall; font.family: Shared.Theme.fontFamily }
}
MouseArea {
id: dndMouse
anchors.fill: parent
hoverEnabled: true
onClicked: { if (root.daemon) root.daemon.dnd = !root.daemon.dnd; }
}
}
// Clear all
Rectangle {
visible: root.trackedNotifications.values.length > 0
implicitWidth: clearRow.implicitWidth + 12
implicitHeight: 24
radius: 12
color: clearMouse.containsMouse ? Shared.Theme.surface1 : Shared.Theme.surface0
Behavior on color { ColorAnimation { duration: 100 } }
RowLayout {
id: clearRow
anchors.centerIn: parent
spacing: 4
Text { text: "Clear all"; color: Shared.Theme.overlay0; font.pixelSize: Shared.Theme.fontSmall; font.family: Shared.Theme.fontFamily }
}
MouseArea {
id: clearMouse
anchors.fill: parent
hoverEnabled: true
onClicked: {
let tracked = root.trackedNotifications;
for (let i = tracked.values.length - 1; i >= 0; i--)
tracked.values[i].dismiss();
}
}
}
}
// DnD schedule (visible when DnD is active)
Loader {
Layout.fillWidth: true
active: root.daemon?.dnd === true
visible: active
sourceComponent: RowLayout {
spacing: 4
Repeater {
model: [
{ label: "30m", mins: 30 },
{ label: "1h", mins: 60 },
{ label: "2h", mins: 120 },
{ label: "\u{f0026}", mins: 0 } // infinity = until manual off
]
Rectangle {
required property var modelData
required property int index
readonly property bool isActive: {
if (modelData.mins === 0) return dndTimer.remaining <= 0;
return dndTimer.remaining > 0 && dndTimer.remaining <= modelData.mins * 60;
}
Layout.fillWidth: true
implicitHeight: 22
radius: 11
color: isActive ? Qt.alpha(Shared.Theme.danger, Shared.Theme.opacityLight) : (schedMouse.containsMouse ? Shared.Theme.surface1 : Shared.Theme.surface0)
Behavior on color { ColorAnimation { duration: 100 } }
Text {
anchors.centerIn: parent
text: parent.modelData.label
color: parent.isActive ? Shared.Theme.danger : Shared.Theme.overlay0
font.pixelSize: Shared.Theme.fontSmall
font.family: Shared.Theme.fontFamily
}
MouseArea {
id: schedMouse
anchors.fill: parent
hoverEnabled: true
onClicked: {
if (parent.modelData.mins === 0) {
dndTimer.remaining = 0; // indefinite
} else {
dndTimer.remaining = parent.modelData.mins * 60;
dndTimer.running = true;
}
}
}
}
}
}
}
Rectangle { Layout.fillWidth: true; height: 1; color: Shared.Theme.surface0 }
// Empty state
Text {
visible: root.trackedNotifications.values.length === 0
text: "No notifications"
color: Shared.Theme.overlay0
font.pixelSize: Shared.Theme.fontSize
font.family: Shared.Theme.fontFamily
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 20
Layout.bottomMargin: 20
}
// Notification list
Flickable {
Layout.fillWidth: true
Layout.preferredHeight: Math.min(480, notifList.implicitHeight)
contentHeight: notifList.implicitHeight
clip: true
visible: root.trackedNotifications.values.length > 0
ColumnLayout {
id: notifList
width: parent.width
spacing: 6
Repeater {
model: root.trackedNotifications
Rectangle {
id: notifCard
required property var modelData
Layout.fillWidth: true
implicitHeight: notifContent.implicitHeight + 16
radius: Shared.Theme.radiusSmall
color: notifMouse.containsMouse ? Shared.Theme.surface1 : Shared.Theme.surface0
border.width: modelData.urgency === NotificationUrgency.Critical ? 1 : 0
border.color: Shared.Theme.danger
Behavior on color { ColorAnimation { duration: 100 } }
MouseArea {
id: notifMouse
anchors.fill: parent
hoverEnabled: true
onClicked: {
let actions = notifCard.modelData.actions;
if (actions.length > 0)
actions[0].invoke();
else
notifCard.modelData.dismiss();
}
}
ColumnLayout {
id: notifContent
anchors.fill: parent
anchors.margins: 8
spacing: 4
// App name + close
RowLayout {
Layout.fillWidth: true
spacing: 6
IconImage {
source: notifCard.modelData.appIcon
implicitSize: 14
visible: notifCard.modelData.appIcon !== ""
}
Text {
text: notifCard.modelData.appName || "App"
color: Shared.Theme.overlay0
font.pixelSize: Shared.Theme.fontSmall
font.family: Shared.Theme.fontFamily
Layout.fillWidth: true
elide: Text.ElideRight
}
Text {
property int tick: tsRefresh.tick
text: root.relativeTime(notifCard.modelData.id)
color: Shared.Theme.surface2
font.pixelSize: Shared.Theme.fontSmall
font.family: Shared.Theme.fontFamily
visible: text !== ""
}
Text {
text: "\u{f0156}"
color: dismissMouse.containsMouse ? Shared.Theme.danger : Shared.Theme.overlay0
font.pixelSize: 12
font.family: Shared.Theme.iconFont
MouseArea {
id: dismissMouse
anchors.fill: parent
hoverEnabled: true
onClicked: notifCard.modelData.dismiss()
}
}
}
// Summary
Text {
visible: text !== ""
text: notifCard.modelData.summary
color: Shared.Theme.text
font.pixelSize: Shared.Theme.fontSize
font.family: Shared.Theme.fontFamily
font.bold: true
Layout.fillWidth: true
wrapMode: Text.WordWrap
maximumLineCount: 2
elide: Text.ElideRight
}
// Body
Text {
visible: text !== ""
text: notifCard.modelData.body
textFormat: Text.PlainText
color: Shared.Theme.subtext0
font.pixelSize: Shared.Theme.fontSmall
font.family: Shared.Theme.fontFamily
Layout.fillWidth: true
wrapMode: Text.WordWrap
maximumLineCount: 3
elide: Text.ElideRight
}
// Actions
RowLayout {
visible: notifCard.modelData.actions.length > 0
Layout.fillWidth: true
spacing: 4
Repeater {
model: notifCard.modelData.actions
Rectangle {
required property var modelData
Layout.fillWidth: true
implicitHeight: 24
radius: 4
color: actMouse.containsMouse ? Shared.Theme.surface2 : Shared.Theme.surface1
Text {
anchors.centerIn: parent
text: modelData.text
color: Shared.Theme.text
font.pixelSize: Shared.Theme.fontSmall
font.family: Shared.Theme.fontFamily
}
MouseArea {
id: actMouse
anchors.fill: parent
hoverEnabled: true
onClicked: modelData.invoke()
}
}
}
}
}
}
}
}
}
}
}