fedora
This commit is contained in:
@@ -0,0 +1,812 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Commons
|
||||
import qs.Services.System
|
||||
import qs.Widgets
|
||||
|
||||
// Simple notification popup - displays multiple notifications
|
||||
Variants {
|
||||
|
||||
model: {
|
||||
const screens = Quickshell.screens.filter(screen => Settings.data.notifications.monitors.includes(screen.name));
|
||||
// Empty list can mean two things :
|
||||
// - No (visible) notification display activated in settings
|
||||
// - One or more (not visible) displays are activated but unplugged
|
||||
// In both cases we fallback to show notification on all screens
|
||||
return screens.length === 0 ? Quickshell.screens : screens;
|
||||
}
|
||||
|
||||
delegate: Loader {
|
||||
id: root
|
||||
|
||||
required property ShellScreen modelData
|
||||
|
||||
property ListModel notificationModel: NotificationService.popupModel
|
||||
|
||||
// Deferred activation via Qt.callLater to avoid activating the Loader
|
||||
// synchronously during ListModel.insert() (which would cause nested
|
||||
// incubation with the inner Repeater).
|
||||
property bool shouldBeActive: false
|
||||
active: shouldBeActive || delayTimer.running
|
||||
|
||||
// Keep loader active briefly after last notification to allow animations to complete
|
||||
Timer {
|
||||
id: delayTimer
|
||||
interval: Style.animationSlow + 200
|
||||
repeat: false
|
||||
}
|
||||
|
||||
function activate() {
|
||||
shouldBeActive = true;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: notificationModel
|
||||
function onCountChanged() {
|
||||
if (notificationModel.count > 0) {
|
||||
if (!root.shouldBeActive) {
|
||||
Qt.callLater(root.activate);
|
||||
}
|
||||
} else if (root.shouldBeActive) {
|
||||
root.shouldBeActive = false;
|
||||
delayTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sourceComponent: PanelWindow {
|
||||
id: notifWindow
|
||||
screen: modelData
|
||||
|
||||
WlrLayershell.namespace: "noctalia-notifications-" + (screen?.name || "unknown")
|
||||
WlrLayershell.layer: (Settings.data.notifications?.overlayLayer) ? WlrLayer.Overlay : WlrLayer.Top
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
color: "transparent"
|
||||
|
||||
// Make shadow area click-through, only notification content is clickable
|
||||
mask: Region {
|
||||
x: 0
|
||||
y: 0
|
||||
width: notifWindow.width
|
||||
height: notifWindow.height
|
||||
intersection: Intersection.Xor
|
||||
|
||||
Region {
|
||||
// The clickable content area is inset by shadowPadding from all edges
|
||||
x: notifWindow.shadowPadding
|
||||
y: notifWindow.shadowPadding
|
||||
width: notifWindow.notifWidth
|
||||
height: Math.max(0, notifWindow.height - notifWindow.shadowPadding * 2)
|
||||
intersection: Intersection.Subtract
|
||||
}
|
||||
}
|
||||
|
||||
// Parse location setting
|
||||
readonly property string location: Settings.data.notifications.location || "top_right"
|
||||
readonly property bool isTop: location.startsWith("top")
|
||||
readonly property bool isBottom: location.startsWith("bottom")
|
||||
readonly property bool isLeft: location.endsWith("_left")
|
||||
readonly property bool isRight: location.endsWith("_right")
|
||||
readonly property bool isCentered: location === "top" || location === "bottom"
|
||||
|
||||
readonly property string barPos: Settings.getBarPositionForScreen(notifWindow.screen?.name)
|
||||
readonly property bool isFloating: Settings.data.bar.barType === "floating"
|
||||
readonly property real barHeight: Style.getBarHeightForScreen(notifWindow.screen?.name)
|
||||
|
||||
readonly property bool isFramed: Settings.data.bar.barType === "framed"
|
||||
readonly property real frameThickness: Settings.data.bar.frameThickness ?? 8
|
||||
|
||||
readonly property bool isCompact: Settings.data.notifications.density === "compact"
|
||||
readonly property int notifWidth: Math.round((isCompact ? 320 : 440) * Style.uiScaleRatio)
|
||||
readonly property int shadowPadding: Style.shadowBlurMax + Style.marginL
|
||||
|
||||
// Calculate bar and frame offsets for each edge separately
|
||||
readonly property int barOffsetTop: {
|
||||
if (barPos !== "top")
|
||||
return isFramed ? frameThickness : 0;
|
||||
const floatMarginV = isFloating ? Math.ceil(Settings.data.bar.marginVertical) : 0;
|
||||
return barHeight + floatMarginV;
|
||||
}
|
||||
|
||||
readonly property int barOffsetBottom: {
|
||||
if (barPos !== "bottom")
|
||||
return isFramed ? frameThickness : 0;
|
||||
const floatMarginV = isFloating ? Math.ceil(Settings.data.bar.marginVertical) : 0;
|
||||
return barHeight + floatMarginV;
|
||||
}
|
||||
|
||||
readonly property int barOffsetLeft: {
|
||||
if (barPos !== "left")
|
||||
return isFramed ? frameThickness : 0;
|
||||
const floatMarginH = isFloating ? Math.ceil(Settings.data.bar.marginHorizontal) : 0;
|
||||
return barHeight + floatMarginH;
|
||||
}
|
||||
|
||||
readonly property int barOffsetRight: {
|
||||
if (barPos !== "right")
|
||||
return isFramed ? frameThickness : 0;
|
||||
const floatMarginH = isFloating ? Math.ceil(Settings.data.bar.marginHorizontal) : 0;
|
||||
return barHeight + floatMarginH;
|
||||
}
|
||||
|
||||
// Anchoring
|
||||
anchors.top: isTop
|
||||
anchors.bottom: isBottom
|
||||
anchors.left: isLeft
|
||||
anchors.right: isRight
|
||||
|
||||
// Margins for PanelWindow - only apply bar offset for the specific edge where the bar is
|
||||
margins.top: isTop ? barOffsetTop - shadowPadding + Style.marginM : 0
|
||||
margins.bottom: isBottom ? barOffsetBottom - shadowPadding : 0
|
||||
margins.left: isLeft ? barOffsetLeft - shadowPadding + Style.marginM : 0
|
||||
margins.right: isRight ? barOffsetRight - shadowPadding + Style.marginM : 0
|
||||
|
||||
implicitWidth: notifWidth + shadowPadding * 2
|
||||
implicitHeight: notificationStack.implicitHeight + Style.marginL
|
||||
|
||||
property var animateConnection: null
|
||||
|
||||
Component.onCompleted: {
|
||||
animateConnection = function (notificationId) {
|
||||
var delegate = null;
|
||||
if (notificationRepeater) {
|
||||
for (var i = 0; i < notificationRepeater.count; i++) {
|
||||
var item = notificationRepeater.itemAt(i);
|
||||
if (item?.notificationId === notificationId) {
|
||||
delegate = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (delegate && typeof delegate.animateOut === "function" && !delegate.isRemoving) {
|
||||
delegate.animateOut();
|
||||
}
|
||||
} catch (e) {
|
||||
// Service fallback if delegate is already invalid
|
||||
NotificationService.dismissPopup(notificationId);
|
||||
}
|
||||
};
|
||||
|
||||
NotificationService.animateAndRemove.connect(animateConnection);
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
if (animateConnection) {
|
||||
NotificationService.animateAndRemove.disconnect(animateConnection);
|
||||
animateConnection = null;
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: notificationStack
|
||||
|
||||
anchors {
|
||||
top: parent.isTop ? parent.top : undefined
|
||||
bottom: parent.isBottom ? parent.bottom : undefined
|
||||
left: parent.isLeft ? parent.left : undefined
|
||||
right: parent.isRight ? parent.right : undefined
|
||||
horizontalCenter: parent.isCentered ? parent.horizontalCenter : undefined
|
||||
}
|
||||
|
||||
spacing: -notifWindow.shadowPadding * 2 + Style.marginM
|
||||
|
||||
Behavior on implicitHeight {
|
||||
enabled: !Settings.data.general.animationDisabled
|
||||
SpringAnimation {
|
||||
spring: 2.0
|
||||
damping: 0.4
|
||||
epsilon: 0.01
|
||||
mass: 0.8
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: notificationRepeater
|
||||
model: notificationModel
|
||||
|
||||
delegate: Item {
|
||||
id: card
|
||||
|
||||
property string notificationId: model.id
|
||||
property var notificationData: model
|
||||
property bool isHovered: false
|
||||
property bool isRemoving: false
|
||||
|
||||
readonly property int animationDelay: index * 100
|
||||
readonly property int slideDistance: 300
|
||||
|
||||
Layout.preferredWidth: notifWidth + notifWindow.shadowPadding * 2
|
||||
Layout.preferredHeight: (notifWindow.isCompact ? compactContent.implicitHeight : notificationContent.implicitHeight) + Style.margin2M + notifWindow.shadowPadding * 2
|
||||
Layout.maximumHeight: Layout.preferredHeight
|
||||
|
||||
// Animation properties
|
||||
property real scaleValue: 0.8
|
||||
property real opacityValue: 0.0
|
||||
property real slideOffset: 0
|
||||
property real swipeOffset: 0
|
||||
property real swipeOffsetY: 0
|
||||
property real pressGlobalX: 0
|
||||
property real pressGlobalY: 0
|
||||
property bool isSwiping: false
|
||||
property bool suppressClick: false
|
||||
readonly property bool useVerticalSwipe: notifWindow.location === "bottom" || notifWindow.location === "top"
|
||||
readonly property real swipeStartThreshold: Math.round(18 * Style.uiScaleRatio)
|
||||
readonly property real swipeDismissThreshold: Math.max(110, cardBackground.width * 0.32)
|
||||
readonly property real verticalSwipeDismissThreshold: Math.max(70, cardBackground.height * 0.35)
|
||||
|
||||
scale: scaleValue
|
||||
opacity: opacityValue
|
||||
transform: Translate {
|
||||
x: card.swipeOffset
|
||||
y: card.slideOffset + card.swipeOffsetY
|
||||
}
|
||||
|
||||
readonly property real slideInOffset: notifWindow.isTop ? -slideDistance : slideDistance
|
||||
readonly property real slideOutOffset: slideInOffset
|
||||
|
||||
function clampSwipeDelta(deltaX) {
|
||||
if (notifWindow.isRight)
|
||||
return Math.max(0, deltaX);
|
||||
if (notifWindow.isLeft)
|
||||
return Math.min(0, deltaX);
|
||||
return deltaX;
|
||||
}
|
||||
|
||||
function clampVerticalSwipeDelta(deltaY) {
|
||||
if (notifWindow.isBottom)
|
||||
return Math.max(0, deltaY);
|
||||
if (notifWindow.isTop)
|
||||
return Math.min(0, deltaY);
|
||||
return deltaY;
|
||||
}
|
||||
|
||||
// Animation setup
|
||||
function triggerEntryAnimation() {
|
||||
animInDelayTimer.stop();
|
||||
removalTimer.stop();
|
||||
resumeTimer.stop();
|
||||
isRemoving = false;
|
||||
isHovered = false;
|
||||
isSwiping = false;
|
||||
swipeOffset = 0;
|
||||
swipeOffsetY = 0;
|
||||
if (Settings.data.general.animationDisabled) {
|
||||
slideOffset = 0;
|
||||
scaleValue = 1.0;
|
||||
opacityValue = 1.0;
|
||||
return;
|
||||
}
|
||||
|
||||
slideOffset = slideInOffset;
|
||||
scaleValue = 0.8;
|
||||
opacityValue = 0.0;
|
||||
animInDelayTimer.interval = animationDelay;
|
||||
animInDelayTimer.start();
|
||||
}
|
||||
|
||||
Component.onCompleted: triggerEntryAnimation()
|
||||
|
||||
onNotificationIdChanged: triggerEntryAnimation()
|
||||
|
||||
Timer {
|
||||
id: animInDelayTimer
|
||||
interval: 0
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (card.isRemoving)
|
||||
return;
|
||||
slideOffset = 0;
|
||||
scaleValue = 1.0;
|
||||
opacityValue = 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
function animateOut() {
|
||||
if (isRemoving)
|
||||
return;
|
||||
animInDelayTimer.stop();
|
||||
resumeTimer.stop();
|
||||
isRemoving = true;
|
||||
isSwiping = false;
|
||||
swipeOffset = 0;
|
||||
swipeOffsetY = 0;
|
||||
if (!Settings.data.general.animationDisabled) {
|
||||
slideOffset = slideOutOffset;
|
||||
scaleValue = 0.8;
|
||||
opacityValue = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
function dismissBySwipe() {
|
||||
if (isRemoving)
|
||||
return;
|
||||
animInDelayTimer.stop();
|
||||
resumeTimer.stop();
|
||||
isRemoving = true;
|
||||
isSwiping = false;
|
||||
if (!Settings.data.general.animationDisabled) {
|
||||
if (useVerticalSwipe) {
|
||||
swipeOffset = 0;
|
||||
swipeOffsetY = swipeOffsetY >= 0 ? cardBackground.height + Style.marginXL : -cardBackground.height - Style.marginXL;
|
||||
} else {
|
||||
swipeOffset = swipeOffset >= 0 ? cardBackground.width + Style.marginXL : -cardBackground.width - Style.marginXL;
|
||||
swipeOffsetY = 0;
|
||||
}
|
||||
scaleValue = 0.8;
|
||||
opacityValue = 0.0;
|
||||
} else {
|
||||
swipeOffset = 0;
|
||||
swipeOffsetY = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function runAction(actionId, isDismissed) {
|
||||
if (!isDismissed) {
|
||||
if (NotificationService.invokeActionAndSuppressClose(notificationId, actionId))
|
||||
card.animateOut();
|
||||
} else {
|
||||
if (Settings.data.notifications.clearDismissed)
|
||||
NotificationService.removeFromHistory(notificationId);
|
||||
card.animateOut();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: removalTimer
|
||||
interval: Style.animationSlow
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
NotificationService.dismissPopup(notificationId);
|
||||
}
|
||||
}
|
||||
|
||||
onIsRemovingChanged: {
|
||||
if (isRemoving) {
|
||||
removalTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
enabled: !Settings.data.general.animationDisabled
|
||||
SpringAnimation {
|
||||
spring: 3
|
||||
damping: 0.4
|
||||
epsilon: 0.01
|
||||
mass: 0.8
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: !Settings.data.general.animationDisabled
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on slideOffset {
|
||||
enabled: !Settings.data.general.animationDisabled
|
||||
SpringAnimation {
|
||||
spring: 2.5
|
||||
damping: 0.3
|
||||
epsilon: 0.01
|
||||
mass: 0.6
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on swipeOffset {
|
||||
enabled: !Settings.data.general.animationDisabled && !card.isSwiping
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on swipeOffsetY {
|
||||
enabled: !Settings.data.general.animationDisabled && !card.isSwiping
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
// Sub item with the right dimensions, really usefull for the
|
||||
// HoverHandler: card items are overlapping because of the
|
||||
// negative spacing of notificationStack.
|
||||
Item {
|
||||
id: displayedCard
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: notifWindow.shadowPadding
|
||||
|
||||
HoverHandler {
|
||||
onHoveredChanged: {
|
||||
isHovered = hovered;
|
||||
if (isHovered) {
|
||||
resumeTimer.stop();
|
||||
NotificationService.pauseTimeout(notificationId);
|
||||
} else {
|
||||
resumeTimer.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: resumeTimer
|
||||
interval: 50
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (!isHovered) {
|
||||
NotificationService.resumeTimeout(notificationId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Right-click to dismiss
|
||||
MouseArea {
|
||||
id: cardDragArea
|
||||
anchors.fill: cardBackground
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
hoverEnabled: true
|
||||
onPressed: mouse => {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
const globalPoint = cardDragArea.mapToGlobal(mouse.x, mouse.y);
|
||||
card.pressGlobalX = globalPoint.x;
|
||||
card.pressGlobalY = globalPoint.y;
|
||||
card.isSwiping = false;
|
||||
card.suppressClick = false;
|
||||
}
|
||||
}
|
||||
onPositionChanged: mouse => {
|
||||
if (!(mouse.buttons & Qt.LeftButton) || card.isRemoving)
|
||||
return;
|
||||
const globalPoint = cardDragArea.mapToGlobal(mouse.x, mouse.y);
|
||||
const rawDeltaX = globalPoint.x - card.pressGlobalX;
|
||||
const rawDeltaY = globalPoint.y - card.pressGlobalY;
|
||||
const deltaX = card.clampSwipeDelta(rawDeltaX);
|
||||
const deltaY = card.clampVerticalSwipeDelta(rawDeltaY);
|
||||
if (!card.isSwiping) {
|
||||
if (card.useVerticalSwipe) {
|
||||
if (Math.abs(deltaY) < card.swipeStartThreshold)
|
||||
return;
|
||||
card.isSwiping = true;
|
||||
} else {
|
||||
if (Math.abs(deltaX) < card.swipeStartThreshold)
|
||||
return;
|
||||
card.isSwiping = true;
|
||||
}
|
||||
}
|
||||
if (card.useVerticalSwipe) {
|
||||
card.swipeOffset = 0;
|
||||
card.swipeOffsetY = deltaY;
|
||||
} else {
|
||||
card.swipeOffset = deltaX;
|
||||
card.swipeOffsetY = 0;
|
||||
}
|
||||
}
|
||||
onReleased: mouse => {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
card.animateOut();
|
||||
if (Settings.data.notifications.clearDismissed) {
|
||||
NotificationService.removeFromHistory(notificationId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (mouse.button !== Qt.LeftButton)
|
||||
return;
|
||||
|
||||
if (card.isSwiping) {
|
||||
const dismissDistance = card.useVerticalSwipe ? Math.abs(card.swipeOffsetY) : Math.abs(card.swipeOffset);
|
||||
const threshold = card.useVerticalSwipe ? card.verticalSwipeDismissThreshold : card.swipeDismissThreshold;
|
||||
if (dismissDistance >= threshold) {
|
||||
card.dismissBySwipe();
|
||||
if (Settings.data.notifications.clearDismissed) {
|
||||
NotificationService.removeFromHistory(notificationId);
|
||||
}
|
||||
} else {
|
||||
card.swipeOffset = 0;
|
||||
card.swipeOffsetY = 0;
|
||||
}
|
||||
card.suppressClick = true;
|
||||
card.isSwiping = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (card.suppressClick)
|
||||
return;
|
||||
|
||||
var actions = model.actionsJson ? JSON.parse(model.actionsJson) : [];
|
||||
var hasDefault = actions.some(function (a) {
|
||||
return a.identifier === "default";
|
||||
});
|
||||
if (hasDefault && NotificationService.invokeActionAndSuppressClose(notificationId, "default")) {
|
||||
card.animateOut();
|
||||
} else {
|
||||
// Without a default action, or if invoking it fails,
|
||||
// the best fallback is focusing the sender window by app identity.
|
||||
NotificationService.focusSenderWindow(model.appName);
|
||||
card.animateOut();
|
||||
}
|
||||
}
|
||||
onCanceled: {
|
||||
card.isSwiping = false;
|
||||
card.swipeOffset = 0;
|
||||
card.swipeOffsetY = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Background with border
|
||||
Rectangle {
|
||||
id: cardBackground
|
||||
anchors.fill: parent
|
||||
radius: Style.radiusL
|
||||
border.color: Qt.alpha(Color.mOutline, Color.adaptiveOpacity(Settings.data.notifications.backgroundOpacity) || 1.0)
|
||||
border.width: Style.borderS
|
||||
color: Qt.alpha(Color.mSurface, Color.adaptiveOpacity(Settings.data.notifications.backgroundOpacity) || 1.0)
|
||||
|
||||
// Progress bar
|
||||
Rectangle {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 2
|
||||
color: "transparent"
|
||||
|
||||
Rectangle {
|
||||
id: progressBar
|
||||
readonly property real progressWidth: cardBackground.width - (2 * cardBackground.radius)
|
||||
height: parent.height
|
||||
x: cardBackground.radius + (progressWidth * (1 - model.progress)) / 2
|
||||
width: progressWidth * model.progress
|
||||
|
||||
color: {
|
||||
var baseColor = model.urgency === 2 ? Color.mError : model.urgency === 0 ? Color.mOnSurface : Color.mPrimary;
|
||||
return Qt.alpha(baseColor, Color.adaptiveOpacity(Settings.data.notifications.backgroundOpacity) || 1.0);
|
||||
}
|
||||
|
||||
antialiasing: true
|
||||
|
||||
Behavior on width {
|
||||
enabled: !card.isRemoving
|
||||
NumberAnimation {
|
||||
duration: 100
|
||||
easing.type: Easing.Linear
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on x {
|
||||
enabled: !card.isRemoving
|
||||
NumberAnimation {
|
||||
duration: 100
|
||||
easing.type: Easing.Linear
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NDropShadow {
|
||||
anchors.fill: cardBackground
|
||||
source: cardBackground
|
||||
autoPaddingEnabled: true
|
||||
}
|
||||
|
||||
// Content
|
||||
ColumnLayout {
|
||||
id: notificationContent
|
||||
visible: !notifWindow.isCompact
|
||||
anchors.fill: cardBackground
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginM
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginL
|
||||
Layout.leftMargin: Style.marginM
|
||||
Layout.rightMargin: Style.marginM
|
||||
Layout.topMargin: Style.marginM
|
||||
Layout.bottomMargin: Style.marginM
|
||||
|
||||
NImageRounded {
|
||||
Layout.preferredWidth: Math.round(40 * Style.uiScaleRatio)
|
||||
Layout.preferredHeight: Math.round(40 * Style.uiScaleRatio)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
radius: Math.min(Style.radiusL, Layout.preferredWidth / 2)
|
||||
imagePath: model.originalImage || ""
|
||||
borderColor: "transparent"
|
||||
borderWidth: 0
|
||||
fallbackIcon: "bell"
|
||||
fallbackIconSize: 24
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
// Header with urgency indicator
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 6
|
||||
Layout.preferredHeight: 6
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
radius: Style.radiusXS
|
||||
color: model.urgency === 2 ? Color.mError : model.urgency === 0 ? Color.mOnSurface : Color.mPrimary
|
||||
}
|
||||
|
||||
NText {
|
||||
text: model.appName || "Unknown App"
|
||||
pointSize: Style.fontSizeXS
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mSecondary
|
||||
}
|
||||
|
||||
NText {
|
||||
textFormat: Text.PlainText
|
||||
text: " " + Time.formatRelativeTime(model.timestamp)
|
||||
pointSize: Style.fontSizeXXS
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignBottom
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: model.summary || I18n.tr("common.no-summary")
|
||||
pointSize: Style.fontSizeM
|
||||
font.weight: Style.fontWeightMedium
|
||||
color: Color.mOnSurface
|
||||
textFormat: Text.StyledText
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
maximumLineCount: 3
|
||||
elide: Text.ElideRight
|
||||
visible: text.length > 0
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: Style.marginM
|
||||
}
|
||||
|
||||
NText {
|
||||
text: model.body || ""
|
||||
pointSize: Style.fontSizeM
|
||||
color: Color.mOnSurface
|
||||
textFormat: Text.StyledText
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
|
||||
maximumLineCount: 5
|
||||
elide: Text.ElideRight
|
||||
visible: text.length > 0
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: Style.marginXL
|
||||
}
|
||||
|
||||
// Actions
|
||||
Flow {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
Layout.topMargin: Style.marginM
|
||||
flow: Flow.LeftToRight
|
||||
|
||||
property string parentNotificationId: notificationId
|
||||
property var parsedActions: {
|
||||
try {
|
||||
return model.actionsJson ? JSON.parse(model.actionsJson) : [];
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
visible: parsedActions.length > 0
|
||||
|
||||
Repeater {
|
||||
model: parent.parsedActions
|
||||
|
||||
delegate: NButton {
|
||||
property var actionData: modelData
|
||||
|
||||
text: {
|
||||
var actionText = actionData.text || "Open";
|
||||
if (actionText.includes(",")) {
|
||||
return actionText.split(",")[1] || actionText;
|
||||
}
|
||||
return actionText;
|
||||
}
|
||||
fontSize: Style.fontSizeS
|
||||
backgroundColor: Color.mPrimary
|
||||
textColor: hovered ? Color.mOnHover : Color.mOnPrimary
|
||||
hoverColor: Color.mHover
|
||||
outlined: false
|
||||
implicitHeight: 24
|
||||
onClicked: {
|
||||
card.runAction(actionData.identifier, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close button
|
||||
NIconButton {
|
||||
visible: !notifWindow.isCompact
|
||||
icon: "close"
|
||||
tooltipText: I18n.tr("tooltips.dismiss-notification")
|
||||
baseSize: Style.baseWidgetSize * 0.6
|
||||
anchors.top: cardBackground.top
|
||||
anchors.topMargin: Style.marginXL
|
||||
anchors.right: cardBackground.right
|
||||
anchors.rightMargin: Style.marginXL
|
||||
|
||||
onClicked: {
|
||||
card.runAction("", true);
|
||||
}
|
||||
}
|
||||
|
||||
// Compact content
|
||||
RowLayout {
|
||||
id: compactContent
|
||||
visible: notifWindow.isCompact
|
||||
anchors.fill: cardBackground
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginS
|
||||
|
||||
NImageRounded {
|
||||
Layout.preferredWidth: Math.round(24 * Style.uiScaleRatio)
|
||||
Layout.preferredHeight: Math.round(24 * Style.uiScaleRatio)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
radius: Style.radiusXS
|
||||
imagePath: model.originalImage || ""
|
||||
borderColor: "transparent"
|
||||
borderWidth: 0
|
||||
fallbackIcon: "bell"
|
||||
fallbackIconSize: 16
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginXS
|
||||
|
||||
NText {
|
||||
text: model.summary || I18n.tr("common.no-summary")
|
||||
pointSize: Style.fontSizeM
|
||||
font.weight: Style.fontWeightMedium
|
||||
color: Color.mOnSurface
|
||||
textFormat: Text.StyledText
|
||||
maximumLineCount: 1
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NText {
|
||||
visible: model.body && model.body.length > 0
|
||||
Layout.fillWidth: true
|
||||
text: model.body || ""
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mOnSurfaceVariant
|
||||
textFormat: Text.StyledText
|
||||
wrapMode: Text.Wrap
|
||||
maximumLineCount: 2
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user