1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-04 04:42:05 -04:00
Files
DankMaterialShell/quickshell/Modules/Notifications/Center/HistoryNotificationCard.qml
purian23 971a511edb fix(notifications): Apply appIdSubs to iconFrImage fallback path
- Consistent with the
appIcon PR changes in #1880.
2026-03-01 17:37:21 -05:00

278 lines
11 KiB
QML

import QtQuick
import QtQuick.Effects
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
required property var historyItem
property bool isSelected: false
property bool keyboardNavigationActive: false
property bool descriptionExpanded: NotificationService.expandedMessages[historyItem?.id ? (historyItem.id + "_hist") : ""] || false
property bool __initialized: false
Component.onCompleted: {
Qt.callLater(() => {
if (root)
root.__initialized = true;
});
}
readonly property bool compactMode: SettingsData.notificationCompactMode
readonly property real cardPadding: compactMode ? Theme.notificationCardPaddingCompact : Theme.notificationCardPadding
readonly property real iconSize: compactMode ? Theme.notificationIconSizeCompact : Theme.notificationIconSizeNormal
readonly property real contentSpacing: compactMode ? Theme.spacingXS : Theme.spacingS
readonly property real collapsedContentHeight: iconSize + cardPadding
readonly property real baseCardHeight: cardPadding * 2 + collapsedContentHeight
width: parent ? parent.width : 400
height: baseCardHeight + contentItem.extraHeight
radius: Theme.cornerRadius
clip: false
readonly property bool shadowsAllowed: Theme.elevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
ElevationShadow {
id: shadowLayer
anchors.fill: parent
z: -1
level: Theme.elevationLevel1
fallbackOffset: 1
targetRadius: root.radius
targetColor: root.color
borderColor: root.border.color
borderWidth: root.border.width
shadowEnabled: root.shadowsAllowed
}
color: {
if (isSelected && keyboardNavigationActive)
return Theme.primaryPressed;
return Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency);
}
border.color: {
if (isSelected && keyboardNavigationActive)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.5);
if (historyItem.urgency === 2)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3);
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05);
}
border.width: {
if (isSelected && keyboardNavigationActive)
return 1.5;
if (historyItem.urgency === 2)
return 2;
return 0;
}
Behavior on border.color {
enabled: root.__initialized
ColorAnimation {
duration: root.__initialized ? Theme.shortDuration : 0
easing.type: Theme.standardEasing
}
}
Rectangle {
anchors.fill: parent
radius: parent.radius
visible: historyItem.urgency === 2
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop {
position: 0.0
color: Theme.primary
}
GradientStop {
position: 0.02
color: Theme.primary
}
GradientStop {
position: 0.021
color: "transparent"
}
}
}
Item {
id: contentItem
readonly property real expandedTextHeight: descriptionText.contentHeight
readonly property real twoLineHeight: descriptionText.font.pixelSize * 1.2 * 2
readonly property real extraHeight: (descriptionExpanded && expandedTextHeight > twoLineHeight + 2) ? (expandedTextHeight - twoLineHeight) : 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: collapsedContentHeight + extraHeight
DankCircularImage {
id: iconContainer
readonly property string rawImage: historyItem.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/")
width: iconSize
height: iconSize
anchors.left: parent.left
anchors.top: parent.top
imageSource: {
if (hasNotificationImage)
return historyItem.image;
if (imageHasSpecialPrefix)
return "";
const appIcon = historyItem.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 historyItem.appIcon || iconFromImage || "";
}
fallbackText: {
const appName = historyItem.appName || "?";
return appName.charAt(0).toUpperCase();
}
Rectangle {
anchors.fill: parent
anchors.margins: -2
radius: width / 2
color: "transparent"
border.color: root.color
border.width: 5
visible: parent.hasImage
antialiasing: true
}
}
Rectangle {
anchors.left: iconContainer.right
anchors.leftMargin: Theme.spacingM
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.bottomMargin: contentSpacing
color: "transparent"
Column {
width: parent.width
anchors.top: parent.top
spacing: Theme.notificationContentSpacing
Row {
width: parent.width
spacing: Theme.spacingXS
readonly property real reservedTrailingWidth: historySeparator.implicitWidth + Math.max(historyTimeText.implicitWidth, 72) + spacing
StyledText {
id: historyTitleText
width: Math.min(implicitWidth, Math.max(0, parent.width - parent.reservedTrailingWidth))
text: {
let title = historyItem.summary || "";
const appName = historyItem.appName || "";
const prefix = appName + " • ";
if (appName && title.toLowerCase().startsWith(prefix.toLowerCase())) {
title = title.substring(prefix.length);
}
return title;
}
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
elide: Text.ElideRight
maximumLineCount: 1
visible: text.length > 0
}
StyledText {
id: historySeparator
text: (historyTitleText.text.length > 0 && historyTimeText.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: historyTimeText
text: NotificationService.formatHistoryTime(historyItem.timestamp)
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Normal
visible: text.length > 0
}
}
StyledText {
id: descriptionText
property bool hasMoreText: truncated
text: historyItem.htmlBody || historyItem.body || ""
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
width: parent.width
elide: descriptionExpanded ? Text.ElideNone : Text.ElideRight
maximumLineCount: descriptionExpanded ? -1 : (compactMode ? 1 : 2)
wrapMode: Text.WordWrap
visible: text.length > 0
linkColor: Theme.primary
onLinkActivated: link => Qt.openUrlExternally(link)
MouseArea {
anchors.fill: parent
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : (parent.hasMoreText || descriptionExpanded) ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: mouse => {
if (!parent.hoveredLink && (parent.hasMoreText || descriptionExpanded)) {
const messageId = historyItem?.id ? (historyItem.id + "_hist") : "";
NotificationService.toggleMessageExpansion(messageId);
}
}
propagateComposedEvents: true
onPressed: mouse => {
if (parent.hoveredLink)
mouse.accepted = false;
}
onReleased: mouse => {
if (parent.hoveredLink)
mouse.accepted = false;
}
}
}
}
}
}
DankActionButton {
anchors.top: parent.top
anchors.right: parent.right
anchors.topMargin: cardPadding
anchors.rightMargin: Theme.spacingL
iconName: "close"
iconSize: compactMode ? 16 : 18
buttonSize: compactMode ? 24 : 28
onClicked: NotificationService.removeFromHistory(historyItem.id)
}
}