1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-06 21:45:38 -05:00
Files
DankMaterialShell/Widgets/NotificationPopup.qml
2025-07-14 20:32:06 -04:00

490 lines
19 KiB
QML

import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import "../Common"
import "../Common/Utilities.js" as Utils
PanelWindow {
id: notificationPopup
visible: root.showNotificationPopup && root.activeNotification
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
right: true
}
margins {
top: Theme.barHeight
right: 0
}
implicitWidth: 396
implicitHeight: 116 // Just the notification area
Rectangle {
id: popupContainer
anchors.fill: parent
anchors.topMargin: 16 // 16px from the top of this window
anchors.rightMargin: 16 // 16px from the right edge
color: Theme.popupBackground()
radius: Theme.cornerRadiusLarge
border.width: 0 // Remove border completely
// TopBar dropdown animation - slide down from bar
transform: [
Translate {
id: swipeTransform
x: 0
y: root.showNotificationPopup ? 0 : -30
Behavior on y {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
},
Scale {
id: scaleTransform
origin.x: parent.width
origin.y: 0
xScale: root.showNotificationPopup ? 1.0 : 0.95
yScale: root.showNotificationPopup ? 1.0 : 0.8
Behavior on xScale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on yScale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
]
opacity: root.showNotificationPopup ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
// Drag area for swipe gestures
DragHandler {
id: dragHandler
target: null // We'll handle the transform manually
acceptedDevices: PointerDevice.TouchScreen | PointerDevice.Mouse
property real startX: 0
property real currentDelta: 0
property bool isDismissing: false
onActiveChanged: {
if (active) {
startX = centroid.position.x
currentDelta = 0
isDismissing = false
} else {
// Handle end of drag
let deltaX = centroid.position.x - startX
if (Math.abs(deltaX) > 80) { // Threshold for swipe action
if (deltaX > 0) {
// Swipe right - open notification history
swipeOpenHistory()
} else {
// Swipe left - dismiss notification
swipeDismiss()
}
} else {
// Snap back to original position
snapBack()
}
}
}
onCentroidChanged: {
if (active) {
let deltaX = centroid.position.x - startX
currentDelta = deltaX
// Limit swipe distance and add resistance
let maxDistance = 120
let resistance = 0.6
if (Math.abs(deltaX) > maxDistance) {
deltaX = deltaX > 0 ? maxDistance : -maxDistance
}
swipeTransform.x = deltaX * resistance
// Visual feedback - reduce opacity when swiping left (dismiss)
if (deltaX < 0) {
popupContainer.opacity = Math.max(0.3, 1.0 - Math.abs(deltaX) / 150)
} else {
popupContainer.opacity = Math.max(0.7, 1.0 - Math.abs(deltaX) / 200)
}
}
}
function swipeOpenHistory() {
// Animate to the right and open history
swipeAnimation.to = 400
swipeAnimation.onFinished = function() {
root.notificationHistoryVisible = true
Utils.hideNotificationPopup()
snapBack()
}
swipeAnimation.start()
}
function swipeDismiss() {
// Animate to the left and dismiss
swipeAnimation.to = -400
swipeAnimation.onFinished = function() {
Utils.hideNotificationPopup()
snapBack()
}
swipeAnimation.start()
}
function snapBack() {
swipeAnimation.to = 0
swipeAnimation.onFinished = function() {
popupContainer.opacity = Qt.binding(() => root.showNotificationPopup ? 1.0 : 0.0)
}
swipeAnimation.start()
}
}
// Swipe animation
NumberAnimation {
id: swipeAnimation
target: swipeTransform
property: "x"
duration: 200
easing.type: Easing.OutCubic
}
// Tap area for notification interaction
MouseArea {
anchors.fill: parent
anchors.rightMargin: 36 // Don't overlap with close button
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton
onClicked: (mouse) => {
console.log("Popup clicked!")
if (root.activeNotification) {
root.handleNotificationClick(root.activeNotification)
// Don't remove from history - just hide popup
}
// Hide popup but keep in history
Utils.hideNotificationPopup()
mouse.accepted = true // Prevent event propagation
}
}
// Close button with hover styling
Rectangle {
width: 28
height: 28
radius: 14
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 8
color: closeButtonArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Text {
anchors.centerIn: parent
text: "close"
font.family: Theme.iconFont
font.pixelSize: 16
color: closeButtonArea.containsMouse ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: closeButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton
onClicked: (mouse) => {
Utils.hideNotificationPopup()
mouse.accepted = true // Prevent event propagation
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
// Small dismiss button - bottom right corner with better positioning
Rectangle {
width: 60
height: 18
radius: 9
color: dismissButtonArea.containsMouse ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.1)
border.color: dismissButtonArea.containsMouse ?
Theme.primary :
Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.5)
border.width: 1
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.rightMargin: 12
anchors.bottomMargin: 14 // Moved up for better padding
Row {
anchors.centerIn: parent
spacing: 4
Text {
text: "archive"
font.family: Theme.iconFont
font.pixelSize: 10
color: dismissButtonArea.containsMouse ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: "Dismiss"
font.pixelSize: 10
color: dismissButtonArea.containsMouse ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: dismissButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
// Just hide the popup, keep in history
Utils.hideNotificationPopup()
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
// Content layout
Row {
anchors.fill: parent
anchors.margins: 12
anchors.rightMargin: 32
anchors.bottomMargin: 24 // Even more space to ensure 2px+ margin above dismiss button
spacing: 12
// Notification icon based on EXAMPLE NotificationAppIcon pattern
Rectangle {
width: 48
height: 48
radius: width / 2 // Fully rounded like EXAMPLE
color: Theme.primaryContainer
anchors.verticalCenter: parent.verticalCenter
// Material icon fallback (when no app icon)
Loader {
active: !root.activeNotification || !root.activeNotification.appIcon || root.activeNotification.appIcon === ""
anchors.fill: parent
sourceComponent: Text {
anchors.centerIn: parent
text: "notifications"
font.family: Theme.iconFont
font.pixelSize: 20
color: Theme.primaryText
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
// App icon (when no notification image)
Loader {
active: root.activeNotification && root.activeNotification.appIcon !== "" && (!root.activeNotification.image || root.activeNotification.image === "")
anchors.centerIn: parent
sourceComponent: IconImage {
width: 32
height: 32
asynchronous: true
source: {
if (!root.activeNotification || !root.activeNotification.appIcon) return ""
let appIcon = root.activeNotification.appIcon
// Handle file:// URLs directly
if (appIcon.startsWith("file://") || appIcon.startsWith("/")) {
return appIcon
}
// Otherwise treat as icon name
return Quickshell.iconPath(appIcon, "image-missing")
}
}
}
// Notification image (like Discord user avatar) - PRIORITY
Loader {
active: root.activeNotification && root.activeNotification.image !== ""
anchors.fill: parent
sourceComponent: Item {
anchors.fill: parent
Image {
id: notifImage
anchors.fill: parent
source: root.activeNotification ? root.activeNotification.image : ""
fillMode: Image.PreserveAspectCrop
cache: false
antialiasing: true
asynchronous: true
smooth: true
// Use the parent size for optimization
sourceSize.width: parent.width
sourceSize.height: parent.height
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
maskSource: Rectangle {
width: 48
height: 48
radius: 24 // Fully rounded
}
}
onStatusChanged: {
if (status === Image.Error) {
console.warn("Failed to load notification image:", source)
} else if (status === Image.Ready) {
console.log("Notification image loaded:", source, "size:", sourceSize.width + "x" + sourceSize.height)
}
}
}
// Small app icon overlay when showing notification image
Loader {
active: root.activeNotification && root.activeNotification.appIcon !== ""
anchors.bottom: parent.bottom
anchors.right: parent.right
sourceComponent: IconImage {
width: 16
height: 16
asynchronous: true
source: {
if (!root.activeNotification || !root.activeNotification.appIcon) return ""
let appIcon = root.activeNotification.appIcon
if (appIcon.startsWith("file://") || appIcon.startsWith("/")) {
return appIcon
}
return Quickshell.iconPath(appIcon, "image-missing")
}
}
}
}
}
}
// Text content
Column {
width: parent.width - 68
anchors.top: parent.top
anchors.topMargin: 4 // Move content up slightly
spacing: 3
// Title and timestamp row
Row {
width: parent.width
spacing: 8
Text {
text: root.activeNotification ? (root.activeNotification.summary || "") : ""
font.pixelSize: 14
color: Theme.surfaceText
font.weight: Font.Medium
width: parent.width - timestampText.width - parent.spacing
elide: Text.ElideRight
visible: text.length > 0
anchors.verticalCenter: parent.verticalCenter
}
Text {
id: timestampText
text: root.activeNotification ? formatNotificationTime(root.activeNotification.timestamp) : ""
font.pixelSize: 9
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
visible: text.length > 0
anchors.verticalCenter: parent.verticalCenter
function formatNotificationTime(timestamp) {
if (!timestamp) return ""
const now = new Date()
const notifTime = new Date(timestamp)
const diffMs = now.getTime() - notifTime.getTime()
const diffMinutes = Math.floor(diffMs / 60000)
if (diffMinutes < 1) {
return "now"
} else if (diffMinutes < 60) {
return `${diffMinutes}m`
} else {
const diffHours = Math.floor(diffMs / 3600000)
return `${diffHours}h`
}
}
}
}
Text {
text: root.activeNotification ? (root.activeNotification.body || "") : ""
font.pixelSize: 12
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
width: parent.width
wrapMode: Text.WordWrap
maximumLineCount: 2
elide: Text.ElideRight
visible: text.length > 0
}
}
}
}
}