summaryrefslogtreecommitdiff
path: root/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Popup/NotificationPopup.qml
diff options
context:
space:
mode:
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Popup/NotificationPopup.qml')
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Popup/NotificationPopup.qml1164
1 files changed, 1164 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Popup/NotificationPopup.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Popup/NotificationPopup.qml
new file mode 100644
index 0000000..8c9f7f4
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Notifications/Popup/NotificationPopup.qml
@@ -0,0 +1,1164 @@
+import QtQuick
+import QtQuick.Controls
+import Quickshell
+import Quickshell.Wayland
+import Quickshell.Services.Notifications
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+PanelWindow {
+ id: win
+
+ WindowBlur {
+ targetWindow: win
+ blurX: content.x + content.cardInset + swipeTx.x + tx.x
+ blurY: content.y + content.cardInset + swipeTx.y + tx.y
+ blurWidth: !win._finalized ? Math.max(0, content.width - content.cardInset * 2) : 0
+ blurHeight: !win._finalized ? Math.max(0, content.height - content.cardInset * 2) : 0
+ blurRadius: Theme.cornerRadius
+ }
+
+ WlrLayershell.namespace: "dms:notification-popup"
+
+ required property var notificationData
+ required property string notificationId
+ readonly property bool hasValidData: notificationData && notificationData.notification
+ readonly property alias hovered: cardHoverHandler.hovered
+ property int screenY: 0
+ property bool exiting: false
+ property bool _isDestroying: false
+ property bool _finalized: false
+ property real _lastReportedAlignedHeight: -1
+ property real _storedTopMargin: 0
+ property real _storedBottomMargin: 0
+ readonly property string clearText: I18n.tr("Dismiss")
+ property bool descriptionExpanded: false
+ readonly property bool hasExpandableBody: (notificationData?.htmlBody || "").replace(/<[^>]*>/g, "").trim().length > 0
+ onDescriptionExpandedChanged: {
+ popupHeightChanged();
+ }
+ onImplicitHeightChanged: {
+ const aligned = Theme.px(implicitHeight, dpr);
+ if (Math.abs(aligned - _lastReportedAlignedHeight) < 0.5)
+ return;
+ _lastReportedAlignedHeight = aligned;
+ popupHeightChanged();
+ }
+
+ readonly property bool compactMode: SettingsData.notificationCompactMode
+ readonly property real cardPadding: compactMode ? Theme.notificationCardPaddingCompact : Theme.notificationCardPadding
+ readonly property real popupIconSize: compactMode ? Theme.notificationIconSizeCompact : Theme.notificationIconSizeNormal
+ readonly property real contentSpacing: compactMode ? Theme.spacingXS : Theme.spacingS
+ readonly property real contentBottomClearance: 8
+ readonly property real actionButtonHeight: compactMode ? 20 : 24
+ readonly property real collapsedContentHeight: Math.max(popupIconSize, Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2)) + contentBottomClearance
+ readonly property real privacyCollapsedContentHeight: Math.max(popupIconSize, Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2) + contentBottomClearance
+ readonly property real basePopupHeight: cardPadding * 2 + collapsedContentHeight + actionButtonHeight + contentSpacing
+ readonly property real basePopupHeightPrivacy: cardPadding * 2 + privacyCollapsedContentHeight + actionButtonHeight + contentSpacing
+
+ signal entered
+ signal exitStarted
+ signal exitFinished
+ signal popupHeightChanged
+
+ function startExit() {
+ if (exiting || _isDestroying) {
+ return;
+ }
+ exiting = true;
+ exitStarted();
+ exitAnim.restart();
+ exitWatchdog.restart();
+ if (NotificationService.removeFromVisibleNotifications)
+ NotificationService.removeFromVisibleNotifications(win.notificationData);
+ }
+
+ function forceExit() {
+ if (_isDestroying) {
+ return;
+ }
+ _isDestroying = true;
+ exiting = true;
+ visible = false;
+ exitWatchdog.stop();
+ finalizeExit("forced");
+ }
+
+ function finalizeExit(reason) {
+ if (_finalized) {
+ return;
+ }
+
+ _finalized = true;
+ _isDestroying = true;
+ exitWatchdog.stop();
+ wrapperConn.enabled = false;
+ wrapperConn.target = null;
+ win.exitFinished();
+ }
+
+ visible: !_finalized
+ WlrLayershell.layer: {
+ const envLayer = Quickshell.env("DMS_NOTIFICATION_LAYER");
+ if (envLayer) {
+ switch (envLayer) {
+ case "bottom":
+ return WlrLayershell.Bottom;
+ case "overlay":
+ return WlrLayershell.Overlay;
+ case "background":
+ return WlrLayershell.Background;
+ case "top":
+ return WlrLayershell.Top;
+ }
+ }
+
+ if (!notificationData)
+ return WlrLayershell.Top;
+
+ SettingsData.notificationOverlayEnabled;
+
+ const shouldUseOverlay = (SettingsData.notificationOverlayEnabled) || (notificationData.urgency === NotificationUrgency.Critical);
+
+ return shouldUseOverlay ? WlrLayershell.Overlay : WlrLayershell.Top;
+ }
+ WlrLayershell.exclusiveZone: -1
+ WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
+ color: "transparent"
+ readonly property real contentImplicitWidth: screen ? Math.min(400, Math.max(320, screen.width * 0.23)) : 380
+ readonly property real contentImplicitHeight: {
+ if (SettingsData.notificationPopupPrivacyMode && !descriptionExpanded)
+ return basePopupHeightPrivacy;
+ if (!descriptionExpanded)
+ return basePopupHeight;
+ const bodyTextHeight = bodyText.contentHeight || 0;
+ const collapsedBodyHeight = Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2);
+ if (bodyTextHeight > collapsedBodyHeight + 2)
+ return basePopupHeight + bodyTextHeight - collapsedBodyHeight;
+ return basePopupHeight;
+ }
+ implicitWidth: contentImplicitWidth + (windowShadowPad * 2)
+ implicitHeight: contentImplicitHeight + (windowShadowPad * 2)
+
+ Behavior on implicitHeight {
+ enabled: !exiting && !_isDestroying
+ NumberAnimation {
+ id: implicitHeightAnim
+ duration: descriptionExpanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: Theme.expressiveCurves.emphasized
+ }
+ }
+
+ onHasValidDataChanged: {
+ if (!hasValidData && !exiting && !_isDestroying) {
+ forceExit();
+ }
+ }
+ Component.onCompleted: {
+ _lastReportedAlignedHeight = Theme.px(implicitHeight, dpr);
+ _storedTopMargin = getTopMargin();
+ _storedBottomMargin = getBottomMargin();
+ if (SettingsData.notificationPopupPrivacyMode)
+ descriptionExpanded = false;
+ if (hasValidData) {
+ Qt.callLater(() => enterX.restart());
+ } else {
+ forceExit();
+ }
+ }
+ onNotificationDataChanged: {
+ if (!_isDestroying) {
+ if (SettingsData.notificationPopupPrivacyMode)
+ descriptionExpanded = false;
+ wrapperConn.target = win.notificationData || null;
+ notificationConn.target = (win.notificationData && win.notificationData.notification && win.notificationData.notification.Retainable) || null;
+ }
+ }
+ onEntered: {
+ if (!_isDestroying) {
+ enterDelay.start();
+ }
+ }
+ Component.onDestruction: {
+ _isDestroying = true;
+ exitWatchdog.stop();
+ if (notificationData && notificationData.timer) {
+ notificationData.timer.stop();
+ }
+ }
+
+ property bool isTopCenter: SettingsData.notificationPopupPosition === -1
+ property bool isBottomCenter: SettingsData.notificationPopupPosition === SettingsData.Position.BottomCenter
+ property bool isCenterPosition: isTopCenter || isBottomCenter
+ readonly property real maxPopupShadowBlurPx: Math.max((Theme.elevationLevel3 && Theme.elevationLevel3.blurPx !== undefined) ? Theme.elevationLevel3.blurPx : 12, (Theme.elevationLevel4 && Theme.elevationLevel4.blurPx !== undefined) ? Theme.elevationLevel4.blurPx : 16)
+ readonly property real maxPopupShadowOffsetXPx: Math.max(Math.abs(Theme.elevationOffsetX(Theme.elevationLevel3)), Math.abs(Theme.elevationOffsetX(Theme.elevationLevel4)))
+ readonly property real maxPopupShadowOffsetYPx: Math.max(Math.abs(Theme.elevationOffsetY(Theme.elevationLevel3, 6)), Math.abs(Theme.elevationOffsetY(Theme.elevationLevel4, 8)))
+ readonly property real windowShadowPad: Theme.elevationEnabled && SettingsData.notificationPopupShadowEnabled ? Theme.snap(Math.max(16, maxPopupShadowBlurPx + Math.max(maxPopupShadowOffsetXPx, maxPopupShadowOffsetYPx) + 8), dpr) : 0
+
+ anchors.top: true
+ anchors.left: true
+ anchors.bottom: false
+ anchors.right: false
+
+ mask: contentInputMask
+
+ Region {
+ id: contentInputMask
+ item: contentMaskRect
+ }
+
+ Item {
+ id: contentMaskRect
+ visible: false
+ x: content.x
+ y: content.y
+ width: alignedWidth
+ height: alignedHeight
+ }
+
+ margins {
+ top: getWindowTopMargin()
+ bottom: 0
+ left: getWindowLeftMargin()
+ right: 0
+ }
+
+ function getBarInfo() {
+ if (!screen)
+ return {
+ topBar: 0,
+ bottomBar: 0,
+ leftBar: 0,
+ rightBar: 0
+ };
+ return SettingsData.getAdjacentBarInfo(screen, SettingsData.notificationPopupPosition, {
+ id: "notification-popup",
+ screenPreferences: [screen.name],
+ autoHide: false
+ });
+ }
+
+ function getTopMargin() {
+ const popupPos = SettingsData.notificationPopupPosition;
+ const isTop = isTopCenter || popupPos === SettingsData.Position.Top || popupPos === SettingsData.Position.Left;
+ if (!isTop)
+ return 0;
+
+ const barInfo = getBarInfo();
+ const base = barInfo.topBar > 0 ? barInfo.topBar : Theme.popupDistance;
+ return base + screenY;
+ }
+
+ function getBottomMargin() {
+ const popupPos = SettingsData.notificationPopupPosition;
+ const isBottom = isBottomCenter || popupPos === SettingsData.Position.Bottom || popupPos === SettingsData.Position.Right;
+ if (!isBottom)
+ return 0;
+
+ const barInfo = getBarInfo();
+ const base = barInfo.bottomBar > 0 ? barInfo.bottomBar : Theme.popupDistance;
+ return base + screenY;
+ }
+
+ function getLeftMargin() {
+ if (isCenterPosition)
+ return screen ? (screen.width - alignedWidth) / 2 : 0;
+
+ const popupPos = SettingsData.notificationPopupPosition;
+ const isLeft = popupPos === SettingsData.Position.Left || popupPos === SettingsData.Position.Bottom;
+ if (!isLeft)
+ return 0;
+
+ const barInfo = getBarInfo();
+ return barInfo.leftBar > 0 ? barInfo.leftBar : Theme.popupDistance;
+ }
+
+ function getRightMargin() {
+ if (isCenterPosition)
+ return 0;
+
+ const popupPos = SettingsData.notificationPopupPosition;
+ const isRight = popupPos === SettingsData.Position.Top || popupPos === SettingsData.Position.Right;
+ if (!isRight)
+ return 0;
+
+ const barInfo = getBarInfo();
+ return barInfo.rightBar > 0 ? barInfo.rightBar : Theme.popupDistance;
+ }
+
+ function getContentX() {
+ if (!screen)
+ return 0;
+
+ const popupPos = SettingsData.notificationPopupPosition;
+ const barLeft = getLeftMargin();
+ const barRight = getRightMargin();
+
+ if (isCenterPosition)
+ return Theme.snap((screen.width - alignedWidth) / 2, dpr);
+ if (popupPos === SettingsData.Position.Left || popupPos === SettingsData.Position.Bottom)
+ return Theme.snap(barLeft, dpr);
+ return Theme.snap(screen.width - alignedWidth - barRight, dpr);
+ }
+
+ function getContentY() {
+ if (!screen)
+ return 0;
+
+ const popupPos = SettingsData.notificationPopupPosition;
+ const barTop = getTopMargin();
+ const barBottom = getBottomMargin();
+ const isTop = isTopCenter || popupPos === SettingsData.Position.Top || popupPos === SettingsData.Position.Left;
+ if (isTop)
+ return Theme.snap(barTop, dpr);
+ return Theme.snap(screen.height - alignedHeight - barBottom, dpr);
+ }
+
+ function getWindowLeftMargin() {
+ if (!screen)
+ return 0;
+ return Theme.snap(getContentX() - windowShadowPad, dpr);
+ }
+
+ function getWindowTopMargin() {
+ if (!screen)
+ return 0;
+ return Theme.snap(getContentY() - windowShadowPad, dpr);
+ }
+
+ readonly property bool screenValid: win.screen && !_isDestroying
+ readonly property real dpr: screenValid ? CompositorService.getScreenScale(win.screen) : 1
+ readonly property real alignedWidth: Theme.px(Math.max(0, implicitWidth - (windowShadowPad * 2)), dpr)
+ readonly property real alignedHeight: Theme.px(Math.max(0, implicitHeight - (windowShadowPad * 2)), dpr)
+
+ Item {
+ id: content
+
+ x: Theme.snap(windowShadowPad, dpr)
+ y: Theme.snap(windowShadowPad, dpr)
+ width: alignedWidth
+ height: alignedHeight
+ visible: !win._finalized
+ scale: cardHoverHandler.hovered ? 1.01 : 1.0
+ transformOrigin: Item.Center
+
+ Behavior on scale {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ property real swipeOffset: 0
+ readonly property real dismissThreshold: isCenterPosition ? height * 0.4 : width * 0.35
+ readonly property real swipeFadeStartRatio: 0.75
+ readonly property real swipeTravelDistance: isCenterPosition ? height : width
+ readonly property real swipeFadeStartOffset: swipeTravelDistance * swipeFadeStartRatio
+ readonly property real swipeFadeDistance: Math.max(1, swipeTravelDistance - swipeFadeStartOffset)
+ readonly property bool swipeActive: swipeDragHandler.active
+ property bool swipeDismissing: false
+
+ readonly property bool shadowsAllowed: Theme.elevationEnabled && SettingsData.notificationPopupShadowEnabled
+ readonly property var elevLevel: cardHoverHandler.hovered ? Theme.elevationLevel4 : Theme.elevationLevel3
+ readonly property real cardInset: Theme.snap(4, win.dpr)
+ readonly property real shadowRenderPadding: shadowsAllowed ? Theme.snap(Math.max(16, shadowBlurPx + Math.max(Math.abs(shadowOffsetX), Math.abs(shadowOffsetY)) + 8), win.dpr) : 0
+ property real shadowBlurPx: shadowsAllowed ? (elevLevel && elevLevel.blurPx !== undefined ? elevLevel.blurPx : 12) : 0
+ property real shadowOffsetX: shadowsAllowed ? Theme.elevationOffsetX(elevLevel) : 0
+ property real shadowOffsetY: shadowsAllowed ? Theme.elevationOffsetY(elevLevel, 6) : 0
+
+ Behavior on shadowBlurPx {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ Behavior on shadowOffsetX {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ Behavior on shadowOffsetY {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ ElevationShadow {
+ id: bgShadowLayer
+ anchors.fill: parent
+ anchors.margins: -content.shadowRenderPadding
+ level: content.elevLevel
+ fallbackOffset: 6
+ shadowBlurPx: content.shadowBlurPx
+ shadowOffsetX: content.shadowOffsetX
+ shadowOffsetY: content.shadowOffsetY
+ shadowColor: content.shadowsAllowed && content.elevLevel ? Theme.elevationShadowColor(content.elevLevel) : "transparent"
+ shadowEnabled: !win._isDestroying && win.screenValid && content.shadowsAllowed
+ layer.textureSize: Qt.size(Math.round(width * win.dpr), Math.round(height * win.dpr))
+ layer.textureMirroring: ShaderEffectSource.MirrorVertically
+
+ sourceRect.anchors.fill: undefined
+ sourceRect.x: content.shadowRenderPadding + content.cardInset
+ sourceRect.y: content.shadowRenderPadding + content.cardInset
+ sourceRect.width: Math.max(0, content.width - (content.cardInset * 2))
+ sourceRect.height: Math.max(0, content.height - (content.cardInset * 2))
+ sourceRect.radius: Theme.cornerRadius
+ sourceRect.color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
+ sourceRect.border.color: notificationData && notificationData.urgency === NotificationUrgency.Critical ? Theme.withAlpha(Theme.primary, 0.3) : Theme.withAlpha(Theme.outline, 0.08)
+ sourceRect.border.width: notificationData && notificationData.urgency === NotificationUrgency.Critical ? 2 : 0
+
+ Rectangle {
+ x: bgShadowLayer.sourceRect.x
+ y: bgShadowLayer.sourceRect.y
+ width: bgShadowLayer.sourceRect.width
+ height: bgShadowLayer.sourceRect.height
+ radius: bgShadowLayer.sourceRect.radius
+ visible: notificationData && notificationData.urgency === NotificationUrgency.Critical
+ opacity: 1
+ clip: true
+
+ gradient: Gradient {
+ orientation: Gradient.Horizontal
+
+ GradientStop {
+ position: 0
+ color: Theme.primary
+ }
+
+ GradientStop {
+ position: 0.02
+ color: Theme.primary
+ }
+
+ GradientStop {
+ position: 0.021
+ color: "transparent"
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ anchors.margins: content.cardInset
+ radius: Theme.cornerRadius
+ color: "transparent"
+ border.color: BlurService.borderColor
+ border.width: BlurService.borderWidth
+ z: 100
+ }
+
+ Item {
+ id: backgroundContainer
+ anchors.fill: parent
+ anchors.margins: content.cardInset
+ clip: true
+
+ HoverHandler {
+ id: cardHoverHandler
+ }
+
+ Connections {
+ target: cardHoverHandler
+ function onHoveredChanged() {
+ if (!notificationData || win.exiting || win._isDestroying)
+ return;
+ if (cardHoverHandler.hovered) {
+ if (notificationData.timer)
+ notificationData.timer.stop();
+ } else if (notificationData.popup && notificationData.timer) {
+ notificationData.timer.restart();
+ }
+ }
+ }
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ Item {
+ id: notificationContent
+
+ readonly property real expandedTextHeight: bodyText.contentHeight || 0
+ readonly property real collapsedBodyHeight: Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2)
+ readonly property real effectiveCollapsedHeight: (SettingsData.notificationPopupPrivacyMode && !descriptionExpanded) ? win.privacyCollapsedContentHeight : win.collapsedContentHeight
+ readonly property real extraHeight: (descriptionExpanded && expandedTextHeight > collapsedBodyHeight + 2) ? (expandedTextHeight - collapsedBodyHeight) : 0
+
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.topMargin: cardPadding
+ anchors.leftMargin: Theme.spacingL
+ anchors.rightMargin: Theme.spacingL + Theme.notificationHoverRevealMargin
+ height: effectiveCollapsedHeight + extraHeight
+ clip: SettingsData.notificationPopupPrivacyMode && !descriptionExpanded
+
+ DankCircularImage {
+ id: iconContainer
+
+ readonly property string rawImage: notificationData?.image || ""
+ readonly property string iconFromImage: {
+ if (rawImage.startsWith("image://icon/"))
+ return rawImage.substring(13);
+ return "";
+ }
+ readonly property bool imageHasSpecialPrefix: {
+ const icon = iconFromImage;
+ return icon.startsWith("material:") || icon.startsWith("svg:") || icon.startsWith("unicode:") || icon.startsWith("image:");
+ }
+ readonly property bool hasNotificationImage: rawImage !== "" && !rawImage.startsWith("image://icon/")
+ readonly property bool needsImagePersist: hasNotificationImage && rawImage.startsWith("image://qsimage/") && !notificationData.persistedImagePath
+
+ width: popupIconSize
+ height: popupIconSize
+ anchors.left: parent.left
+ anchors.top: parent.top
+ anchors.topMargin: {
+ if (SettingsData.notificationPopupPrivacyMode && !descriptionExpanded) {
+ const headerSummary = Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2;
+ return Math.max(0, headerSummary / 2 - popupIconSize / 2);
+ }
+ if (descriptionExpanded)
+ return Math.max(0, Theme.fontSizeSmall * 1.2 + (Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2)) / 2 - popupIconSize / 2);
+ return Math.max(0, Theme.fontSizeSmall * 1.2 + (textContainer.height - Theme.fontSizeSmall * 1.2) / 2 - popupIconSize / 2);
+ }
+
+ imageSource: {
+ if (!notificationData)
+ return "";
+ if (hasNotificationImage)
+ return notificationData.cleanImage || "";
+ if (imageHasSpecialPrefix)
+ return "";
+ const appIcon = notificationData.appIcon;
+ if (!appIcon)
+ return iconFromImage ? Paths.resolveIconUrl(iconFromImage) : "";
+ if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://") || appIcon.includes("/"))
+ return appIcon;
+ if (appIcon.startsWith("material:") || appIcon.startsWith("svg:") || appIcon.startsWith("unicode:") || appIcon.startsWith("image:"))
+ return "";
+ return Paths.resolveIconPath(appIcon);
+ }
+
+ hasImage: hasNotificationImage
+ fallbackIcon: {
+ if (imageHasSpecialPrefix)
+ return iconFromImage;
+ return notificationData?.appIcon || iconFromImage || "";
+ }
+ fallbackText: {
+ const appName = notificationData?.appName || "?";
+ return appName.charAt(0).toUpperCase();
+ }
+
+ onImageStatusChanged: {
+ if (imageStatus === Image.Ready && needsImagePersist) {
+ const cachePath = NotificationService.getImageCachePath(notificationData);
+ saveImageToFile(cachePath);
+ }
+ }
+
+ onImageSaved: filePath => {
+ if (!notificationData)
+ return;
+ notificationData.persistedImagePath = filePath;
+ const wrapperId = notificationData.notification?.id?.toString() || "";
+ if (wrapperId)
+ NotificationService.updateHistoryImage(wrapperId, filePath);
+ }
+ }
+
+ Column {
+ id: textContainer
+
+ anchors.left: iconContainer.right
+ anchors.leftMargin: Theme.spacingM
+ anchors.right: parent.right
+ anchors.top: parent.top
+ spacing: Theme.notificationContentSpacing
+
+ Row {
+ id: headerRow
+ width: parent.width
+ spacing: Theme.spacingXS
+ visible: headerAppNameText.text.length > 0 || headerTimeText.text.length > 0
+
+ StyledText {
+ id: headerAppNameText
+ text: notificationData ? (notificationData.appName || "") : ""
+ color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Normal
+ elide: Text.ElideRight
+ maximumLineCount: 1
+ width: Math.min(implicitWidth, parent.width - headerSeparator.implicitWidth - headerTimeText.implicitWidth - parent.spacing * 2)
+ }
+
+ StyledText {
+ id: headerSeparator
+ text: (headerAppNameText.text.length > 0 && headerTimeText.text.length > 0) ? " • " : ""
+ color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Normal
+ }
+
+ StyledText {
+ id: headerTimeText
+ text: notificationData ? (notificationData.timeStr || "") : ""
+ color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Normal
+ }
+ }
+
+ StyledText {
+ text: notificationData ? (notificationData.summary || "") : ""
+ color: Theme.surfaceText
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Medium
+ width: parent.width
+ elide: Text.ElideRight
+ horizontalAlignment: Text.AlignLeft
+ maximumLineCount: 1
+ visible: text.length > 0
+ }
+
+ StyledText {
+ id: bodyText
+ property bool hasMoreText: truncated
+
+ text: notificationData ? (notificationData.htmlBody || "") : ""
+ color: Theme.surfaceVariantText
+ font.pixelSize: Theme.fontSizeSmall
+ width: parent.width
+ elide: descriptionExpanded ? Text.ElideNone : Text.ElideRight
+ horizontalAlignment: Text.AlignLeft
+ maximumLineCount: descriptionExpanded ? -1 : (compactMode ? 1 : 2)
+ wrapMode: Text.WrapAtWordBoundaryOrAnywhere
+ visible: text.length > 0
+ opacity: (SettingsData.notificationPopupPrivacyMode && !descriptionExpanded) ? 0 : 1
+ linkColor: Theme.primary
+ onLinkActivated: link => Qt.openUrlExternally(link)
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : (bodyText.hasMoreText || descriptionExpanded) ? Qt.PointingHandCursor : Qt.ArrowCursor
+
+ onClicked: mouse => {
+ if (!parent.hoveredLink && (bodyText.hasMoreText || descriptionExpanded))
+ win.descriptionExpanded = !win.descriptionExpanded;
+ }
+
+ propagateComposedEvents: true
+ onPressed: mouse => {
+ if (parent.hoveredLink)
+ mouse.accepted = false;
+ }
+ onReleased: mouse => {
+ if (parent.hoveredLink)
+ mouse.accepted = false;
+ }
+ }
+ }
+
+ StyledText {
+ text: I18n.tr("Message Content", "notification privacy mode placeholder")
+ color: Theme.surfaceVariantText
+ font.pixelSize: Theme.fontSizeSmall
+ width: parent.width
+ visible: SettingsData.notificationPopupPrivacyMode && !descriptionExpanded && win.hasExpandableBody
+ }
+ }
+ }
+
+ DankActionButton {
+ id: closeButton
+
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.topMargin: cardPadding
+ anchors.rightMargin: Theme.spacingL
+ iconName: "close"
+ iconSize: compactMode ? 14 : 16
+ buttonSize: compactMode ? 20 : 24
+ z: 15
+
+ onClicked: {
+ if (notificationData && !win.exiting)
+ notificationData.popup = false;
+ }
+ }
+
+ DankActionButton {
+ id: expandButton
+
+ anchors.right: closeButton.left
+ anchors.rightMargin: Theme.spacingXS
+ anchors.top: parent.top
+ anchors.topMargin: cardPadding
+ iconName: descriptionExpanded ? "expand_less" : "expand_more"
+ iconSize: compactMode ? 14 : 16
+ buttonSize: compactMode ? 20 : 24
+ z: 15
+ visible: SettingsData.notificationPopupPrivacyMode && win.hasExpandableBody
+
+ onClicked: {
+ if (win.hasExpandableBody)
+ win.descriptionExpanded = !win.descriptionExpanded;
+ }
+ }
+
+ Row {
+ visible: cardHoverHandler.hovered
+ opacity: visible ? 1 : 0
+ anchors.right: clearButton.visible ? clearButton.left : parent.right
+ anchors.rightMargin: clearButton.visible ? contentSpacing : Theme.spacingL
+ anchors.top: notificationContent.bottom
+ anchors.topMargin: contentSpacing
+ spacing: contentSpacing
+ z: 20
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ Repeater {
+ model: notificationData ? (notificationData.actions || []) : []
+
+ Rectangle {
+ property bool isHovered: false
+
+ width: Math.max(actionText.implicitWidth + Theme.spacingM, Theme.notificationActionMinWidth)
+ height: actionButtonHeight
+ radius: Theme.notificationButtonCornerRadius
+ color: isHovered ? Theme.withAlpha(Theme.primary, Theme.stateLayerHover) : "transparent"
+
+ StyledText {
+ id: actionText
+
+ text: modelData.text || "Open"
+ color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ anchors.centerIn: parent
+ elide: Text.ElideRight
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ acceptedButtons: Qt.LeftButton
+ onEntered: parent.isHovered = true
+ onExited: parent.isHovered = false
+ onClicked: {
+ if (modelData && modelData.invoke)
+ modelData.invoke();
+ if (notificationData && !win.exiting)
+ notificationData.popup = false;
+ }
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ id: clearButton
+
+ property bool isHovered: false
+ readonly property int actionCount: notificationData ? (notificationData.actions || []).length : 0
+
+ visible: actionCount < 3 && cardHoverHandler.hovered
+ opacity: visible ? 1 : 0
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingL
+ anchors.top: notificationContent.bottom
+ anchors.topMargin: contentSpacing
+ width: Math.max(clearTextLabel.implicitWidth + Theme.spacingM, Theme.notificationActionMinWidth)
+ height: actionButtonHeight
+ radius: Theme.notificationButtonCornerRadius
+ color: isHovered ? Theme.withAlpha(Theme.primary, Theme.stateLayerHover) : "transparent"
+ z: 20
+
+ StyledText {
+ id: clearTextLabel
+
+ text: win.clearText
+ color: clearButton.isHovered ? Theme.primary : Theme.surfaceVariantText
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ anchors.centerIn: parent
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ acceptedButtons: Qt.LeftButton
+ onEntered: clearButton.isHovered = true
+ onExited: clearButton.isHovered = false
+ onClicked: {
+ if (notificationData && !win.exiting)
+ NotificationService.dismissNotification(notificationData);
+ }
+ }
+ }
+
+ MouseArea {
+ id: cardHoverArea
+
+ anchors.fill: parent
+ hoverEnabled: true
+ acceptedButtons: Qt.LeftButton | Qt.RightButton
+ cursorShape: Qt.PointingHandCursor
+ propagateComposedEvents: true
+ z: -1
+ onClicked: mouse => {
+ if (!notificationData || win.exiting)
+ return;
+ if (mouse.button === Qt.RightButton) {
+ popupContextMenu.popup();
+ } else if (mouse.button === Qt.LeftButton) {
+ const canExpand = bodyText.hasMoreText || win.descriptionExpanded || (SettingsData.notificationPopupPrivacyMode && win.hasExpandableBody);
+ if (canExpand) {
+ win.descriptionExpanded = !win.descriptionExpanded;
+ } else if (notificationData.actions && notificationData.actions.length > 0) {
+ notificationData.actions[0].invoke();
+ NotificationService.dismissNotification(notificationData);
+ } else {
+ notificationData.popup = false;
+ }
+ }
+ }
+ }
+ }
+
+ DragHandler {
+ id: swipeDragHandler
+ target: null
+ xAxis.enabled: !isCenterPosition
+ yAxis.enabled: isCenterPosition
+
+ onActiveChanged: {
+ if (active || win.exiting || content.swipeDismissing)
+ return;
+
+ if (Math.abs(content.swipeOffset) > content.dismissThreshold) {
+ content.swipeDismissing = true;
+ swipeDismissAnim.start();
+ } else {
+ content.swipeOffset = 0;
+ }
+ }
+
+ onTranslationChanged: {
+ if (win.exiting)
+ return;
+
+ const raw = isCenterPosition ? translation.y : translation.x;
+ if (isTopCenter) {
+ content.swipeOffset = Math.min(0, raw);
+ } else if (isBottomCenter) {
+ content.swipeOffset = Math.max(0, raw);
+ } else {
+ const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
+ content.swipeOffset = isLeft ? Math.min(0, raw) : Math.max(0, raw);
+ }
+ }
+ }
+
+ opacity: {
+ const swipeAmount = Math.abs(content.swipeOffset);
+ if (swipeAmount <= content.swipeFadeStartOffset)
+ return 1;
+ const fadeProgress = (swipeAmount - content.swipeFadeStartOffset) / content.swipeFadeDistance;
+ return Math.max(0, 1 - fadeProgress);
+ }
+
+ Behavior on opacity {
+ enabled: !content.swipeActive
+ NumberAnimation {
+ duration: Theme.shortDuration
+ }
+ }
+
+ Behavior on swipeOffset {
+ enabled: !content.swipeActive && !content.swipeDismissing
+ NumberAnimation {
+ duration: Theme.notificationExitDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ NumberAnimation {
+ id: swipeDismissAnim
+ target: content
+ property: "swipeOffset"
+ to: isTopCenter ? -content.height : isBottomCenter ? content.height : (SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom ? -content.width : content.width)
+ duration: Theme.notificationExitDuration
+ easing.type: Easing.OutCubic
+ onStopped: {
+ NotificationService.dismissNotification(notificationData);
+ win.forceExit();
+ }
+ }
+
+ transform: [
+ Translate {
+ id: swipeTx
+ x: isCenterPosition ? 0 : content.swipeOffset
+ y: isCenterPosition ? content.swipeOffset : 0
+ },
+ Translate {
+ id: tx
+ x: {
+ if (isCenterPosition)
+ return 0;
+ const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
+ return isLeft ? -Anims.slidePx : Anims.slidePx;
+ }
+ y: isTopCenter ? -Anims.slidePx : isBottomCenter ? Anims.slidePx : 0
+ }
+ ]
+ }
+
+ NumberAnimation {
+ id: enterX
+
+ target: tx
+ property: isCenterPosition ? "y" : "x"
+ from: {
+ if (isTopCenter)
+ return -Anims.slidePx;
+ if (isBottomCenter)
+ return Anims.slidePx;
+ const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
+ return isLeft ? -Anims.slidePx : Anims.slidePx;
+ }
+ to: 0
+ duration: Theme.notificationEnterDuration
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: isCenterPosition ? Theme.expressiveCurves.standardDecel : Theme.expressiveCurves.emphasizedDecel
+ onStopped: {
+ if (!win.exiting && !win._isDestroying) {
+ if (isCenterPosition) {
+ if (Math.abs(tx.y) < 0.5)
+ win.entered();
+ } else {
+ if (Math.abs(tx.x) < 0.5)
+ win.entered();
+ }
+ }
+ }
+ }
+
+ ParallelAnimation {
+ id: exitAnim
+
+ onStopped: finalizeExit("animStopped")
+
+ PropertyAnimation {
+ target: tx
+ property: isCenterPosition ? "y" : "x"
+ from: 0
+ to: {
+ if (isTopCenter)
+ return -Anims.slidePx;
+ if (isBottomCenter)
+ return Anims.slidePx;
+ const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
+ return isLeft ? -Anims.slidePx : Anims.slidePx;
+ }
+ duration: Theme.notificationExitDuration
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel
+ }
+
+ NumberAnimation {
+ target: content
+ property: "opacity"
+ from: 1
+ to: 0
+ duration: Theme.notificationExitDuration
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: Theme.expressiveCurves.standardAccel
+ }
+
+ NumberAnimation {
+ target: content
+ property: "scale"
+ from: 1
+ to: 0.98
+ duration: Theme.notificationExitDuration
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel
+ }
+ }
+
+ Connections {
+ id: wrapperConn
+
+ function onPopupChanged() {
+ if (!win.notificationData || win._isDestroying)
+ return;
+ if (!win.notificationData.popup && !win.exiting)
+ startExit();
+ }
+
+ target: win.notificationData || null
+ ignoreUnknownSignals: true
+ enabled: !win._isDestroying
+ }
+
+ Connections {
+ id: notificationConn
+
+ function onDropped() {
+ if (!win._isDestroying && !win.exiting)
+ forceExit();
+ }
+
+ target: (win.notificationData && win.notificationData.notification && win.notificationData.notification.Retainable) || null
+ ignoreUnknownSignals: true
+ enabled: !win._isDestroying
+ }
+
+ Timer {
+ id: enterDelay
+
+ interval: 160
+ repeat: false
+ onTriggered: {
+ if (notificationData && notificationData.timer && !exiting && !_isDestroying)
+ notificationData.timer.start();
+ }
+ }
+
+ Timer {
+ id: exitWatchdog
+
+ interval: 600
+ repeat: false
+ onTriggered: finalizeExit("watchdog")
+ }
+
+ Behavior on screenY {
+ id: screenYAnim
+
+ enabled: !exiting && !_isDestroying
+
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: Theme.expressiveCurves.standardDecel
+ }
+ }
+
+ Menu {
+ id: popupContextMenu
+ width: 220
+ contentHeight: 130
+ margins: -1
+ popupType: Popup.Window
+ closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
+
+ background: Rectangle {
+ color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
+ radius: Theme.cornerRadius
+ border.width: 0
+ border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
+ }
+
+ MenuItem {
+ id: setNotificationRulesItem
+ text: I18n.tr("Set notification rules")
+
+ contentItem: StyledText {
+ text: parent.text
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ leftPadding: Theme.spacingS
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ background: Rectangle {
+ color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
+ radius: Theme.cornerRadius / 2
+ }
+
+ onTriggered: {
+ const appName = notificationData?.appName || "";
+ const desktopEntry = notificationData?.desktopEntry || "";
+ SettingsData.addNotificationRuleForNotification(appName, desktopEntry);
+ PopoutService.openSettingsWithTab("notifications");
+ }
+ }
+
+ MenuItem {
+ id: muteUnmuteItem
+ readonly property bool isMuted: SettingsData.isAppMuted(notificationData?.appName || "", notificationData?.desktopEntry || "")
+ text: isMuted ? I18n.tr("Unmute popups for %1").arg(notificationData?.appName || I18n.tr("this app")) : I18n.tr("Mute popups for %1").arg(notificationData?.appName || I18n.tr("this app"))
+
+ contentItem: StyledText {
+ text: parent.text
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ leftPadding: Theme.spacingS
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ background: Rectangle {
+ color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
+ radius: Theme.cornerRadius / 2
+ }
+
+ onTriggered: {
+ const appName = notificationData?.appName || "";
+ const desktopEntry = notificationData?.desktopEntry || "";
+ if (isMuted) {
+ SettingsData.removeMuteRuleForApp(appName, desktopEntry);
+ } else {
+ SettingsData.addMuteRuleForApp(appName, desktopEntry);
+ if (notificationData && !exiting)
+ NotificationService.dismissNotification(notificationData);
+ }
+ }
+ }
+
+ MenuItem {
+ text: I18n.tr("Dismiss")
+
+ contentItem: StyledText {
+ text: parent.text
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ leftPadding: Theme.spacingS
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ background: Rectangle {
+ color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
+ radius: Theme.cornerRadius / 2
+ }
+
+ onTriggered: {
+ if (notificationData && !exiting)
+ NotificationService.dismissNotification(notificationData);
+ }
+ }
+ }
+}