1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 21:42:51 -05:00

notifications: add compact mode, expansion in history, expansion in

popup
fixes #1282
This commit is contained in:
bbedward
2026-01-11 12:11:44 -05:00
parent 510ea5d2e4
commit 3c38e17472
7 changed files with 359 additions and 283 deletions

View File

@@ -404,6 +404,7 @@ Singleton {
property int notificationTimeoutLow: 5000 property int notificationTimeoutLow: 5000
property int notificationTimeoutNormal: 5000 property int notificationTimeoutNormal: 5000
property int notificationTimeoutCritical: 0 property int notificationTimeoutCritical: 0
property bool notificationCompactMode: false
property int notificationPopupPosition: SettingsData.Position.Top property int notificationPopupPosition: SettingsData.Position.Top
property bool notificationHistoryEnabled: true property bool notificationHistoryEnabled: true
property int notificationHistoryMaxCount: 50 property int notificationHistoryMaxCount: 50
@@ -1845,7 +1846,11 @@ Singleton {
function addAppIdSubstitution(pattern, replacement, type) { function addAppIdSubstitution(pattern, replacement, type) {
var subs = JSON.parse(JSON.stringify(appIdSubstitutions)); var subs = JSON.parse(JSON.stringify(appIdSubstitutions));
subs.push({ pattern: pattern, replacement: replacement, type: type }); subs.push({
pattern: pattern,
replacement: replacement,
type: type
});
appIdSubstitutions = subs; appIdSubstitutions = subs;
saveSettings(); saveSettings();
} }
@@ -1854,7 +1859,11 @@ Singleton {
var subs = JSON.parse(JSON.stringify(appIdSubstitutions)); var subs = JSON.parse(JSON.stringify(appIdSubstitutions));
if (index < 0 || index >= subs.length) if (index < 0 || index >= subs.length)
return; return;
subs[index] = { pattern: pattern, replacement: replacement, type: type }; subs[index] = {
pattern: pattern,
replacement: replacement,
type: type
};
appIdSubstitutions = subs; appIdSubstitutions = subs;
saveSettings(); saveSettings();
} }

View File

@@ -268,6 +268,7 @@ var SPEC = {
notificationTimeoutLow: { def: 5000 }, notificationTimeoutLow: { def: 5000 },
notificationTimeoutNormal: { def: 5000 }, notificationTimeoutNormal: { def: 5000 },
notificationTimeoutCritical: { def: 0 }, notificationTimeoutCritical: { def: 0 },
notificationCompactMode: { def: false },
notificationPopupPosition: { def: 0 }, notificationPopupPosition: { def: 0 },
notificationHistoryEnabled: { def: true }, notificationHistoryEnabled: { def: true },
notificationHistoryMaxCount: { def: 50 }, notificationHistoryMaxCount: { def: 50 },

View File

@@ -10,9 +10,17 @@ Rectangle {
required property var historyItem required property var historyItem
property bool isSelected: false property bool isSelected: false
property bool keyboardNavigationActive: false property bool keyboardNavigationActive: false
property bool descriptionExpanded: NotificationService.expandedMessages[historyItem?.id ? (historyItem.id + "_hist") : ""] || false
readonly property bool compactMode: SettingsData.notificationCompactMode
readonly property real cardPadding: compactMode ? Theme.spacingS : Theme.spacingM
readonly property real iconSize: compactMode ? 48 : 63
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 width: parent ? parent.width : 400
height: 116 height: baseCardHeight + contentItem.extraHeight
radius: Theme.cornerRadius radius: Theme.cornerRadius
clip: true clip: true
@@ -65,23 +73,28 @@ Rectangle {
} }
Item { 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.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 12 anchors.topMargin: cardPadding
anchors.leftMargin: 16 anchors.leftMargin: Theme.spacingL
anchors.rightMargin: 56 anchors.rightMargin: Theme.spacingL + (compactMode ? 32 : 40)
height: 92 height: collapsedContentHeight + extraHeight
DankCircularImage { DankCircularImage {
id: iconContainer id: iconContainer
readonly property bool hasNotificationImage: historyItem.image && historyItem.image !== "" readonly property bool hasNotificationImage: historyItem.image && historyItem.image !== ""
width: 63 width: iconSize
height: 63 height: iconSize
anchors.left: parent.left anchors.left: parent.left
anchors.top: parent.top anchors.verticalCenter: parent.verticalCenter
anchors.topMargin: 14
imageSource: { imageSource: {
if (hasNotificationImage) if (hasNotificationImage)
@@ -116,60 +129,78 @@ Rectangle {
Rectangle { Rectangle {
anchors.left: iconContainer.right anchors.left: iconContainer.right
anchors.leftMargin: 12 anchors.leftMargin: Theme.spacingM
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top anchors.top: parent.top
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.bottomMargin: 8 anchors.bottomMargin: contentSpacing
color: "transparent" color: "transparent"
Item { Column {
width: parent.width width: parent.width
height: parent.height anchors.verticalCenter: parent.verticalCenter
anchors.top: parent.top spacing: compactMode ? 1 : 2
anchors.topMargin: -2
Column { StyledText {
width: parent.width width: parent.width
spacing: 2 text: {
const timeStr = NotificationService.formatHistoryTime(historyItem.timestamp);
const appName = historyItem.appName || "";
return timeStr.length > 0 ? `${appName} ${timeStr}` : appName;
}
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
elide: Text.ElideRight
maximumLineCount: 1
}
StyledText { StyledText {
width: parent.width text: historyItem.summary || ""
text: { color: Theme.surfaceText
const timeStr = NotificationService.formatHistoryTime(historyItem.timestamp); font.pixelSize: Theme.fontSizeMedium
const appName = historyItem.appName || ""; font.weight: Font.Medium
return timeStr.length > 0 ? `${appName} ${timeStr}` : appName; width: parent.width
elide: Text.ElideRight
maximumLineCount: 1
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);
}
} }
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
elide: Text.ElideRight
maximumLineCount: 1
}
StyledText { propagateComposedEvents: true
text: historyItem.summary || "" onPressed: mouse => {
color: Theme.surfaceText if (parent.hoveredLink)
font.pixelSize: Theme.fontSizeMedium mouse.accepted = false;
font.weight: Font.Medium }
width: parent.width onReleased: mouse => {
elide: Text.ElideRight if (parent.hoveredLink)
maximumLineCount: 1 mouse.accepted = false;
visible: text.length > 0 }
}
StyledText {
id: descriptionText
text: historyItem.htmlBody || historyItem.body || ""
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
width: parent.width
elide: Text.ElideRight
maximumLineCount: 2
wrapMode: Text.WordWrap
visible: text.length > 0
linkColor: Theme.primary
onLinkActivated: link => Qt.openUrlExternally(link)
} }
} }
} }
@@ -179,11 +210,11 @@ Rectangle {
DankActionButton { DankActionButton {
anchors.top: parent.top anchors.top: parent.top
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 12 anchors.topMargin: cardPadding
anchors.rightMargin: 16 anchors.rightMargin: Theme.spacingL
iconName: "close" iconName: "close"
iconSize: 18 iconSize: compactMode ? 16 : 18
buttonSize: 28 buttonSize: compactMode ? 24 : 28
onClicked: NotificationService.removeFromHistory(historyItem.id) onClicked: NotificationService.removeFromHistory(historyItem.id)
} }
} }

View File

@@ -18,17 +18,17 @@ Rectangle {
property int selectedNotificationIndex: -1 property int selectedNotificationIndex: -1
property bool keyboardNavigationActive: false property bool keyboardNavigationActive: false
readonly property bool compactMode: SettingsData.notificationCompactMode
readonly property real cardPadding: compactMode ? Theme.spacingS : Theme.spacingM
readonly property real iconSize: compactMode ? 48 : 63
readonly property real contentSpacing: compactMode ? Theme.spacingXS : Theme.spacingS
readonly property real badgeSize: compactMode ? 16 : 18
readonly property real actionButtonHeight: compactMode ? 20 : 24
readonly property real baseCardHeight: cardPadding * 2 + collapsedContentHeight
readonly property real collapsedContentHeight: iconSize + cardPadding
width: parent ? parent.width : 400 width: parent ? parent.width : 400
height: { height: expanded ? (expandedContent.height + cardPadding * 2) : (baseCardHeight + collapsedContent.extraHeight)
if (expanded) {
return expandedContent.height + 28;
}
const baseHeight = 116;
if (descriptionExpanded) {
return baseHeight + descriptionText.contentHeight - (descriptionText.font.pixelSize * 1.2 * 2);
}
return baseHeight;
}
radius: Theme.cornerRadius radius: Theme.cornerRadius
Behavior on border.color { Behavior on border.color {
@@ -97,24 +97,28 @@ Rectangle {
Item { Item {
id: collapsedContent id: collapsedContent
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.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 12 anchors.topMargin: cardPadding
anchors.leftMargin: 16 anchors.leftMargin: Theme.spacingL
anchors.rightMargin: 56 anchors.rightMargin: Theme.spacingL + (compactMode ? 32 : 40)
height: 92 height: collapsedContentHeight + extraHeight
visible: !expanded visible: !expanded
DankCircularImage { DankCircularImage {
id: iconContainer id: iconContainer
readonly property bool hasNotificationImage: notificationGroup?.latestNotification?.image && notificationGroup.latestNotification.image !== "" readonly property bool hasNotificationImage: notificationGroup?.latestNotification?.image && notificationGroup.latestNotification.image !== ""
width: 63 width: iconSize
height: 63 height: iconSize
anchors.left: parent.left anchors.left: parent.left
anchors.top: parent.top anchors.verticalCenter: parent.verticalCenter
anchors.topMargin: 14
imageSource: { imageSource: {
if (hasNotificationImage) if (hasNotificationImage)
@@ -147,9 +151,9 @@ Rectangle {
} }
Rectangle { Rectangle {
width: 18 width: badgeSize
height: 18 height: badgeSize
radius: 9 radius: badgeSize / 2
color: Theme.primary color: Theme.primary
anchors.top: parent.top anchors.top: parent.top
anchors.right: parent.right anchors.right: parent.right
@@ -161,7 +165,7 @@ Rectangle {
anchors.centerIn: parent anchors.centerIn: parent
text: (notificationGroup?.count || 0) > 99 ? "99+" : (notificationGroup?.count || 0).toString() text: (notificationGroup?.count || 0) > 99 ? "99+" : (notificationGroup?.count || 0).toString()
color: Theme.primaryText color: Theme.primaryText
font.pixelSize: 9 font.pixelSize: compactMode ? 8 : 9
font.weight: Font.Bold font.weight: Font.Bold
} }
} }
@@ -171,87 +175,78 @@ Rectangle {
id: textContainer id: textContainer
anchors.left: iconContainer.right anchors.left: iconContainer.right
anchors.leftMargin: 12 anchors.leftMargin: Theme.spacingM
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 0
anchors.top: parent.top anchors.top: parent.top
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.bottomMargin: 8 anchors.bottomMargin: contentSpacing
color: "transparent" color: "transparent"
Item { Column {
width: parent.width width: parent.width
height: parent.height anchors.verticalCenter: parent.verticalCenter
anchors.top: parent.top spacing: compactMode ? 1 : 2
anchors.topMargin: -2
Column { StyledText {
width: parent.width width: parent.width
spacing: 2 text: {
const timeStr = (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.timeStr) || "";
const appName = (notificationGroup && notificationGroup.appName) || "";
return timeStr.length > 0 ? `${appName} ${timeStr}` : appName;
}
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
elide: Text.ElideRight
maximumLineCount: 1
}
StyledText { StyledText {
width: parent.width text: (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.summary) || ""
text: { color: Theme.surfaceText
const timeStr = (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.timeStr) || ""; font.pixelSize: Theme.fontSizeMedium
const appName = (notificationGroup && notificationGroup.appName) || ""; font.weight: Font.Medium
return timeStr.length > 0 ? `${appName} ${timeStr}` : appName; width: parent.width
elide: Text.ElideRight
maximumLineCount: 1
visible: text.length > 0
}
StyledText {
id: descriptionText
property string fullText: (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.htmlBody) || ""
property bool hasMoreText: truncated
text: fullText
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
width: parent.width
elide: 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 = (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.notification && notificationGroup.latestNotification.notification.id) ? (notificationGroup.latestNotification.notification.id + "_desc") : "";
NotificationService.toggleMessageExpansion(messageId);
}
} }
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
elide: Text.ElideRight
maximumLineCount: 1
}
StyledText { propagateComposedEvents: true
text: (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.summary) || "" onPressed: mouse => {
color: Theme.surfaceText if (parent.hoveredLink)
font.pixelSize: Theme.fontSizeMedium mouse.accepted = false;
font.weight: Font.Medium }
width: parent.width onReleased: mouse => {
elide: Text.ElideRight if (parent.hoveredLink)
maximumLineCount: 1 mouse.accepted = false;
visible: text.length > 0
}
StyledText {
id: descriptionText
property string fullText: (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.htmlBody) || ""
property bool hasMoreText: truncated
text: fullText
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
width: parent.width
elide: Text.ElideRight
maximumLineCount: descriptionExpanded ? -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 = (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.notification && notificationGroup.latestNotification.notification.id) ? (notificationGroup.latestNotification.notification.id + "_desc") : "";
NotificationService.toggleMessageExpansion(messageId);
}
}
propagateComposedEvents: true
onPressed: mouse => {
if (parent.hoveredLink) {
mouse.accepted = false;
}
}
onReleased: mouse => {
if (parent.hoveredLink) {
mouse.accepted = false;
}
}
} }
} }
} }
@@ -265,21 +260,20 @@ Rectangle {
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 14 anchors.topMargin: cardPadding
anchors.bottomMargin: 14
anchors.leftMargin: Theme.spacingL anchors.leftMargin: Theme.spacingL
anchors.rightMargin: Theme.spacingL anchors.rightMargin: Theme.spacingL
spacing: -1 spacing: compactMode ? Theme.spacingXS : Theme.spacingS
visible: expanded visible: expanded
Item { Item {
width: parent.width width: parent.width
height: 40 height: compactMode ? 32 : 40
Row { Row {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 56 anchors.rightMargin: Theme.spacingL + (compactMode ? 32 : 40)
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS spacing: Theme.spacingS
@@ -294,9 +288,9 @@ Rectangle {
} }
Rectangle { Rectangle {
width: 18 width: badgeSize
height: 18 height: badgeSize
radius: 9 radius: badgeSize / 2
color: Theme.primary color: Theme.primary
visible: (notificationGroup?.count || 0) > 1 visible: (notificationGroup?.count || 0) > 1
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -305,7 +299,7 @@ Rectangle {
anchors.centerIn: parent anchors.centerIn: parent
text: (notificationGroup?.count || 0) > 99 ? "99+" : (notificationGroup?.count || 0).toString() text: (notificationGroup?.count || 0) > 99 ? "99+" : (notificationGroup?.count || 0).toString()
color: Theme.primaryText color: Theme.primaryText
font.pixelSize: 9 font.pixelSize: compactMode ? 8 : 9
font.weight: Font.Bold font.weight: Font.Bold
} }
} }
@@ -314,7 +308,7 @@ Rectangle {
Column { Column {
width: parent.width width: parent.width
spacing: 16 spacing: compactMode ? Theme.spacingS : Theme.spacingL
Repeater { Repeater {
id: notificationRepeater id: notificationRepeater
@@ -328,23 +322,23 @@ Rectangle {
required property int index required property int index
readonly property bool messageExpanded: NotificationService.expandedMessages[modelData?.notification?.id] || false readonly property bool messageExpanded: NotificationService.expandedMessages[modelData?.notification?.id] || false
readonly property bool isSelected: root.selectedNotificationIndex === index readonly property bool isSelected: root.selectedNotificationIndex === index
readonly property real expandedIconSize: compactMode ? 40 : 48
readonly property real expandedItemPadding: compactMode ? Theme.spacingS : Theme.spacingM
readonly property real expandedBaseHeight: expandedItemPadding * 2 + expandedIconSize + actionButtonHeight + contentSpacing * 2
width: parent.width width: parent.width
height: { height: {
const baseHeight = 120; if (!messageExpanded)
if (messageExpanded) { return expandedBaseHeight;
const twoLineHeight = bodyText.font.pixelSize * 1.2 * 2; const twoLineHeight = bodyText.font.pixelSize * 1.2 * 2;
if (bodyText.implicitHeight > twoLineHeight + 2) { if (bodyText.implicitHeight > twoLineHeight + 2)
const extraHeight = bodyText.implicitHeight - twoLineHeight; return expandedBaseHeight + bodyText.implicitHeight - twoLineHeight;
return baseHeight + extraHeight; return expandedBaseHeight;
}
}
return baseHeight;
} }
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: isSelected ? Theme.primaryPressed : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) color: isSelected ? Theme.primaryPressed : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: isSelected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4) : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05) border.color: isSelected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4) : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05)
border.width: isSelected ? 1 : 1 border.width: 1
Behavior on border.color { Behavior on border.color {
ColorAnimation { ColorAnimation {
@@ -359,19 +353,19 @@ Rectangle {
Item { Item {
anchors.fill: parent anchors.fill: parent
anchors.margins: 12 anchors.margins: compactMode ? Theme.spacingS : Theme.spacingM
anchors.bottomMargin: 8 anchors.bottomMargin: contentSpacing
DankCircularImage { DankCircularImage {
id: messageIcon id: messageIcon
readonly property bool hasNotificationImage: modelData?.image && modelData.image !== "" readonly property bool hasNotificationImage: modelData?.image && modelData.image !== ""
width: 48 width: expandedIconSize
height: 48 height: expandedIconSize
anchors.left: parent.left anchors.left: parent.left
anchors.top: parent.top anchors.top: parent.top
anchors.topMargin: 32 anchors.topMargin: compactMode ? Theme.spacingM : Theme.spacingXL
imageSource: { imageSource: {
if (hasNotificationImage) if (hasNotificationImage)
@@ -397,9 +391,9 @@ Rectangle {
Item { Item {
anchors.left: messageIcon.right anchors.left: messageIcon.right
anchors.leftMargin: 12 anchors.leftMargin: Theme.spacingM
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 12 anchors.rightMargin: Theme.spacingM
anchors.top: parent.top anchors.top: parent.top
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
@@ -408,8 +402,8 @@ Rectangle {
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top anchors.top: parent.top
anchors.bottom: buttonArea.top anchors.bottom: buttonArea.top
anchors.bottomMargin: 4 anchors.bottomMargin: contentSpacing
spacing: 2 spacing: compactMode ? 1 : 2
StyledText { StyledText {
width: parent.width width: parent.width
@@ -477,12 +471,12 @@ Rectangle {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
height: 30 height: actionButtonHeight + contentSpacing
Row { Row {
anchors.right: parent.right anchors.right: parent.right
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
spacing: 8 spacing: contentSpacing
Repeater { Repeater {
model: modelData?.actions || [] model: modelData?.actions || []
@@ -490,18 +484,17 @@ Rectangle {
Rectangle { Rectangle {
property bool isHovered: false property bool isHovered: false
width: Math.max(actionText.implicitWidth + 12, 50) width: Math.max(actionText.implicitWidth + Theme.spacingM, compactMode ? 40 : 50)
height: 24 height: actionButtonHeight
radius: 4 radius: Theme.spacingXS
color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent" color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent"
StyledText { StyledText {
id: actionText id: actionText
text: { text: {
const baseText = modelData.text || "View"; const baseText = modelData.text || "View";
if (keyboardNavigationActive && (isGroupSelected || selectedNotificationIndex >= 0)) { if (keyboardNavigationActive && (isGroupSelected || selectedNotificationIndex >= 0))
return `${baseText} (${index + 1})`; return `${baseText} (${index + 1})`;
}
return baseText; return baseText;
} }
color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText
@@ -518,9 +511,8 @@ Rectangle {
onEntered: parent.isHovered = true onEntered: parent.isHovered = true
onExited: parent.isHovered = false onExited: parent.isHovered = false
onClicked: { onClicked: {
if (modelData && modelData.invoke) { if (modelData && modelData.invoke)
modelData.invoke(); modelData.invoke();
}
} }
} }
} }
@@ -529,9 +521,9 @@ Rectangle {
Rectangle { Rectangle {
property bool isHovered: false property bool isHovered: false
width: Math.max(clearText.implicitWidth + 12, 50) width: Math.max(clearText.implicitWidth + Theme.spacingM, compactMode ? 40 : 50)
height: 24 height: actionButtonHeight
radius: 4 radius: Theme.spacingXS
color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent" color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent"
StyledText { StyledText {
@@ -564,10 +556,10 @@ Rectangle {
Row { Row {
visible: !expanded visible: !expanded
anchors.right: clearButton.left anchors.right: clearButton.left
anchors.rightMargin: 8 anchors.rightMargin: contentSpacing
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.bottomMargin: 8 anchors.bottomMargin: contentSpacing
spacing: 8 spacing: contentSpacing
Repeater { Repeater {
model: notificationGroup?.latestNotification?.actions || [] model: notificationGroup?.latestNotification?.actions || []
@@ -575,9 +567,9 @@ Rectangle {
Rectangle { Rectangle {
property bool isHovered: false property bool isHovered: false
width: Math.max(actionText.implicitWidth + 12, 50) width: Math.max(actionText.implicitWidth + Theme.spacingM, compactMode ? 40 : 50)
height: 24 height: actionButtonHeight
radius: 4 radius: Theme.spacingXS
color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent" color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent"
StyledText { StyledText {
@@ -619,12 +611,12 @@ Rectangle {
visible: !expanded visible: !expanded
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 16 anchors.rightMargin: Theme.spacingL
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.bottomMargin: 8 anchors.bottomMargin: contentSpacing
width: Math.max(clearText.implicitWidth + 12, 50) width: Math.max(clearText.implicitWidth + Theme.spacingM, compactMode ? 40 : 50)
height: 24 height: actionButtonHeight
radius: 4 radius: Theme.spacingXS
color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent" color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent"
StyledText { StyledText {
@@ -660,18 +652,18 @@ Rectangle {
id: fixedControls id: fixedControls
anchors.top: parent.top anchors.top: parent.top
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 12 anchors.topMargin: cardPadding
anchors.rightMargin: 16 anchors.rightMargin: Theme.spacingL
width: 60 width: compactMode ? 52 : 60
height: 28 height: compactMode ? 24 : 28
DankActionButton { DankActionButton {
anchors.left: parent.left anchors.left: parent.left
anchors.top: parent.top anchors.top: parent.top
visible: (notificationGroup?.count || 0) > 1 visible: (notificationGroup?.count || 0) > 1
iconName: expanded ? "expand_less" : "expand_more" iconName: expanded ? "expand_less" : "expand_more"
iconSize: 18 iconSize: compactMode ? 16 : 18
buttonSize: 28 buttonSize: compactMode ? 24 : 28
onClicked: { onClicked: {
root.userInitiatedExpansion = true; root.userInitiatedExpansion = true;
NotificationService.toggleGroupExpansion(notificationGroup?.key || ""); NotificationService.toggleGroupExpansion(notificationGroup?.key || "");
@@ -682,8 +674,8 @@ Rectangle {
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top anchors.top: parent.top
iconName: "close" iconName: "close"
iconSize: 18 iconSize: compactMode ? 16 : 18
buttonSize: 28 buttonSize: compactMode ? 24 : 28
onClicked: NotificationService.dismissGroup(notificationGroup?.key || "") onClicked: NotificationService.dismissGroup(notificationGroup?.key || "")
} }
} }

View File

@@ -22,6 +22,15 @@ PanelWindow {
property bool _isDestroying: false property bool _isDestroying: false
property bool _finalized: false property bool _finalized: false
readonly property string clearText: I18n.tr("Dismiss") readonly property string clearText: I18n.tr("Dismiss")
property bool descriptionExpanded: false
readonly property bool compactMode: SettingsData.notificationCompactMode
readonly property real cardPadding: compactMode ? Theme.spacingS : Theme.spacingM
readonly property real popupIconSize: compactMode ? 48 : 63
readonly property real contentSpacing: compactMode ? Theme.spacingXS : Theme.spacingS
readonly property real actionButtonHeight: compactMode ? 20 : 24
readonly property real collapsedContentHeight: popupIconSize + cardPadding
readonly property real basePopupHeight: cardPadding * 2 + collapsedContentHeight + Theme.spacingS
signal entered signal entered
signal exitStarted signal exitStarted
@@ -92,7 +101,15 @@ PanelWindow {
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent" color: "transparent"
implicitWidth: 400 implicitWidth: 400
implicitHeight: 122 implicitHeight: {
if (!descriptionExpanded)
return basePopupHeight;
const bodyTextHeight = bodyTextLoader.item?.contentHeight || 0;
const twoLineHeight = Theme.fontSizeSmall * 1.2 * 2;
if (bodyTextHeight > twoLineHeight + 2)
return basePopupHeight + bodyTextHeight - twoLineHeight;
return basePopupHeight;
}
onHasValidDataChanged: { onHasValidDataChanged: {
if (!hasValidData && !exiting && !_isDestroying) { if (!hasValidData && !exiting && !_isDestroying) {
forceExit(); forceExit();
@@ -352,13 +369,17 @@ PanelWindow {
Item { Item {
id: notificationContent id: notificationContent
readonly property real expandedTextHeight: bodyTextLoader.item?.contentHeight || 0
readonly property real twoLineHeight: Theme.fontSizeSmall * 1.2 * 2
readonly property real extraHeight: (descriptionExpanded && expandedTextHeight > twoLineHeight + 2) ? (expandedTextHeight - twoLineHeight) : 0
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 12 anchors.topMargin: cardPadding
anchors.leftMargin: 16 anchors.leftMargin: Theme.spacingL
anchors.rightMargin: 56 anchors.rightMargin: Theme.spacingL + (compactMode ? 32 : 40)
height: 98 height: collapsedContentHeight + extraHeight
DankCircularImage { DankCircularImage {
id: iconContainer id: iconContainer
@@ -366,8 +387,8 @@ PanelWindow {
readonly property bool hasNotificationImage: notificationData && notificationData.image && notificationData.image !== "" readonly property bool hasNotificationImage: notificationData && notificationData.image && notificationData.image !== ""
readonly property bool needsImagePersist: hasNotificationImage && notificationData.image.startsWith("image://qsimage/") && !notificationData.persistedImagePath readonly property bool needsImagePersist: hasNotificationImage && notificationData.image.startsWith("image://qsimage/") && !notificationData.persistedImagePath
width: 63 width: popupIconSize
height: 63 height: popupIconSize
anchors.left: parent.left anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -416,76 +437,85 @@ PanelWindow {
id: textContainer id: textContainer
anchors.left: iconContainer.right anchors.left: iconContainer.right
anchors.leftMargin: 12 anchors.leftMargin: Theme.spacingM
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 0
anchors.top: parent.top anchors.top: parent.top
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.bottomMargin: 8 anchors.bottomMargin: contentSpacing
color: "transparent" color: "transparent"
Item { Column {
width: parent.width width: parent.width
height: parent.height anchors.verticalCenter: parent.verticalCenter
anchors.top: parent.top spacing: compactMode ? 1 : 2
anchors.topMargin: -2
Column { StyledText {
width: parent.width width: parent.width
spacing: 2 text: {
if (!notificationData)
StyledText { return "";
width: parent.width const appName = notificationData.appName || "";
text: { const timeStr = notificationData.timeStr || "";
if (!notificationData) return timeStr.length > 0 ? appName + " • " + timeStr : appName;
return "";
const appName = notificationData.appName || "";
const timeStr = notificationData.timeStr || "";
if (timeStr.length > 0)
return appName + " • " + timeStr;
else
return appName;
}
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
elide: Text.ElideRight
horizontalAlignment: Text.AlignLeft
maximumLineCount: 1
} }
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
elide: Text.ElideRight
horizontalAlignment: Text.AlignLeft
maximumLineCount: 1
}
StyledText { StyledText {
text: notificationData ? (notificationData.summary || "") : "" text: notificationData ? (notificationData.summary || "") : ""
color: Theme.surfaceText color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium font.weight: Font.Medium
width: parent.width width: parent.width
elide: Text.ElideRight elide: Text.ElideRight
horizontalAlignment: Text.AlignLeft horizontalAlignment: Text.AlignLeft
maximumLineCount: 1 maximumLineCount: 1
visible: text.length > 0 visible: text.length > 0
} }
Loader {
id: bodyTextLoader
width: parent.width
active: notificationData && (notificationData.htmlBody || "").length > 0
sourceComponent: StyledText {
id: bodyText
property bool hasMoreText: truncated
StyledText {
text: notificationData ? (notificationData.htmlBody || "") : "" text: notificationData ? (notificationData.htmlBody || "") : ""
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
width: parent.width width: parent.width
elide: Text.ElideRight elide: descriptionExpanded ? Text.ElideNone : Text.ElideRight
horizontalAlignment: Text.AlignLeft horizontalAlignment: Text.AlignLeft
maximumLineCount: 2 maximumLineCount: descriptionExpanded ? -1 : (compactMode ? 1 : 2)
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
visible: text.length > 0
linkColor: Theme.primary linkColor: Theme.primary
onLinkActivated: link => { onLinkActivated: link => Qt.openUrlExternally(link)
return Qt.openUrlExternally(link);
}
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
acceptedButtons: Qt.NoButton cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : (parent.hasMoreText || descriptionExpanded) ? Qt.PointingHandCursor : Qt.ArrowCursor
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: mouse => {
if (!parent.hoveredLink && (parent.hasMoreText || descriptionExpanded))
win.descriptionExpanded = !win.descriptionExpanded;
}
propagateComposedEvents: true
onPressed: mouse => {
if (parent.hoveredLink)
mouse.accepted = false;
}
onReleased: mouse => {
if (parent.hoveredLink)
mouse.accepted = false;
}
} }
} }
} }
@@ -498,11 +528,11 @@ PanelWindow {
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top anchors.top: parent.top
anchors.topMargin: 12 anchors.topMargin: cardPadding
anchors.rightMargin: 16 anchors.rightMargin: Theme.spacingL
iconName: "close" iconName: "close"
iconSize: 18 iconSize: compactMode ? 16 : 18
buttonSize: 28 buttonSize: compactMode ? 24 : 28
z: 15 z: 15
onClicked: { onClicked: {
if (notificationData && !win.exiting) if (notificationData && !win.exiting)
@@ -512,10 +542,10 @@ PanelWindow {
Row { Row {
anchors.right: clearButton.left anchors.right: clearButton.left
anchors.rightMargin: 8 anchors.rightMargin: contentSpacing
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.bottomMargin: 8 anchors.bottomMargin: contentSpacing
spacing: 8 spacing: contentSpacing
z: 20 z: 20
Repeater { Repeater {
@@ -524,9 +554,9 @@ PanelWindow {
Rectangle { Rectangle {
property bool isHovered: false property bool isHovered: false
width: Math.max(actionText.implicitWidth + 12, 50) width: Math.max(actionText.implicitWidth + Theme.spacingM, compactMode ? 40 : 50)
height: 24 height: actionButtonHeight
radius: 4 radius: Theme.spacingXS
color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent" color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent"
StyledText { StyledText {
@@ -550,7 +580,6 @@ PanelWindow {
onClicked: { onClicked: {
if (modelData && modelData.invoke) if (modelData && modelData.invoke)
modelData.invoke(); modelData.invoke();
if (notificationData && !win.exiting) if (notificationData && !win.exiting)
notificationData.popup = false; notificationData.popup = false;
} }
@@ -565,12 +594,12 @@ PanelWindow {
property bool isHovered: false property bool isHovered: false
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 16 anchors.rightMargin: Theme.spacingL
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.bottomMargin: 8 anchors.bottomMargin: contentSpacing
width: Math.max(clearTextLabel.implicitWidth + 12, 50) width: Math.max(clearTextLabel.implicitWidth + Theme.spacingM, compactMode ? 40 : 50)
height: 24 height: actionButtonHeight
radius: 4 radius: Theme.spacingXS
color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent" color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent"
z: 20 z: 20

View File

@@ -1,4 +1,5 @@
import QtQuick import QtQuick
import qs.Common
import qs.Services import qs.Services
QtObject { QtObject {
@@ -6,7 +7,11 @@ QtObject {
property var modelData property var modelData
property int topMargin: 0 property int topMargin: 0
property int baseNotificationHeight: 120 readonly property bool compactMode: SettingsData.notificationCompactMode
readonly property real cardPadding: compactMode ? Theme.spacingS : Theme.spacingM
readonly property real popupIconSize: compactMode ? 48 : 63
readonly property real popupSpacing: Theme.spacingS
readonly property int baseNotificationHeight: cardPadding * 3 + popupIconSize + popupSpacing
property int maxTargetNotifications: 4 property int maxTargetNotifications: 4
property var popupWindows: [] // strong refs to windows (live until exitFinished) property var popupWindows: [] // strong refs to windows (live until exitFinished)
property var destroyingWindows: new Set() property var destroyingWindows: new Set()

View File

@@ -144,6 +144,15 @@ Item {
checked: SettingsData.notificationOverlayEnabled checked: SettingsData.notificationOverlayEnabled
onToggled: checked => SettingsData.set("notificationOverlayEnabled", checked) onToggled: checked => SettingsData.set("notificationOverlayEnabled", checked)
} }
SettingsToggleRow {
settingKey: "notificationCompactMode"
tags: ["notification", "compact", "size", "display", "mode"]
text: I18n.tr("Compact")
description: I18n.tr("Use smaller notification cards")
checked: SettingsData.notificationCompactMode
onToggled: checked => SettingsData.set("notificationCompactMode", checked)
}
} }
SettingsCard { SettingsCard {