diff options
Diffstat (limited to 'raveos-hyprland-theme/theme-data/dms/Modules/Notifications/Popup/NotificationPopup.qml')
| -rw-r--r-- | raveos-hyprland-theme/theme-data/dms/Modules/Notifications/Popup/NotificationPopup.qml | 1164 |
1 files changed, 1164 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/Notifications/Popup/NotificationPopup.qml b/raveos-hyprland-theme/theme-data/dms/Modules/Notifications/Popup/NotificationPopup.qml new file mode 100644 index 0000000..8c9f7f4 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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); + } + } + } +} |