first commit
This commit is contained in:
357
bar/modules/Notifications.qml
Normal file
357
bar/modules/Notifications.qml
Normal file
@@ -0,0 +1,357 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Services.Notifications
|
||||
|
||||
import "../../config" as Cfg
|
||||
import "../../notifications" as Notif
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property real panelRadius: Cfg.Config.radius / 2
|
||||
property int borderWidth: Cfg.Config.panelBorderWidth
|
||||
property bool isTop: true
|
||||
property var targetScreen: null
|
||||
property int barHeight: Cfg.Config.barHeight
|
||||
property int barMargin: Cfg.Config.margin
|
||||
readonly property var t: Cfg.Config.theme
|
||||
|
||||
property int unreadCount: 0
|
||||
property bool historyOpen: historyPopup.popupOpen
|
||||
|
||||
width: 35
|
||||
height: 40
|
||||
radius: panelRadius
|
||||
|
||||
color: hoverHandler.hovered ? t.notifBgHover : t.notifBg
|
||||
border.color: (hoverHandler.hovered || historyOpen) ? t.notifBorderActive : t.notifBorder
|
||||
border.width: borderWidth
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
Behavior on border.color { ColorAnimation { duration: 150 } }
|
||||
Behavior on width { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
|
||||
|
||||
HoverHandler { id: hoverHandler; cursorShape: Qt.PointingHandCursor }
|
||||
TapHandler {
|
||||
onTapped: {
|
||||
if (historyPopup.popupOpen) {
|
||||
historyPopup.close()
|
||||
} else {
|
||||
root.unreadCount = 0
|
||||
historyPopup.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.centerIn: parent
|
||||
width: Cfg.Config.notifIconSize + 10
|
||||
height: Cfg.Config.notifIconSize + 10
|
||||
|
||||
Text {
|
||||
id: bellIcon
|
||||
anchors.centerIn: parent
|
||||
text: ""
|
||||
color: root.unreadCount > 0 ? t.notifIconActive : t.notifIcon
|
||||
font.pixelSize: Cfg.Config.notifIconSize
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: root.unreadCount > 0
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
width: Math.max(14, badgeLabel.implicitWidth + 6)
|
||||
height: 14
|
||||
radius: 7
|
||||
color: t.notifBadgeBg
|
||||
|
||||
Behavior on width { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } }
|
||||
|
||||
Text {
|
||||
id: badgeLabel
|
||||
anchors.centerIn: parent
|
||||
text: root.unreadCount > 99 ? "99+" : root.unreadCount.toString()
|
||||
color: t.notifBadgeText
|
||||
font.pixelSize: 8
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListModel { id: notifHistory }
|
||||
ListModel { id: activeToasts }
|
||||
|
||||
function removeToast(toastId) {
|
||||
for (var i = 0; i < activeToasts.count; i++) {
|
||||
if (activeToasts.get(i).toastId === toastId) {
|
||||
activeToasts.remove(i)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function urgencyColor(urgency) {
|
||||
if (urgency === NotificationUrgency.Critical) return t.notifUrgencyCritical
|
||||
if (urgency === NotificationUrgency.Normal) return t.notifUrgencyNormal
|
||||
return t.notifUrgencyLow
|
||||
}
|
||||
|
||||
function urgencyIcon(urgency) {
|
||||
if (urgency === NotificationUrgency.Critical) return ""
|
||||
if (urgency === NotificationUrgency.Normal) return ""
|
||||
return ""
|
||||
}
|
||||
|
||||
NotificationServer {
|
||||
id: notifServer
|
||||
keepOnReload: false
|
||||
actionIconsSupported: true
|
||||
bodyMarkupSupported: true
|
||||
bodySupported: true
|
||||
persistenceSupported: true
|
||||
imageSupported: true
|
||||
|
||||
onNotification: function(notif) {
|
||||
notif.tracked = true
|
||||
var timeStr = new Date().toLocaleTimeString(Qt.locale(), "HH:mm")
|
||||
var toastId = notif.id + "_" + Date.now()
|
||||
|
||||
notifHistory.insert(0, {
|
||||
nid: notif.id,
|
||||
appName: notif.appName || "Unknown",
|
||||
summary: notif.summary || "",
|
||||
body: notif.body || "",
|
||||
urgency: notif.urgency,
|
||||
timeStr: timeStr
|
||||
})
|
||||
while (notifHistory.count > Cfg.Config.notificationHistorySize)
|
||||
notifHistory.remove(notifHistory.count - 1)
|
||||
|
||||
var cfgTimeout = notif.urgency === NotificationUrgency.Critical
|
||||
? Cfg.Config.notifTimeoutCritical
|
||||
: (notif.urgency === NotificationUrgency.Low
|
||||
? Cfg.Config.notifTimeoutLow
|
||||
: Cfg.Config.notifTimeoutNormal)
|
||||
|
||||
var finalTimeout = (notif.expireTimeout > 0) ? notif.expireTimeout : cfgTimeout
|
||||
|
||||
var toastEntry = {
|
||||
toastId: toastId,
|
||||
nid: notif.id,
|
||||
appName: notif.appName || "Unknown",
|
||||
summary: notif.summary || "",
|
||||
body: notif.body || "",
|
||||
urgency: notif.urgency,
|
||||
timeStr: timeStr,
|
||||
timeout: finalTimeout
|
||||
}
|
||||
if (toastLayer.atBottom) activeToasts.append(toastEntry)
|
||||
else activeToasts.insert(0, toastEntry)
|
||||
|
||||
if (!root.historyOpen) root.unreadCount++
|
||||
}
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: toastLayer
|
||||
screen: root.targetScreen
|
||||
visible: activeToasts.count > 0
|
||||
color: "transparent"
|
||||
|
||||
readonly property string pos: Cfg.Config.notificationPosition
|
||||
readonly property bool atRight: pos === "topright" || pos === "bottomright"
|
||||
readonly property bool atBottom: pos === "bottomleft" || pos === "bottomright"
|
||||
|
||||
anchors { top: true; bottom: true; right: atRight; left: !atRight }
|
||||
implicitWidth: 350 + root.barMargin + 4
|
||||
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.namespace: "main-shell-notifications-toasts"
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
|
||||
mask: Region { item: toastCol }
|
||||
|
||||
readonly property bool shareEdge: (root.isTop && !atBottom) || (!root.isTop && atBottom)
|
||||
readonly property int edgeGap: shareEdge ? root.barMargin + root.barHeight + 10 : root.barMargin + 10
|
||||
|
||||
Column {
|
||||
id: toastCol
|
||||
x: !toastLayer.atRight ? 4 : 4
|
||||
y: !toastLayer.atBottom ? toastLayer.edgeGap : parent.height - toastLayer.edgeGap - toastCol.height
|
||||
spacing: 8
|
||||
width: 350
|
||||
|
||||
Repeater {
|
||||
model: activeToasts
|
||||
Notif.NotificationCard {
|
||||
width: toastCol.width
|
||||
toastId: model.toastId
|
||||
appName: model.appName
|
||||
summary: model.summary
|
||||
body: model.body
|
||||
urgency: model.urgency
|
||||
timeStr: model.timeStr
|
||||
timeout: model.timeout
|
||||
t: root.t
|
||||
urgencyColor: root.urgencyColor
|
||||
urgencyIcon: root.urgencyIcon
|
||||
onDismissed: (id) => root.removeToast(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: historyPopup
|
||||
screen: root.targetScreen
|
||||
visible: false
|
||||
color: "transparent"
|
||||
|
||||
property bool popupOpen: false
|
||||
|
||||
function open() { hideTimer.stop(); visible = true; popupOpen = true }
|
||||
function close() { popupOpen = false; hideTimer.restart() }
|
||||
|
||||
Timer {
|
||||
id: hideTimer
|
||||
interval: Cfg.Config.popupAnimDuration + 70
|
||||
onTriggered: historyPopup.visible = false
|
||||
}
|
||||
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
WlrLayershell.namespace: "main-shell-notifications-history"
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
mask: popupOpen ? null : _noInput
|
||||
Region { id: _noInput }
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: historyPopup.close()
|
||||
}
|
||||
|
||||
Item {
|
||||
id: histClipContainer
|
||||
readonly property int cardW: 340
|
||||
readonly property int screenPad: root.barMargin + 10
|
||||
|
||||
x: {
|
||||
var cx = root.mapToItem(null, 0, 0).x + root.width / 2
|
||||
return Math.max(screenPad, Math.min(cx - cardW / 2, parent.width - cardW - screenPad))
|
||||
}
|
||||
width: cardW
|
||||
|
||||
anchors.top: root.isTop ? parent.top : undefined
|
||||
anchors.bottom: root.isTop ? undefined : parent.bottom
|
||||
anchors.topMargin: root.isTop ? (root.barHeight + 10) : 0
|
||||
anchors.bottomMargin: root.isTop ? 0 : (root.barHeight + 10)
|
||||
height: parent.height - root.barHeight - 10
|
||||
|
||||
clip: true
|
||||
|
||||
Rectangle {
|
||||
id: historyCard
|
||||
width: parent.width
|
||||
height: Math.min(histHeader.implicitHeight + 56 + (notifHistory.count > 0 ? notifHistory.count * 76 : 90), 520)
|
||||
|
||||
anchors.top: root.isTop ? parent.top : undefined
|
||||
anchors.bottom: root.isTop ? undefined : parent.bottom
|
||||
|
||||
transform: Translate {
|
||||
y: historyPopup.popupOpen ? 0 : (root.isTop ? -historyCard.height : historyCard.height)
|
||||
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: Cfg.Config.popupAnimDuration;
|
||||
easing.type: Easing.OutExpo
|
||||
}
|
||||
enabled: historyPopup.visible
|
||||
}
|
||||
}
|
||||
|
||||
opacity: historyPopup.popupOpen ? 1.0 : 0.0
|
||||
Behavior on opacity { NumberAnimation { duration: 200 } }
|
||||
|
||||
color: t.notifHistoryBg
|
||||
radius: Cfg.Config.popupRadius
|
||||
border.color: t.notifHistoryBorder
|
||||
border.width: Cfg.Config.popupBorderWidth
|
||||
layer.enabled: true
|
||||
|
||||
MouseArea { anchors.fill: parent; propagateComposedEvents: false }
|
||||
|
||||
RowLayout {
|
||||
id: histHeader
|
||||
anchors { left: parent.left; right: parent.right; top: parent.top }
|
||||
anchors.topMargin: 20; anchors.leftMargin: 20; anchors.rightMargin: 20
|
||||
spacing: 10
|
||||
|
||||
Text { text: ""; color: t.notifToastAppName; font.pixelSize: 18; Layout.alignment: Qt.AlignVCenter }
|
||||
Text {
|
||||
text: "Notifications"; color: t.notifToastAppName; font.pixelSize: 15; font.bold: true
|
||||
Layout.fillWidth: true; Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: notifHistory.count > 0
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
width: clearText.implicitWidth + 16
|
||||
height: 24
|
||||
radius: 12
|
||||
color: clearHover.hovered ? t.notifHistoryHover : "transparent"
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
|
||||
Text { id: clearText; anchors.centerIn: parent; text: "Clear all"; color: t.notifToastDim; font.pixelSize: 11 }
|
||||
HoverHandler { id: clearHover; cursorShape: Qt.PointingHandCursor }
|
||||
TapHandler { onTapped: { notifHistory.clear(); root.unreadCount = 0 } }
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: histDivider
|
||||
anchors { left: parent.left; right: parent.right; top: histHeader.bottom }
|
||||
anchors.topMargin: 12; height: 1; color: t.notifSeparator
|
||||
}
|
||||
|
||||
Flickable {
|
||||
id: histFlick
|
||||
anchors { left: parent.left; right: parent.right; top: histDivider.bottom; bottom: parent.bottom }
|
||||
anchors.leftMargin: 8; anchors.rightMargin: 8
|
||||
anchors.topMargin: 6; anchors.bottomMargin: 8; clip: true
|
||||
contentHeight: histCol.implicitHeight
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
|
||||
|
||||
Column {
|
||||
id: histCol
|
||||
width: histFlick.width; spacing: 2; topPadding: 4; bottomPadding: 4; leftPadding: 8; rightPadding: 8
|
||||
|
||||
Item {
|
||||
width: histCol.width - 16; height: 90; visible: notifHistory.count === 0
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent; spacing: 8
|
||||
Text { text: ""; color: t.notifHistoryEmpty; font.pixelSize: 30; Layout.alignment: Qt.AlignHCenter }
|
||||
Text { text: "No notifications"; color: t.notifHistoryEmpty; font.pixelSize: 12; Layout.alignment: Qt.AlignHCenter }
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: notifHistory
|
||||
Notif.HistoryCard {
|
||||
width: histCol.width - 16; x: 8; itemIndex: index
|
||||
appName: model.appName; summary: model.summary; body: model.body; urgency: model.urgency; timeStr: model.timeStr
|
||||
t: root.t; urgencyColor: root.urgencyColor; urgencyIcon: root.urgencyIcon
|
||||
onDismissed: (idx) => notifHistory.remove(idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user