mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-04-03 20:32:07 -04:00
Squashed commit of the following:
commit051b7576f7Author: purian23 <purian23@gmail.com> Date: Sun Feb 15 16:38:45 2026 -0500 Height for realz commit7784488a61Author: purian23 <purian23@gmail.com> Date: Sun Feb 15 16:34:09 2026 -0500 Fix height and truncate text/URLs commit31b328d428Author: bbedward <bbedward@gmail.com> Date: Sun Feb 15 16:25:57 2026 -0500 notifications: handle URL encoding in markdown2html commitdbb04f74a2Author: bbedward <bbedward@gmail.com> Date: Sun Feb 15 16:10:20 2026 -0500 notifications: more comprehensive decoder commitb29c7192c2Author: bbedward <bbedward@gmail.com> Date: Sun Feb 15 15:51:37 2026 -0500 notifications: html unescape commit8a48fa11ecAuthor: purian23 <purian23@gmail.com> Date: Sun Feb 15 15:04:33 2026 -0500 Add expressive curve on init toast commitee124f5e04Author: purian23 <purian23@gmail.com> Date: Sun Feb 15 15:02:16 2026 -0500 Expressive curves on swipe & btn height commit0fce904635Author: purian23 <purian23@gmail.com> Date: Sun Feb 15 13:40:02 2026 -0500 Provide bottom button clearance commit00d3829999Author: bbedward <bbedward@gmail.com> Date: Sun Feb 15 13:24:31 2026 -0500 notifications: cleanup popup display logic commitfd05768059Author: purian23 <purian23@gmail.com> Date: Sun Feb 15 01:00:55 2026 -0500 Add Privacy Mode - Smoother notification expansions - Shadow & Privacy Toggles commit0dba11d845Author: purian23 <purian23@gmail.com> Date: Sat Feb 14 22:48:46 2026 -0500 Further M3 enhancements commit949c216964Author: purian23 <purian23@gmail.com> Date: Sat Feb 14 19:59:38 2026 -0500 Right-Click to set Rules on Notifications directly commit62bc25782cAuthor: bbedward <bbedward@gmail.com> Date: Fri Feb 13 21:44:27 2026 -0500 notifications: fix compact spacing, reveal header bar, add bottom center position, pointing hand cursor fix commited495d4396Author: purian23 <purian23@gmail.com> Date: Fri Feb 13 20:25:40 2026 -0500 Tighten init toast commitebe38322a0Author: purian23 <purian23@gmail.com> Date: Fri Feb 13 20:09:59 2026 -0500 Update more m3 baselines & spacing commitb1735bb701Author: purian23 <purian23@gmail.com> Date: Fri Feb 13 14:10:05 2026 -0500 Expand rules on-Click commit9f13546b4dAuthor: purian23 <purian23@gmail.com> Date: Fri Feb 13 12:59:29 2026 -0500 Add Notification Rules - Additional right-click ops - Allow for 3rd boy line on init notification popup commitbe133b73c7Author: purian23 <purian23@gmail.com> Date: Fri Feb 13 10:10:03 2026 -0500 Truncate long title in groups commit4fc275beadAuthor: bbedward <bbedward@gmail.com> Date: Thu Feb 12 23:27:34 2026 -0500 notification: expand/collapse animation adjustment commit00e6172a68Author: purian23 <purian23@gmail.com> Date: Thu Feb 12 22:50:11 2026 -0500 Fix global warnings commit0772f6deb7Author: purian23 <purian23@gmail.com> Date: Thu Feb 12 22:46:40 2026 -0500 Tweak expansion duration commit0ffeed3ff0Author: purian23 <purian23@gmail.com> Date: Thu Feb 12 22:16:16 2026 -0500 notifications: Update Material 3 baselines - New right-click to mute option - New independent Notification Animation settings
This commit is contained in:
@@ -15,6 +15,9 @@ const (
|
||||
notifyDest = "org.freedesktop.Notifications"
|
||||
notifyPath = "/org/freedesktop/Notifications"
|
||||
notifyInterface = "org.freedesktop.Notifications"
|
||||
|
||||
maxSummaryLen = 29
|
||||
maxBodyLen = 80
|
||||
)
|
||||
|
||||
type Notification struct {
|
||||
@@ -39,6 +42,13 @@ func Send(n Notification) error {
|
||||
n.Timeout = 5000
|
||||
}
|
||||
|
||||
if len(n.Summary) > maxSummaryLen {
|
||||
n.Summary = n.Summary[:maxSummaryLen-3] + "..."
|
||||
}
|
||||
if len(n.Body) > maxBodyLen {
|
||||
n.Body = n.Body[:maxBodyLen-3] + "..."
|
||||
}
|
||||
|
||||
var actions []string
|
||||
if n.FilePath != "" {
|
||||
actions = []string{
|
||||
|
||||
@@ -472,6 +472,8 @@ Singleton {
|
||||
property bool dockShowOverflowBadge: true
|
||||
|
||||
property bool notificationOverlayEnabled: false
|
||||
property bool notificationPopupShadowEnabled: true
|
||||
property bool notificationPopupPrivacyMode: false
|
||||
property int overviewRows: 2
|
||||
property int overviewColumns: 5
|
||||
property real overviewScale: 0.16
|
||||
@@ -501,6 +503,8 @@ Singleton {
|
||||
property int notificationTimeoutCritical: 0
|
||||
property bool notificationCompactMode: false
|
||||
property int notificationPopupPosition: SettingsData.Position.Top
|
||||
property int notificationAnimationSpeed: SettingsData.AnimationSpeed.Short
|
||||
property int notificationCustomAnimationDuration: 400
|
||||
property bool notificationHistoryEnabled: true
|
||||
property int notificationHistoryMaxCount: 50
|
||||
property int notificationHistoryMaxAgeDays: 7
|
||||
@@ -2138,6 +2142,9 @@ Singleton {
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
property bool _pendingExpandNotificationRules: false
|
||||
property int _pendingNotificationRuleIndex: -1
|
||||
|
||||
function addNotificationRule() {
|
||||
var rules = JSON.parse(JSON.stringify(notificationRules || []));
|
||||
rules.push({
|
||||
@@ -2145,6 +2152,45 @@ Singleton {
|
||||
field: "appName",
|
||||
pattern: "",
|
||||
matchType: "contains",
|
||||
action: "default",
|
||||
urgency: "default"
|
||||
});
|
||||
notificationRules = rules;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function addNotificationRuleForNotification(appName, desktopEntry) {
|
||||
var rules = JSON.parse(JSON.stringify(notificationRules || []));
|
||||
var pattern = (desktopEntry && desktopEntry !== "") ? desktopEntry : (appName || "");
|
||||
var field = (desktopEntry && desktopEntry !== "") ? "desktopEntry" : "appName";
|
||||
var rule = {
|
||||
enabled: true,
|
||||
field: pattern ? field : "appName",
|
||||
pattern: pattern || "",
|
||||
matchType: pattern ? "exact" : "contains",
|
||||
action: "default",
|
||||
urgency: "default"
|
||||
};
|
||||
rules.push(rule);
|
||||
notificationRules = rules;
|
||||
saveSettings();
|
||||
var index = rules.length - 1;
|
||||
_pendingExpandNotificationRules = true;
|
||||
_pendingNotificationRuleIndex = index;
|
||||
return index;
|
||||
}
|
||||
|
||||
function addMuteRuleForApp(appName, desktopEntry) {
|
||||
var rules = JSON.parse(JSON.stringify(notificationRules || []));
|
||||
var pattern = (desktopEntry && desktopEntry !== "") ? desktopEntry : (appName || "");
|
||||
var field = (desktopEntry && desktopEntry !== "") ? "desktopEntry" : "appName";
|
||||
if (pattern === "")
|
||||
return;
|
||||
rules.push({
|
||||
enabled: true,
|
||||
field: field,
|
||||
pattern: pattern,
|
||||
matchType: "exact",
|
||||
action: "mute",
|
||||
urgency: "default"
|
||||
});
|
||||
@@ -2152,6 +2198,51 @@ Singleton {
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function isAppMuted(appName, desktopEntry) {
|
||||
const rules = notificationRules || [];
|
||||
const pat = (desktopEntry && desktopEntry !== "" ? desktopEntry : appName || "").toString().toLowerCase();
|
||||
if (!pat)
|
||||
return false;
|
||||
for (let i = 0; i < rules.length; i++) {
|
||||
const r = rules[i];
|
||||
if ((r.action || "").toString().toLowerCase() !== "mute" || r.enabled === false)
|
||||
continue;
|
||||
const field = (r.field || "appName").toString().toLowerCase();
|
||||
const rulePat = (r.pattern || "").toString().toLowerCase();
|
||||
if (!rulePat)
|
||||
continue;
|
||||
const useDesktop = field === "desktopentry";
|
||||
const matches = (useDesktop && desktopEntry) ? (desktopEntry.toString().toLowerCase() === rulePat) : (appName && appName.toString().toLowerCase() === rulePat);
|
||||
if (matches)
|
||||
return true;
|
||||
if (rulePat === pat)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function removeMuteRuleForApp(appName, desktopEntry) {
|
||||
var rules = JSON.parse(JSON.stringify(notificationRules || []));
|
||||
const app = (appName || "").toString().toLowerCase();
|
||||
const desktop = (desktopEntry || "").toString().toLowerCase();
|
||||
if (!app && !desktop)
|
||||
return;
|
||||
for (let i = rules.length - 1; i >= 0; i--) {
|
||||
const r = rules[i];
|
||||
if ((r.action || "").toString().toLowerCase() !== "mute")
|
||||
continue;
|
||||
const rulePat = (r.pattern || "").toString().toLowerCase();
|
||||
if (!rulePat)
|
||||
continue;
|
||||
if (rulePat === app || rulePat === desktop) {
|
||||
rules.splice(i, 1);
|
||||
notificationRules = rules;
|
||||
saveSettings();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateNotificationRule(index, ruleData) {
|
||||
var rules = JSON.parse(JSON.stringify(notificationRules || []));
|
||||
if (index < 0 || index >= rules.length)
|
||||
|
||||
@@ -776,6 +776,53 @@ Singleton {
|
||||
};
|
||||
}
|
||||
|
||||
readonly property int notificationAnimationBaseDuration: {
|
||||
if (typeof SettingsData === "undefined")
|
||||
return 200;
|
||||
if (SettingsData.notificationAnimationSpeed === SettingsData.AnimationSpeed.None)
|
||||
return 0;
|
||||
if (SettingsData.notificationAnimationSpeed === SettingsData.AnimationSpeed.Custom)
|
||||
return SettingsData.notificationCustomAnimationDuration;
|
||||
const presetMap = [0, 200, 400, 600];
|
||||
return presetMap[SettingsData.notificationAnimationSpeed] ?? 200;
|
||||
}
|
||||
|
||||
readonly property int notificationEnterDuration: {
|
||||
const base = notificationAnimationBaseDuration;
|
||||
return base === 0 ? 0 : Math.round(base * 0.875);
|
||||
}
|
||||
|
||||
readonly property int notificationExitDuration: {
|
||||
const base = notificationAnimationBaseDuration;
|
||||
return base === 0 ? 0 : Math.round(base * 0.75);
|
||||
}
|
||||
|
||||
readonly property int notificationExpandDuration: {
|
||||
const base = notificationAnimationBaseDuration;
|
||||
return base === 0 ? 0 : Math.round(base * 1.0);
|
||||
}
|
||||
|
||||
readonly property int notificationCollapseDuration: {
|
||||
const base = notificationAnimationBaseDuration;
|
||||
return base === 0 ? 0 : Math.round(base * 0.85);
|
||||
}
|
||||
|
||||
readonly property real notificationIconSizeNormal: 56
|
||||
readonly property real notificationIconSizeCompact: 48
|
||||
readonly property real notificationExpandedIconSizeNormal: 48
|
||||
readonly property real notificationExpandedIconSizeCompact: 40
|
||||
readonly property real notificationActionMinWidth: 48
|
||||
readonly property real notificationButtonCornerRadius: cornerRadius / 2
|
||||
readonly property real notificationHoverRevealMargin: spacingXL
|
||||
readonly property real notificationContentSpacing: spacingXS
|
||||
readonly property real notificationCardPadding: spacingM
|
||||
readonly property real notificationCardPaddingCompact: spacingS
|
||||
|
||||
readonly property real stateLayerHover: 0.08
|
||||
readonly property real stateLayerFocus: 0.12
|
||||
readonly property real stateLayerPressed: 0.12
|
||||
readonly property real stateLayerDrag: 0.16
|
||||
|
||||
readonly property int popoutAnimationDuration: {
|
||||
if (typeof SettingsData === "undefined")
|
||||
return 150;
|
||||
|
||||
@@ -32,8 +32,15 @@ function markdownToHtml(text) {
|
||||
return `\x00INLINECODE${inlineIndex++}\x00`;
|
||||
});
|
||||
|
||||
// Now process everything else
|
||||
// Escape HTML entities (but not in code blocks)
|
||||
// Extract plain URLs before escaping so & in query strings is preserved
|
||||
const urls = [];
|
||||
let urlIndex = 0;
|
||||
html = html.replace(/(^|[\s])((?:https?|file):\/\/[^\s]+)/gm, (match, prefix, url) => {
|
||||
urls.push(url);
|
||||
return prefix + `\x00URL${urlIndex++}\x00`;
|
||||
});
|
||||
|
||||
// Escape HTML entities (but not in code blocks or URLs)
|
||||
html = html.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
@@ -64,8 +71,12 @@ function markdownToHtml(text) {
|
||||
return '<ul>' + match + '</ul>';
|
||||
});
|
||||
|
||||
// Detect plain URLs and wrap them in anchor tags (but not inside existing <a> or markdown links)
|
||||
html = html.replace(/(^|[^"'>])((https?|file):\/\/[^\s<]+)/g, '$1<a href="$2">$2</a>');
|
||||
// Restore extracted URLs as anchor tags (preserves raw & in href)
|
||||
html = html.replace(/\x00URL(\d+)\x00/g, (_, index) => {
|
||||
const url = urls[parseInt(index)];
|
||||
const display = url.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
return `<a href="${url}">${display}</a>`;
|
||||
});
|
||||
|
||||
// Restore code blocks and inline code BEFORE line break processing
|
||||
html = html.replace(/\x00CODEBLOCK(\d+)\x00/g, (match, index) => {
|
||||
|
||||
@@ -297,6 +297,8 @@ var SPEC = {
|
||||
dockShowOverflowBadge: { def: true },
|
||||
|
||||
notificationOverlayEnabled: { def: false },
|
||||
notificationPopupShadowEnabled: { def: true },
|
||||
notificationPopupPrivacyMode: { def: false },
|
||||
overviewRows: { def: 2, persist: false },
|
||||
overviewColumns: { def: 5, persist: false },
|
||||
overviewScale: { def: 0.16, persist: false },
|
||||
@@ -325,6 +327,8 @@ var SPEC = {
|
||||
notificationTimeoutCritical: { def: 0 },
|
||||
notificationCompactMode: { def: false },
|
||||
notificationPopupPosition: { def: 0 },
|
||||
notificationAnimationSpeed: { def: 1 },
|
||||
notificationCustomAnimationDuration: { def: 400 },
|
||||
notificationHistoryEnabled: { def: true },
|
||||
notificationHistoryMaxCount: { def: 50 },
|
||||
notificationHistoryMaxAgeDays: { def: 7 },
|
||||
|
||||
@@ -70,8 +70,8 @@ DankModal {
|
||||
NotificationService.dismissAllPopups();
|
||||
}
|
||||
|
||||
modalWidth: 500
|
||||
modalHeight: 700
|
||||
modalWidth: Math.min(500, screenWidth - 48)
|
||||
modalHeight: Math.min(700, screenHeight * 0.85)
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
visible: false
|
||||
onBackgroundClicked: hide()
|
||||
|
||||
@@ -21,8 +21,8 @@ Rectangle {
|
||||
}
|
||||
|
||||
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 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
|
||||
@@ -93,7 +93,7 @@ Rectangle {
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: cardPadding
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
anchors.rightMargin: Theme.spacingL + (compactMode ? 32 : 40)
|
||||
anchors.rightMargin: Theme.spacingL + Theme.notificationHoverRevealMargin
|
||||
height: collapsedContentHeight + extraHeight
|
||||
|
||||
DankCircularImage {
|
||||
@@ -165,32 +165,47 @@ Rectangle {
|
||||
Column {
|
||||
width: parent.width
|
||||
anchors.top: parent.top
|
||||
spacing: compactMode ? 1 : 2
|
||||
spacing: Theme.notificationContentSpacing
|
||||
|
||||
StyledText {
|
||||
Row {
|
||||
width: parent.width
|
||||
text: {
|
||||
const timeStr = NotificationService.formatHistoryTime(historyItem.timestamp);
|
||||
const appName = historyItem.appName || "";
|
||||
return timeStr.length > 0 ? `${appName} • ${timeStr}` : appName;
|
||||
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
|
||||
}
|
||||
color: Theme.surfaceVariantText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
visible: text.length > 0
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: historyItem.summary || ""
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
visible: text.length > 0
|
||||
}
|
||||
|
||||
StyledText {
|
||||
|
||||
@@ -11,15 +11,63 @@ DankListView {
|
||||
property bool autoScrollDisabled: false
|
||||
property bool isAnimatingExpansion: false
|
||||
property alias listContentHeight: listView.contentHeight
|
||||
property real stableContentHeight: 0
|
||||
property bool cardAnimateExpansion: true
|
||||
property bool listInitialized: false
|
||||
property int swipingCardIndex: -1
|
||||
property real swipingCardOffset: 0
|
||||
property real __pendingStableHeight: 0
|
||||
property real __heightUpdateThreshold: 20
|
||||
|
||||
Component.onCompleted: {
|
||||
Qt.callLater(() => {
|
||||
listInitialized = true;
|
||||
if (listView) {
|
||||
listView.listInitialized = true;
|
||||
listView.stableContentHeight = listView.contentHeight;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: heightUpdateDebounce
|
||||
interval: Theme.mediumDuration + 20
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (!listView.isAnimatingExpansion && Math.abs(listView.__pendingStableHeight - listView.stableContentHeight) > listView.__heightUpdateThreshold) {
|
||||
listView.stableContentHeight = listView.__pendingStableHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onContentHeightChanged: {
|
||||
if (!isAnimatingExpansion) {
|
||||
__pendingStableHeight = contentHeight;
|
||||
if (Math.abs(contentHeight - stableContentHeight) > __heightUpdateThreshold) {
|
||||
heightUpdateDebounce.restart();
|
||||
} else {
|
||||
stableContentHeight = contentHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onIsAnimatingExpansionChanged: {
|
||||
if (isAnimatingExpansion) {
|
||||
heightUpdateDebounce.stop();
|
||||
let delta = 0;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const item = itemAtIndex(i);
|
||||
if (item && item.children[0] && item.children[0].isAnimating)
|
||||
delta += item.children[0].targetHeight - item.height;
|
||||
}
|
||||
const targetHeight = contentHeight + delta;
|
||||
// During expansion, always update immediately without threshold check
|
||||
stableContentHeight = targetHeight;
|
||||
} else {
|
||||
__pendingStableHeight = contentHeight;
|
||||
heightUpdateDebounce.restart();
|
||||
}
|
||||
}
|
||||
|
||||
clip: true
|
||||
model: NotificationService.groupedNotifications
|
||||
spacing: Theme.spacingL
|
||||
@@ -86,29 +134,47 @@ DankListView {
|
||||
readonly property real dismissThreshold: width * 0.35
|
||||
property bool __delegateInitialized: false
|
||||
|
||||
readonly property bool isAdjacentToSwipe: listView.count >= 2 && listView.swipingCardIndex !== -1 &&
|
||||
(index === listView.swipingCardIndex - 1 || index === listView.swipingCardIndex + 1)
|
||||
readonly property real adjacentSwipeInfluence: isAdjacentToSwipe ? listView.swipingCardOffset * 0.10 : 0
|
||||
readonly property real adjacentScaleInfluence: isAdjacentToSwipe ? 1.0 - Math.abs(listView.swipingCardOffset) / width * 0.02 : 1.0
|
||||
readonly property real swipeFadeStartOffset: width * 0.75
|
||||
readonly property real swipeFadeDistance: Math.max(1, width - swipeFadeStartOffset)
|
||||
|
||||
Component.onCompleted: {
|
||||
Qt.callLater(() => {
|
||||
__delegateInitialized = true;
|
||||
if (delegateRoot)
|
||||
delegateRoot.__delegateInitialized = true;
|
||||
});
|
||||
}
|
||||
|
||||
width: ListView.view.width
|
||||
height: isDismissing ? 0 : notificationCard.targetHeight
|
||||
clip: isDismissing || notificationCard.isAnimating
|
||||
height: notificationCard.height
|
||||
clip: notificationCard.isAnimating
|
||||
|
||||
NotificationCard {
|
||||
id: notificationCard
|
||||
width: parent.width
|
||||
x: delegateRoot.swipeOffset
|
||||
x: delegateRoot.swipeOffset + delegateRoot.adjacentSwipeInfluence
|
||||
listLevelAdjacentScaleInfluence: delegateRoot.adjacentScaleInfluence
|
||||
listLevelScaleAnimationsEnabled: listView.swipingCardIndex === -1 || !delegateRoot.isAdjacentToSwipe
|
||||
notificationGroup: modelData
|
||||
keyboardNavigationActive: listView.keyboardActive
|
||||
animateExpansion: listView.cardAnimateExpansion && listView.listInitialized
|
||||
opacity: 1 - Math.abs(delegateRoot.swipeOffset) / (delegateRoot.width * 0.5)
|
||||
opacity: {
|
||||
const swipeAmount = Math.abs(delegateRoot.swipeOffset);
|
||||
if (swipeAmount <= delegateRoot.swipeFadeStartOffset)
|
||||
return 1;
|
||||
const fadeProgress = (swipeAmount - delegateRoot.swipeFadeStartOffset) / delegateRoot.swipeFadeDistance;
|
||||
return Math.max(0, 1 - fadeProgress);
|
||||
}
|
||||
onIsAnimatingChanged: {
|
||||
if (isAnimating) {
|
||||
listView.isAnimatingExpansion = true;
|
||||
} else {
|
||||
Qt.callLater(() => {
|
||||
if (!notificationCard || !listView)
|
||||
return;
|
||||
let anyAnimating = false;
|
||||
for (let i = 0; i < listView.count; i++) {
|
||||
const item = listView.itemAtIndex(i);
|
||||
@@ -139,7 +205,7 @@ DankListView {
|
||||
}
|
||||
|
||||
Behavior on x {
|
||||
enabled: !swipeDragHandler.active && listView.listInitialized
|
||||
enabled: !swipeDragHandler.active && !delegateRoot.isDismissing && (listView.swipingCardIndex === -1 || !delegateRoot.isAdjacentToSwipe) && listView.listInitialized
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
@@ -161,12 +227,18 @@ DankListView {
|
||||
xAxis.enabled: true
|
||||
|
||||
onActiveChanged: {
|
||||
if (active || delegateRoot.isDismissing)
|
||||
if (active) {
|
||||
listView.swipingCardIndex = index;
|
||||
return;
|
||||
}
|
||||
listView.swipingCardIndex = -1;
|
||||
listView.swipingCardOffset = 0;
|
||||
if (delegateRoot.isDismissing)
|
||||
return;
|
||||
if (Math.abs(delegateRoot.swipeOffset) > delegateRoot.dismissThreshold) {
|
||||
delegateRoot.isDismissing = true;
|
||||
delegateRoot.swipeOffset = delegateRoot.swipeOffset > 0 ? delegateRoot.width : -delegateRoot.width;
|
||||
dismissTimer.start();
|
||||
swipeDismissAnim.to = delegateRoot.swipeOffset > 0 ? delegateRoot.width : -delegateRoot.width;
|
||||
swipeDismissAnim.start();
|
||||
} else {
|
||||
delegateRoot.swipeOffset = 0;
|
||||
}
|
||||
@@ -176,13 +248,18 @@ DankListView {
|
||||
if (delegateRoot.isDismissing)
|
||||
return;
|
||||
delegateRoot.swipeOffset = translation.x;
|
||||
listView.swipingCardOffset = translation.x;
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: dismissTimer
|
||||
interval: Theme.shortDuration
|
||||
onTriggered: NotificationService.dismissGroup(delegateRoot.modelData?.key || "")
|
||||
NumberAnimation {
|
||||
id: swipeDismissAnim
|
||||
target: delegateRoot
|
||||
property: "swipeOffset"
|
||||
to: 0
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Easing.OutCubic
|
||||
onStopped: NotificationService.dismissGroup(delegateRoot.modelData?.key || "")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
import qs.Common
|
||||
@@ -18,28 +19,43 @@ Rectangle {
|
||||
property bool isGroupSelected: false
|
||||
property int selectedNotificationIndex: -1
|
||||
property bool keyboardNavigationActive: false
|
||||
property int swipingNotificationIndex: -1
|
||||
property real swipingNotificationOffset: 0
|
||||
property real listLevelAdjacentScaleInfluence: 1.0
|
||||
property bool listLevelScaleAnimationsEnabled: true
|
||||
|
||||
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 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 collapsedDismissOffset: 5
|
||||
readonly property real badgeSize: compactMode ? 16 : 18
|
||||
readonly property real actionButtonHeight: compactMode ? 20 : 24
|
||||
readonly property real collapsedContentHeight: iconSize
|
||||
readonly property real collapsedContentHeight: Math.max(iconSize, Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2))
|
||||
readonly property real baseCardHeight: cardPadding * 2 + collapsedContentHeight + actionButtonHeight + contentSpacing
|
||||
|
||||
width: parent ? parent.width : 400
|
||||
height: expanded ? (expandedContent.height + cardPadding * 2) : (baseCardHeight + collapsedContent.extraHeight)
|
||||
readonly property real targetHeight: expanded ? (expandedContent.height + cardPadding * 2) : (baseCardHeight + collapsedContent.extraHeight)
|
||||
radius: Theme.cornerRadius
|
||||
scale: (cardHoverHandler.hovered ? 1.01 : 1.0) * listLevelAdjacentScaleInfluence
|
||||
property bool __initialized: false
|
||||
|
||||
Component.onCompleted: {
|
||||
Qt.callLater(() => {
|
||||
__initialized = true;
|
||||
if (root)
|
||||
root.__initialized = true;
|
||||
});
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
enabled: listLevelScaleAnimationsEnabled
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
enabled: root.__initialized
|
||||
ColorAnimation {
|
||||
@@ -83,6 +99,10 @@ Rectangle {
|
||||
}
|
||||
clip: true
|
||||
|
||||
HoverHandler {
|
||||
id: cardHoverHandler
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
@@ -109,15 +129,16 @@ Rectangle {
|
||||
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
|
||||
readonly property real collapsedLineCount: compactMode ? 1 : 2
|
||||
readonly property real collapsedLineHeight: Theme.fontSizeSmall * 1.2 * collapsedLineCount
|
||||
readonly property real extraHeight: (descriptionExpanded && expandedTextHeight > collapsedLineHeight + 2) ? (expandedTextHeight - collapsedLineHeight) : 0
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: cardPadding
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
anchors.rightMargin: Theme.spacingL + (compactMode ? 32 : 40)
|
||||
anchors.rightMargin: Theme.spacingL + Theme.notificationHoverRevealMargin
|
||||
height: collapsedContentHeight + extraHeight
|
||||
visible: !expanded
|
||||
|
||||
@@ -139,6 +160,7 @@ Rectangle {
|
||||
height: iconSize
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: descriptionExpanded ? Math.max(0, Theme.fontSizeSmall * 1.2 + (Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2)) / 2 - iconSize / 2) : Math.max(0, Theme.fontSizeSmall * 1.2 + (textContainer.height - Theme.fontSizeSmall * 1.2) / 2 - iconSize / 2)
|
||||
|
||||
imageSource: {
|
||||
if (hasNotificationImage)
|
||||
@@ -212,29 +234,49 @@ Rectangle {
|
||||
Column {
|
||||
width: parent.width
|
||||
anchors.top: parent.top
|
||||
spacing: compactMode ? 1 : 2
|
||||
spacing: Theme.notificationContentSpacing
|
||||
|
||||
StyledText {
|
||||
Row {
|
||||
id: collapsedHeaderRow
|
||||
width: parent.width
|
||||
text: {
|
||||
const timeStr = (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.timeStr) || "";
|
||||
const appName = (notificationGroup && notificationGroup.appName) || "";
|
||||
return timeStr.length > 0 ? `${appName} • ${timeStr}` : appName;
|
||||
spacing: Theme.spacingXS
|
||||
visible: (collapsedHeaderAppNameText.text.length > 0 || collapsedHeaderTimeText.text.length > 0)
|
||||
|
||||
StyledText {
|
||||
id: collapsedHeaderAppNameText
|
||||
text: notificationGroup?.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 - collapsedHeaderSeparator.implicitWidth - collapsedHeaderTimeText.implicitWidth - parent.spacing * 2)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: collapsedHeaderSeparator
|
||||
text: (collapsedHeaderAppNameText.text.length > 0 && collapsedHeaderTimeText.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: collapsedHeaderTimeText
|
||||
text: notificationGroup?.latestNotification?.timeStr || ""
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Normal
|
||||
}
|
||||
color: Theme.surfaceVariantText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
visible: text.length > 0
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.summary) || ""
|
||||
id: collapsedTitleText
|
||||
width: parent.width
|
||||
text: notificationGroup?.latestNotification?.summary || ""
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
visible: text.length > 0
|
||||
@@ -301,7 +343,7 @@ Rectangle {
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingL + (compactMode ? 32 : 40)
|
||||
anchors.rightMargin: Theme.spacingL + Theme.notificationHoverRevealMargin
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
@@ -343,214 +385,331 @@ Rectangle {
|
||||
objectName: "notificationRepeater"
|
||||
model: notificationGroup?.notifications?.slice(0, 10) || []
|
||||
|
||||
delegate: Rectangle {
|
||||
delegate: Item {
|
||||
id: expandedDelegateWrapper
|
||||
required property var modelData
|
||||
required property int index
|
||||
readonly property bool messageExpanded: NotificationService.expandedMessages[modelData?.notification?.id] || false
|
||||
readonly property bool isSelected: root.selectedNotificationIndex === index
|
||||
readonly property real expandedIconSize: compactMode ? 40 : 48
|
||||
readonly property bool actionsVisible: true
|
||||
readonly property real expandedIconSize: compactMode ? Theme.notificationExpandedIconSizeCompact : Theme.notificationExpandedIconSizeNormal
|
||||
|
||||
HoverHandler {
|
||||
id: expandedDelegateHoverHandler
|
||||
}
|
||||
readonly property real expandedItemPadding: compactMode ? Theme.spacingS : Theme.spacingM
|
||||
readonly property real expandedBaseHeight: expandedItemPadding * 2 + expandedIconSize + actionButtonHeight + contentSpacing * 2
|
||||
readonly property real expandedBaseHeight: expandedItemPadding * 2 + Math.max(expandedIconSize, Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * 2) + actionButtonHeight + contentSpacing * 2
|
||||
property bool __delegateInitialized: false
|
||||
property real swipeOffset: 0
|
||||
property bool isDismissing: false
|
||||
readonly property real dismissThreshold: width * 0.35
|
||||
|
||||
Component.onCompleted: {
|
||||
Qt.callLater(() => {
|
||||
__delegateInitialized = true;
|
||||
if (expandedDelegateWrapper)
|
||||
expandedDelegateWrapper.__delegateInitialized = true;
|
||||
});
|
||||
}
|
||||
|
||||
width: parent.width
|
||||
height: {
|
||||
if (!messageExpanded)
|
||||
return expandedBaseHeight;
|
||||
const twoLineHeight = bodyText.font.pixelSize * 1.2 * 2;
|
||||
if (bodyText.implicitHeight > twoLineHeight + 2)
|
||||
return expandedBaseHeight + bodyText.implicitHeight - twoLineHeight;
|
||||
return expandedBaseHeight;
|
||||
}
|
||||
radius: Theme.cornerRadius
|
||||
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.width: 1
|
||||
height: delegateRect.height
|
||||
clip: true
|
||||
|
||||
Behavior on border.color {
|
||||
enabled: __delegateInitialized
|
||||
ColorAnimation {
|
||||
duration: __delegateInitialized ? Theme.shortDuration : 0
|
||||
easing.type: Theme.standardEasing
|
||||
Rectangle {
|
||||
id: delegateRect
|
||||
width: parent.width
|
||||
|
||||
readonly property bool isAdjacentToSwipe: root.swipingNotificationIndex !== -1 &&
|
||||
(expandedDelegateWrapper.index === root.swipingNotificationIndex - 1 ||
|
||||
expandedDelegateWrapper.index === root.swipingNotificationIndex + 1)
|
||||
readonly property real adjacentSwipeInfluence: isAdjacentToSwipe ? root.swipingNotificationOffset * 0.10 : 0
|
||||
readonly property real adjacentScaleInfluence: isAdjacentToSwipe ? 1.0 - Math.abs(root.swipingNotificationOffset) / width * 0.02 : 1.0
|
||||
|
||||
x: expandedDelegateWrapper.swipeOffset + adjacentSwipeInfluence
|
||||
scale: adjacentScaleInfluence
|
||||
transformOrigin: Item.Center
|
||||
|
||||
Behavior on x {
|
||||
enabled: !expandedSwipeHandler.active && !expandedDelegateWrapper.isDismissing
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on height {
|
||||
enabled: false
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
anchors.margins: compactMode ? Theme.spacingS : Theme.spacingM
|
||||
anchors.bottomMargin: contentSpacing
|
||||
|
||||
DankCircularImage {
|
||||
id: messageIcon
|
||||
|
||||
readonly property string rawImage: modelData?.image || ""
|
||||
readonly property string iconFromImage: {
|
||||
if (rawImage.startsWith("image://icon/"))
|
||||
return rawImage.substring(13);
|
||||
return "";
|
||||
Behavior on scale {
|
||||
enabled: !expandedSwipeHandler.active
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
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: expandedIconSize
|
||||
height: expandedIconSize
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: compactMode ? Theme.spacingM : Theme.spacingXL
|
||||
height: {
|
||||
if (!messageExpanded)
|
||||
return expandedBaseHeight;
|
||||
const twoLineHeight = bodyText.font.pixelSize * 1.2 * 2;
|
||||
if (bodyText.implicitHeight > twoLineHeight + 2)
|
||||
return expandedBaseHeight + bodyText.implicitHeight - twoLineHeight;
|
||||
return expandedBaseHeight;
|
||||
}
|
||||
radius: Theme.cornerRadius
|
||||
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.width: 1
|
||||
|
||||
imageSource: {
|
||||
if (hasNotificationImage)
|
||||
return modelData.cleanImage;
|
||||
if (imageHasSpecialPrefix)
|
||||
return "";
|
||||
const appIcon = modelData?.appIcon;
|
||||
if (!appIcon)
|
||||
return iconFromImage ? "image://icon/" + 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 Quickshell.iconPath(appIcon, true);
|
||||
Behavior on border.color {
|
||||
enabled: __delegateInitialized
|
||||
ColorAnimation {
|
||||
duration: __delegateInitialized ? Theme.shortDuration : 0
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
fallbackIcon: {
|
||||
if (imageHasSpecialPrefix)
|
||||
return iconFromImage;
|
||||
return modelData?.appIcon || iconFromImage || "";
|
||||
}
|
||||
|
||||
fallbackText: {
|
||||
const appName = modelData?.appName || "?";
|
||||
return appName.charAt(0).toUpperCase();
|
||||
}
|
||||
Behavior on height {
|
||||
enabled: false
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.left: messageIcon.right
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.fill: parent
|
||||
anchors.margins: compactMode ? Theme.spacingS : Theme.spacingM
|
||||
anchors.bottomMargin: contentSpacing
|
||||
|
||||
Column {
|
||||
DankCircularImage {
|
||||
id: messageIcon
|
||||
|
||||
readonly property string rawImage: modelData?.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: expandedIconSize
|
||||
height: expandedIconSize
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: buttonArea.top
|
||||
anchors.bottomMargin: contentSpacing
|
||||
spacing: compactMode ? 1 : 2
|
||||
anchors.topMargin: Theme.fontSizeSmall * 1.2 + (compactMode ? Theme.spacingXS : Theme.spacingS)
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: modelData?.timeStr || ""
|
||||
color: Theme.surfaceVariantText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
visible: text.length > 0
|
||||
imageSource: {
|
||||
if (hasNotificationImage)
|
||||
return modelData.cleanImage;
|
||||
if (imageHasSpecialPrefix)
|
||||
return "";
|
||||
const appIcon = modelData?.appIcon;
|
||||
if (!appIcon)
|
||||
return iconFromImage ? "image://icon/" + 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 Quickshell.iconPath(appIcon, true);
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: modelData?.summary || ""
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
visible: text.length > 0
|
||||
fallbackIcon: {
|
||||
if (imageHasSpecialPrefix)
|
||||
return iconFromImage;
|
||||
return modelData?.appIcon || iconFromImage || "";
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: bodyText
|
||||
property bool hasMoreText: truncated
|
||||
|
||||
text: modelData?.htmlBody || ""
|
||||
color: Theme.surfaceVariantText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
width: parent.width
|
||||
elide: messageExpanded ? Text.ElideNone : Text.ElideRight
|
||||
maximumLineCount: messageExpanded ? -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 : (bodyText.hasMoreText || messageExpanded) ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
|
||||
onClicked: mouse => {
|
||||
if (!parent.hoveredLink && (bodyText.hasMoreText || messageExpanded)) {
|
||||
NotificationService.toggleMessageExpansion(modelData?.notification?.id || "");
|
||||
}
|
||||
}
|
||||
|
||||
propagateComposedEvents: true
|
||||
onPressed: mouse => {
|
||||
if (parent.hoveredLink) {
|
||||
mouse.accepted = false;
|
||||
}
|
||||
}
|
||||
onReleased: mouse => {
|
||||
if (parent.hoveredLink) {
|
||||
mouse.accepted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
fallbackText: {
|
||||
const appName = modelData?.appName || "?";
|
||||
return appName.charAt(0).toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: buttonArea
|
||||
anchors.left: parent.left
|
||||
anchors.left: messageIcon.right
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
height: actionButtonHeight + contentSpacing
|
||||
|
||||
Row {
|
||||
Column {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: buttonArea.top
|
||||
anchors.bottomMargin: contentSpacing
|
||||
spacing: Theme.notificationContentSpacing
|
||||
|
||||
Row {
|
||||
id: expandedDelegateHeaderRow
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
visible: (expandedDelegateHeaderAppNameText.text.length > 0 || expandedDelegateHeaderTimeText.text.length > 0)
|
||||
|
||||
StyledText {
|
||||
id: expandedDelegateHeaderAppNameText
|
||||
text: modelData?.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 - expandedDelegateHeaderSeparator.implicitWidth - expandedDelegateHeaderTimeText.implicitWidth - parent.spacing * 2)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: expandedDelegateHeaderSeparator
|
||||
text: (expandedDelegateHeaderAppNameText.text.length > 0 && expandedDelegateHeaderTimeText.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: expandedDelegateHeaderTimeText
|
||||
text: modelData?.timeStr || ""
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Normal
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: expandedDelegateTitleText
|
||||
width: parent.width
|
||||
text: modelData?.summary || ""
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
visible: text.length > 0
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: bodyText
|
||||
property bool hasMoreText: truncated
|
||||
|
||||
text: modelData?.htmlBody || ""
|
||||
color: Theme.surfaceVariantText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
width: parent.width
|
||||
elide: messageExpanded ? Text.ElideNone : Text.ElideRight
|
||||
maximumLineCount: messageExpanded ? -1 : 2
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
visible: text.length > 0
|
||||
linkColor: Theme.primary
|
||||
onLinkActivated: link => Qt.openUrlExternally(link)
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : (bodyText.hasMoreText || messageExpanded) ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
|
||||
onClicked: mouse => {
|
||||
if (!parent.hoveredLink && (bodyText.hasMoreText || messageExpanded)) {
|
||||
NotificationService.toggleMessageExpansion(modelData?.notification?.id || "");
|
||||
}
|
||||
}
|
||||
|
||||
propagateComposedEvents: true
|
||||
onPressed: mouse => {
|
||||
if (parent.hoveredLink) {
|
||||
mouse.accepted = false;
|
||||
}
|
||||
}
|
||||
onReleased: mouse => {
|
||||
if (parent.hoveredLink) {
|
||||
mouse.accepted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: buttonArea
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
spacing: contentSpacing
|
||||
height: actionButtonHeight + contentSpacing
|
||||
|
||||
Repeater {
|
||||
model: modelData?.actions || []
|
||||
Row {
|
||||
visible: expandedDelegateWrapper.actionsVisible
|
||||
opacity: visible ? 1 : 0
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
spacing: contentSpacing
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: modelData?.actions || []
|
||||
|
||||
Rectangle {
|
||||
property bool isHovered: false
|
||||
|
||||
width: Math.max(expandedActionText.implicitWidth + Theme.spacingM, Theme.notificationActionMinWidth)
|
||||
height: actionButtonHeight
|
||||
radius: Theme.notificationButtonCornerRadius
|
||||
color: isHovered ? Theme.withAlpha(Theme.primary, Theme.stateLayerHover) : "transparent"
|
||||
|
||||
StyledText {
|
||||
id: expandedActionText
|
||||
text: {
|
||||
const baseText = modelData.text || "Open";
|
||||
if (keyboardNavigationActive && (isGroupSelected || selectedNotificationIndex >= 0))
|
||||
return `${baseText} (${index + 1})`;
|
||||
return baseText;
|
||||
}
|
||||
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
|
||||
onEntered: parent.isHovered = true
|
||||
onExited: parent.isHovered = false
|
||||
onClicked: {
|
||||
if (modelData && modelData.invoke)
|
||||
modelData.invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: expandedDelegateDismissBtn
|
||||
property bool isHovered: false
|
||||
|
||||
width: Math.max(expandedActionText.implicitWidth + Theme.spacingM, compactMode ? 40 : 50)
|
||||
visible: expandedDelegateWrapper.actionsVisible
|
||||
opacity: visible ? 1 : 0
|
||||
width: Math.max(expandedClearText.implicitWidth + Theme.spacingM, Theme.notificationActionMinWidth)
|
||||
height: actionButtonHeight
|
||||
radius: Theme.spacingXS
|
||||
color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent"
|
||||
radius: Theme.notificationButtonCornerRadius
|
||||
color: isHovered ? Theme.withAlpha(Theme.primary, Theme.stateLayerHover) : "transparent"
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: expandedActionText
|
||||
text: {
|
||||
const baseText = modelData.text || "View";
|
||||
if (keyboardNavigationActive && (isGroupSelected || selectedNotificationIndex >= 0))
|
||||
return `${baseText} (${index + 1})`;
|
||||
return baseText;
|
||||
}
|
||||
id: expandedClearText
|
||||
text: I18n.tr("Dismiss")
|
||||
color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
anchors.centerIn: parent
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
@@ -559,44 +718,56 @@ Rectangle {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onEntered: parent.isHovered = true
|
||||
onExited: parent.isHovered = false
|
||||
onClicked: {
|
||||
if (modelData && modelData.invoke)
|
||||
modelData.invoke();
|
||||
}
|
||||
onClicked: NotificationService.dismissNotification(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
property bool isHovered: false
|
||||
|
||||
width: Math.max(expandedClearText.implicitWidth + Theme.spacingM, compactMode ? 40 : 50)
|
||||
height: actionButtonHeight
|
||||
radius: Theme.spacingXS
|
||||
color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent"
|
||||
|
||||
StyledText {
|
||||
id: expandedClearText
|
||||
text: I18n.tr("Dismiss")
|
||||
color: parent.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
|
||||
onEntered: parent.isHovered = true
|
||||
onExited: parent.isHovered = false
|
||||
onClicked: NotificationService.dismissNotification(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DragHandler {
|
||||
id: expandedSwipeHandler
|
||||
target: null
|
||||
xAxis.enabled: true
|
||||
yAxis.enabled: false
|
||||
grabPermissions: PointerHandler.CanTakeOverFromItems | PointerHandler.CanTakeOverFromHandlersOfDifferentType
|
||||
|
||||
onActiveChanged: {
|
||||
if (active) {
|
||||
root.swipingNotificationIndex = expandedDelegateWrapper.index;
|
||||
} else {
|
||||
root.swipingNotificationIndex = -1;
|
||||
root.swipingNotificationOffset = 0;
|
||||
}
|
||||
if (active || expandedDelegateWrapper.isDismissing)
|
||||
return;
|
||||
if (Math.abs(expandedDelegateWrapper.swipeOffset) > expandedDelegateWrapper.dismissThreshold) {
|
||||
expandedDelegateWrapper.isDismissing = true;
|
||||
expandedSwipeDismissAnim.start();
|
||||
} else {
|
||||
expandedDelegateWrapper.swipeOffset = 0;
|
||||
}
|
||||
}
|
||||
|
||||
onTranslationChanged: {
|
||||
if (expandedDelegateWrapper.isDismissing)
|
||||
return;
|
||||
expandedDelegateWrapper.swipeOffset = translation.x;
|
||||
root.swipingNotificationOffset = translation.x;
|
||||
}
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
id: expandedSwipeDismissAnim
|
||||
target: expandedDelegateWrapper
|
||||
property: "swipeOffset"
|
||||
to: expandedDelegateWrapper.swipeOffset > 0 ? expandedDelegateWrapper.width : -expandedDelegateWrapper.width
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Easing.OutCubic
|
||||
onStopped: NotificationService.dismissNotification(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -607,7 +778,7 @@ Rectangle {
|
||||
anchors.right: clearButton.visible ? clearButton.left : parent.right
|
||||
anchors.rightMargin: clearButton.visible ? contentSpacing : Theme.spacingL
|
||||
anchors.top: collapsedContent.bottom
|
||||
anchors.topMargin: contentSpacing
|
||||
anchors.topMargin: contentSpacing + collapsedDismissOffset
|
||||
spacing: contentSpacing
|
||||
|
||||
Repeater {
|
||||
@@ -616,15 +787,15 @@ Rectangle {
|
||||
Rectangle {
|
||||
property bool isHovered: false
|
||||
|
||||
width: Math.max(collapsedActionText.implicitWidth + Theme.spacingM, compactMode ? 40 : 50)
|
||||
width: Math.max(collapsedActionText.implicitWidth + Theme.spacingM, Theme.notificationActionMinWidth)
|
||||
height: actionButtonHeight
|
||||
radius: Theme.spacingXS
|
||||
color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent"
|
||||
radius: Theme.notificationButtonCornerRadius
|
||||
color: isHovered ? Theme.withAlpha(Theme.primary, Theme.stateLayerHover) : "transparent"
|
||||
|
||||
StyledText {
|
||||
id: collapsedActionText
|
||||
text: {
|
||||
const baseText = modelData.text || "View";
|
||||
const baseText = modelData.text || "Open";
|
||||
if (keyboardNavigationActive && isGroupSelected) {
|
||||
return `${baseText} (${index + 1})`;
|
||||
}
|
||||
@@ -663,11 +834,11 @@ Rectangle {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
anchors.top: collapsedContent.bottom
|
||||
anchors.topMargin: contentSpacing
|
||||
width: Math.max(collapsedClearText.implicitWidth + Theme.spacingM, compactMode ? 40 : 50)
|
||||
anchors.topMargin: contentSpacing + collapsedDismissOffset
|
||||
width: Math.max(collapsedClearText.implicitWidth + Theme.spacingM, Theme.notificationActionMinWidth)
|
||||
height: actionButtonHeight
|
||||
radius: Theme.spacingXS
|
||||
color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent"
|
||||
radius: Theme.notificationButtonCornerRadius
|
||||
color: isHovered ? Theme.withAlpha(Theme.primary, Theme.stateLayerHover) : "transparent"
|
||||
|
||||
StyledText {
|
||||
id: collapsedClearText
|
||||
@@ -691,6 +862,7 @@ Rectangle {
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
visible: !expanded && (notificationGroup?.count || 0) > 1 && !descriptionExpanded
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.userInitiatedExpansion = true;
|
||||
NotificationService.toggleGroupExpansion(notificationGroup?.key || "");
|
||||
@@ -731,11 +903,11 @@ Rectangle {
|
||||
}
|
||||
|
||||
Behavior on height {
|
||||
enabled: root.userInitiatedExpansion && root.animateExpansion
|
||||
enabled: root.__initialized && root.userInitiatedExpansion && root.animateExpansion
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.normal
|
||||
duration: root.expanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.standard
|
||||
easing.bezierCurve: Theme.expressiveCurves.emphasized
|
||||
onRunningChanged: {
|
||||
if (running) {
|
||||
root.isAnimating = true;
|
||||
@@ -746,4 +918,102 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Menu {
|
||||
id: notificationCardContextMenu
|
||||
width: 220
|
||||
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 = notificationGroup?.appName || "";
|
||||
const desktopEntry = notificationGroup?.latestNotification?.desktopEntry || "";
|
||||
SettingsData.addNotificationRuleForNotification(appName, desktopEntry);
|
||||
PopoutService.openSettingsWithTab("notifications");
|
||||
}
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
id: muteUnmuteItem
|
||||
readonly property bool isMuted: SettingsData.isAppMuted(notificationGroup?.appName || "", notificationGroup?.latestNotification?.desktopEntry || "")
|
||||
text: isMuted ? I18n.tr("Unmute popups for %1").arg(notificationGroup?.appName || I18n.tr("this app")) : I18n.tr("Mute popups for %1").arg(notificationGroup?.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 = notificationGroup?.appName || "";
|
||||
const desktopEntry = notificationGroup?.latestNotification?.desktopEntry || "";
|
||||
if (isMuted) {
|
||||
SettingsData.removeMuteRuleForApp(appName, desktopEntry);
|
||||
} else {
|
||||
SettingsData.addMuteRuleForApp(appName, desktopEntry);
|
||||
NotificationService.dismissGroup(notificationGroup?.key || "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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: NotificationService.dismissGroup(notificationGroup?.key || "")
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
z: -2
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.RightButton && notificationGroup) {
|
||||
notificationCardContextMenu.popup();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,17 @@ DankPopout {
|
||||
|
||||
property bool notificationHistoryVisible: false
|
||||
property var triggerScreen: null
|
||||
property real stablePopupHeight: 400
|
||||
property real _lastAlignedContentHeight: -1
|
||||
|
||||
function updateStablePopupHeight() {
|
||||
const item = contentLoader.item;
|
||||
const target = item ? Theme.px(item.implicitHeight, dpr) : 400;
|
||||
if (Math.abs(target - _lastAlignedContentHeight) < 0.5)
|
||||
return;
|
||||
_lastAlignedContentHeight = target;
|
||||
stablePopupHeight = target;
|
||||
}
|
||||
|
||||
NotificationKeyboardController {
|
||||
id: keyboardController
|
||||
@@ -20,11 +31,12 @@ DankPopout {
|
||||
}
|
||||
}
|
||||
|
||||
popupWidth: 400
|
||||
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 400
|
||||
popupWidth: triggerScreen ? Math.min(500, Math.max(380, triggerScreen.width - 48)) : 400
|
||||
popupHeight: stablePopupHeight
|
||||
positioning: ""
|
||||
animationScaleCollapsed: 1.0
|
||||
animationOffset: 0
|
||||
suspendShadowWhileResizing: true
|
||||
|
||||
screen: triggerScreen
|
||||
shouldBeVisible: notificationHistoryVisible
|
||||
@@ -68,19 +80,32 @@ DankPopout {
|
||||
Connections {
|
||||
target: contentLoader
|
||||
function onLoaded() {
|
||||
root.updateStablePopupHeight();
|
||||
if (root.shouldBeVisible)
|
||||
Qt.callLater(root.setupKeyboardNavigation);
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: contentLoader.item
|
||||
function onImplicitHeightChanged() {
|
||||
root.updateStablePopupHeight();
|
||||
}
|
||||
}
|
||||
|
||||
onDprChanged: updateStablePopupHeight()
|
||||
|
||||
onShouldBeVisibleChanged: {
|
||||
if (shouldBeVisible) {
|
||||
NotificationService.onOverlayOpen();
|
||||
updateStablePopupHeight();
|
||||
if (contentLoader.item)
|
||||
Qt.callLater(setupKeyboardNavigation);
|
||||
} else {
|
||||
NotificationService.onOverlayClose();
|
||||
keyboardController.keyboardNavigationActive = false;
|
||||
NotificationService.expandedGroups = {};
|
||||
NotificationService.expandedMessages = {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +138,7 @@ DankPopout {
|
||||
baseHeight += Theme.spacingM * 2;
|
||||
|
||||
const settingsHeight = notificationSettings.expanded ? notificationSettings.contentHeight : 0;
|
||||
let listHeight = notificationHeader.currentTab === 0 ? notificationList.listContentHeight : Math.max(200, NotificationService.historyList.length * 80);
|
||||
let listHeight = notificationHeader.currentTab === 0 ? notificationList.stableContentHeight : Math.max(200, NotificationService.historyList.length * 80);
|
||||
if (notificationHeader.currentTab === 0 && NotificationService.groupedNotifications.length === 0) {
|
||||
listHeight = 200;
|
||||
}
|
||||
|
||||
@@ -262,6 +262,50 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: Math.max(privacyRow.implicitHeight, privacyToggle.implicitHeight) + Theme.spacingS
|
||||
|
||||
Row {
|
||||
id: privacyRow
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "privacy_tip"
|
||||
size: Theme.iconSizeSmall
|
||||
color: SettingsData.notificationPopupPrivacyMode ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Privacy Mode")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Hide notification content until expanded")
|
||||
font.pixelSize: Theme.fontSizeSmall - 1
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: privacyToggle
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: SettingsData.notificationPopupPrivacyMode
|
||||
onToggled: toggled => SettingsData.set("notificationPopupPrivacyMode", toggled)
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
|
||||
@@ -16,24 +16,41 @@ PanelWindow {
|
||||
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
|
||||
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.spacingS : Theme.spacingM
|
||||
readonly property real popupIconSize: compactMode ? 48 : 63
|
||||
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: 5
|
||||
readonly property real actionButtonHeight: compactMode ? 20 : 24
|
||||
readonly property real collapsedContentHeight: popupIconSize
|
||||
readonly property real basePopupHeight: cardPadding * 2 + collapsedContentHeight + actionButtonHeight + Theme.spacingS
|
||||
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) {
|
||||
@@ -99,22 +116,38 @@ PanelWindow {
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
color: "transparent"
|
||||
implicitWidth: 400
|
||||
implicitWidth: screen ? Math.min(400, Math.max(320, screen.width * 0.23)) : 380
|
||||
implicitHeight: {
|
||||
if (SettingsData.notificationPopupPrivacyMode && !descriptionExpanded)
|
||||
return basePopupHeightPrivacy;
|
||||
if (!descriptionExpanded)
|
||||
return basePopupHeight;
|
||||
const bodyTextHeight = bodyText.contentHeight || 0;
|
||||
const twoLineHeight = Theme.fontSizeSmall * 1.2 * 2;
|
||||
if (bodyTextHeight > twoLineHeight + 2)
|
||||
return basePopupHeight + bodyTextHeight - twoLineHeight;
|
||||
const collapsedBodyHeight = Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2);
|
||||
if (bodyTextHeight > collapsedBodyHeight + 2)
|
||||
return basePopupHeight + bodyTextHeight - collapsedBodyHeight;
|
||||
return basePopupHeight;
|
||||
}
|
||||
|
||||
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);
|
||||
if (SettingsData.notificationPopupPrivacyMode)
|
||||
descriptionExpanded = false;
|
||||
if (hasValidData) {
|
||||
Qt.callLater(() => enterX.restart());
|
||||
} else {
|
||||
@@ -123,6 +156,8 @@ PanelWindow {
|
||||
}
|
||||
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;
|
||||
}
|
||||
@@ -141,9 +176,11 @@ PanelWindow {
|
||||
}
|
||||
|
||||
property bool isTopCenter: SettingsData.notificationPopupPosition === -1
|
||||
property bool isBottomCenter: SettingsData.notificationPopupPosition === SettingsData.Position.BottomCenter
|
||||
property bool isCenterPosition: isTopCenter || isBottomCenter
|
||||
|
||||
anchors.top: isTopCenter || SettingsData.notificationPopupPosition === SettingsData.Position.Top || SettingsData.notificationPopupPosition === SettingsData.Position.Left
|
||||
anchors.bottom: SettingsData.notificationPopupPosition === SettingsData.Position.Bottom || SettingsData.notificationPopupPosition === SettingsData.Position.Right
|
||||
anchors.bottom: isBottomCenter || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom || SettingsData.notificationPopupPosition === SettingsData.Position.Right
|
||||
anchors.left: SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom
|
||||
anchors.right: SettingsData.notificationPopupPosition === SettingsData.Position.Top || SettingsData.notificationPopupPosition === SettingsData.Position.Right
|
||||
|
||||
@@ -182,7 +219,7 @@ PanelWindow {
|
||||
|
||||
function getBottomMargin() {
|
||||
const popupPos = SettingsData.notificationPopupPosition;
|
||||
const isBottom = popupPos === SettingsData.Position.Bottom || popupPos === SettingsData.Position.Right;
|
||||
const isBottom = isBottomCenter || popupPos === SettingsData.Position.Bottom || popupPos === SettingsData.Position.Right;
|
||||
if (!isBottom)
|
||||
return 0;
|
||||
|
||||
@@ -192,7 +229,7 @@ PanelWindow {
|
||||
}
|
||||
|
||||
function getLeftMargin() {
|
||||
if (isTopCenter)
|
||||
if (isCenterPosition)
|
||||
return screen ? (screen.width - implicitWidth) / 2 : 0;
|
||||
|
||||
const popupPos = SettingsData.notificationPopupPosition;
|
||||
@@ -205,7 +242,7 @@ PanelWindow {
|
||||
}
|
||||
|
||||
function getRightMargin() {
|
||||
if (isTopCenter)
|
||||
if (isCenterPosition)
|
||||
return 0;
|
||||
|
||||
const popupPos = SettingsData.notificationPopupPosition;
|
||||
@@ -230,23 +267,51 @@ PanelWindow {
|
||||
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: isTopCenter ? height * 0.4 : width * 0.35
|
||||
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
|
||||
|
||||
property real shadowBlurPx: 10
|
||||
property real shadowSpreadPx: 0
|
||||
property real shadowBaseAlpha: 0.60
|
||||
readonly property real radiusForShadow: Theme.cornerRadius
|
||||
property real shadowBlurPx: SettingsData.notificationPopupShadowEnabled ? ((2 + radiusForShadow * 0.2) * (cardHoverHandler.hovered ? 1.2 : 1)) : 0
|
||||
property real shadowSpreadPx: SettingsData.notificationPopupShadowEnabled ? (radiusForShadow * (cardHoverHandler.hovered ? 0.06 : 0)) : 0
|
||||
property real shadowBaseAlpha: 0.35
|
||||
readonly property real popupSurfaceAlpha: SettingsData.popupTransparency
|
||||
readonly property real effectiveShadowAlpha: Math.max(0, Math.min(1, shadowBaseAlpha * popupSurfaceAlpha))
|
||||
|
||||
Behavior on shadowBlurPx {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on shadowSpreadPx {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: bgShadowLayer
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.snap(4, win.dpr)
|
||||
layer.enabled: !win._isDestroying && win.screenValid
|
||||
layer.enabled: !win._isDestroying && win.screenValid && !implicitHeightAnim.running
|
||||
layer.smooth: false
|
||||
layer.textureSize: Qt.size(Math.round(width * win.dpr), Math.round(height * win.dpr))
|
||||
layer.textureMirroring: ShaderEffectSource.MirrorVertically
|
||||
@@ -256,7 +321,7 @@ PanelWindow {
|
||||
layer.effect: MultiEffect {
|
||||
id: shadowFx
|
||||
autoPaddingEnabled: true
|
||||
shadowEnabled: true
|
||||
shadowEnabled: SettingsData.notificationPopupShadowEnabled
|
||||
blurEnabled: false
|
||||
maskEnabled: false
|
||||
shadowBlur: Math.max(0, Math.min(1, content.shadowBlurPx / bgShadowLayer.blurMax))
|
||||
@@ -268,7 +333,7 @@ PanelWindow {
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: backgroundShape
|
||||
id: shadowShapeSource
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
@@ -278,7 +343,7 @@ PanelWindow {
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: backgroundShape.radius
|
||||
radius: shadowShapeSource.radius
|
||||
visible: notificationData && notificationData.urgency === NotificationUrgency.Critical
|
||||
opacity: 1
|
||||
clip: true
|
||||
@@ -310,6 +375,24 @@ PanelWindow {
|
||||
anchors.margins: Theme.snap(4, win.dpr)
|
||||
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
|
||||
|
||||
@@ -317,16 +400,18 @@ PanelWindow {
|
||||
id: notificationContent
|
||||
|
||||
readonly property real expandedTextHeight: bodyText.contentHeight || 0
|
||||
readonly property real twoLineHeight: Theme.fontSizeSmall * 1.2 * 2
|
||||
readonly property real extraHeight: (descriptionExpanded && expandedTextHeight > twoLineHeight + 2) ? (expandedTextHeight - twoLineHeight) : 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 + (compactMode ? 32 : 40)
|
||||
height: collapsedContentHeight + extraHeight
|
||||
anchors.rightMargin: Theme.spacingL + Theme.notificationHoverRevealMargin
|
||||
height: effectiveCollapsedHeight + extraHeight
|
||||
clip: SettingsData.notificationPopupPrivacyMode && !descriptionExpanded
|
||||
|
||||
DankCircularImage {
|
||||
id: iconContainer
|
||||
@@ -348,6 +433,15 @@ PanelWindow {
|
||||
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)
|
||||
@@ -401,24 +495,40 @@ PanelWindow {
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
spacing: compactMode ? 1 : 2
|
||||
spacing: Theme.notificationContentSpacing
|
||||
|
||||
StyledText {
|
||||
Row {
|
||||
id: headerRow
|
||||
width: parent.width
|
||||
text: {
|
||||
if (!notificationData)
|
||||
return "";
|
||||
const appName = notificationData.appName || "";
|
||||
const timeStr = notificationData.timeStr || "";
|
||||
return timeStr.length > 0 ? appName + " • " + timeStr : appName;
|
||||
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
|
||||
}
|
||||
color: Theme.surfaceVariantText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
maximumLineCount: 1
|
||||
visible: text.length > 0
|
||||
}
|
||||
|
||||
StyledText {
|
||||
@@ -444,8 +554,9 @@ PanelWindow {
|
||||
elide: descriptionExpanded ? Text.ElideNone : Text.ElideRight
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
maximumLineCount: descriptionExpanded ? -1 : (compactMode ? 1 : 2)
|
||||
wrapMode: Text.WordWrap
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
visible: text.length > 0
|
||||
opacity: (SettingsData.notificationPopupPrivacyMode && !descriptionExpanded) ? 0 : 1
|
||||
linkColor: Theme.primary
|
||||
onLinkActivated: link => Qt.openUrlExternally(link)
|
||||
|
||||
@@ -469,6 +580,14 @@ PanelWindow {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -480,16 +599,38 @@ PanelWindow {
|
||||
anchors.topMargin: cardPadding
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
iconName: "close"
|
||||
iconSize: compactMode ? 16 : 18
|
||||
buttonSize: compactMode ? 24 : 28
|
||||
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
|
||||
@@ -497,21 +638,28 @@ PanelWindow {
|
||||
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, compactMode ? 40 : 50)
|
||||
width: Math.max(actionText.implicitWidth + Theme.spacingM, Theme.notificationActionMinWidth)
|
||||
height: actionButtonHeight
|
||||
radius: Theme.spacingXS
|
||||
color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent"
|
||||
radius: Theme.notificationButtonCornerRadius
|
||||
color: isHovered ? Theme.withAlpha(Theme.primary, Theme.stateLayerHover) : "transparent"
|
||||
|
||||
StyledText {
|
||||
id: actionText
|
||||
|
||||
text: modelData.text || "View"
|
||||
text: modelData.text || "Open"
|
||||
color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
@@ -543,15 +691,22 @@ PanelWindow {
|
||||
property bool isHovered: false
|
||||
readonly property int actionCount: notificationData ? (notificationData.actions || []).length : 0
|
||||
|
||||
visible: actionCount < 3
|
||||
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, compactMode ? 40 : 50)
|
||||
width: Math.max(clearTextLabel.implicitWidth + Theme.spacingM, Theme.notificationActionMinWidth)
|
||||
height: actionButtonHeight
|
||||
radius: Theme.spacingXS
|
||||
color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent"
|
||||
radius: Theme.notificationButtonCornerRadius
|
||||
color: isHovered ? Theme.withAlpha(Theme.primary, Theme.stateLayerHover) : "transparent"
|
||||
z: 20
|
||||
|
||||
StyledText {
|
||||
@@ -584,23 +739,19 @@ PanelWindow {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
propagateComposedEvents: true
|
||||
z: -1
|
||||
onEntered: {
|
||||
if (notificationData && notificationData.timer)
|
||||
notificationData.timer.stop();
|
||||
}
|
||||
onExited: {
|
||||
if (notificationData && notificationData.popup && notificationData.timer)
|
||||
notificationData.timer.restart();
|
||||
}
|
||||
onClicked: mouse => {
|
||||
if (!notificationData || win.exiting)
|
||||
return;
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
NotificationService.dismissNotification(notificationData);
|
||||
popupContextMenu.popup();
|
||||
} else if (mouse.button === Qt.LeftButton) {
|
||||
if (notificationData.actions && notificationData.actions.length > 0) {
|
||||
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 {
|
||||
@@ -614,8 +765,8 @@ PanelWindow {
|
||||
DragHandler {
|
||||
id: swipeDragHandler
|
||||
target: null
|
||||
xAxis.enabled: !isTopCenter
|
||||
yAxis.enabled: isTopCenter
|
||||
xAxis.enabled: !isCenterPosition
|
||||
yAxis.enabled: isCenterPosition
|
||||
|
||||
onActiveChanged: {
|
||||
if (active || win.exiting || content.swipeDismissing)
|
||||
@@ -633,9 +784,11 @@ PanelWindow {
|
||||
if (win.exiting)
|
||||
return;
|
||||
|
||||
const raw = isTopCenter ? translation.y : translation.x;
|
||||
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);
|
||||
@@ -643,7 +796,13 @@ PanelWindow {
|
||||
}
|
||||
}
|
||||
|
||||
opacity: 1 - Math.abs(content.swipeOffset) / (isTopCenter ? content.height : content.width * 0.6)
|
||||
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
|
||||
@@ -664,7 +823,7 @@ PanelWindow {
|
||||
id: swipeDismissAnim
|
||||
target: content
|
||||
property: "swipeOffset"
|
||||
to: isTopCenter ? -content.height : (SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom ? -content.width : content.width)
|
||||
to: isTopCenter ? -content.height : isBottomCenter ? content.height : (SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom ? -content.width : content.width)
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Easing.OutCubic
|
||||
onStopped: {
|
||||
@@ -676,18 +835,18 @@ PanelWindow {
|
||||
transform: [
|
||||
Translate {
|
||||
id: swipeTx
|
||||
x: isTopCenter ? 0 : content.swipeOffset
|
||||
y: isTopCenter ? content.swipeOffset : 0
|
||||
x: isCenterPosition ? 0 : content.swipeOffset
|
||||
y: isCenterPosition ? content.swipeOffset : 0
|
||||
},
|
||||
Translate {
|
||||
id: tx
|
||||
x: {
|
||||
if (isTopCenter)
|
||||
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 : 0
|
||||
y: isTopCenter ? -Anims.slidePx : isBottomCenter ? Anims.slidePx : 0
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -696,20 +855,22 @@ PanelWindow {
|
||||
id: enterX
|
||||
|
||||
target: tx
|
||||
property: isTopCenter ? "y" : "x"
|
||||
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.mediumDuration
|
||||
duration: Theme.notificationEnterDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: isTopCenter ? Theme.expressiveCurves.standardDecel : Theme.expressiveCurves.emphasizedDecel
|
||||
easing.bezierCurve: isCenterPosition ? Theme.expressiveCurves.standardDecel : Theme.expressiveCurves.emphasizedDecel
|
||||
onStopped: {
|
||||
if (!win.exiting && !win._isDestroying) {
|
||||
if (isTopCenter) {
|
||||
if (isCenterPosition) {
|
||||
if (Math.abs(tx.y) < 0.5)
|
||||
win.entered();
|
||||
} else {
|
||||
@@ -727,15 +888,17 @@ PanelWindow {
|
||||
|
||||
PropertyAnimation {
|
||||
target: tx
|
||||
property: isTopCenter ? "y" : "x"
|
||||
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.shortDuration
|
||||
duration: Theme.notificationExitDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel
|
||||
}
|
||||
@@ -745,7 +908,7 @@ PanelWindow {
|
||||
property: "opacity"
|
||||
from: 1
|
||||
to: 0
|
||||
duration: Theme.shortDuration
|
||||
duration: Theme.notificationExitDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.standardAccel
|
||||
}
|
||||
@@ -755,7 +918,7 @@ PanelWindow {
|
||||
property: "scale"
|
||||
from: 1
|
||||
to: 0.98
|
||||
duration: Theme.shortDuration
|
||||
duration: Theme.notificationExitDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel
|
||||
}
|
||||
@@ -819,4 +982,98 @@ PanelWindow {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,26 +8,23 @@ QtObject {
|
||||
property var modelData
|
||||
property int topMargin: 0
|
||||
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 cardPadding: compactMode ? Theme.notificationCardPaddingCompact : Theme.notificationCardPadding
|
||||
readonly property real popupIconSize: compactMode ? Theme.notificationIconSizeCompact : Theme.notificationIconSizeNormal
|
||||
readonly property real actionButtonHeight: compactMode ? 20 : 24
|
||||
readonly property real popupSpacing: 4
|
||||
readonly property int baseNotificationHeight: cardPadding * 2 + popupIconSize + actionButtonHeight + Theme.spacingS + popupSpacing
|
||||
property int maxTargetNotifications: 4
|
||||
property var popupWindows: [] // strong refs to windows (live until exitFinished)
|
||||
readonly property real contentSpacing: compactMode ? Theme.spacingXS : Theme.spacingS
|
||||
readonly property real popupSpacing: compactMode ? 0 : Theme.spacingXS
|
||||
readonly property real collapsedContentHeight: Math.max(popupIconSize, Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2))
|
||||
readonly property int baseNotificationHeight: cardPadding * 2 + collapsedContentHeight + actionButtonHeight + contentSpacing + popupSpacing
|
||||
property var popupWindows: []
|
||||
property var destroyingWindows: new Set()
|
||||
property var pendingDestroys: []
|
||||
property int destroyDelayMs: 100
|
||||
property var pendingCreates: []
|
||||
property int createDelayMs: 50
|
||||
property bool createBusy: false
|
||||
property Component popupComponent
|
||||
|
||||
popupComponent: Component {
|
||||
NotificationPopup {
|
||||
onEntered: manager._onPopupEntered(this)
|
||||
onExitStarted: manager._onPopupExitStarted(this)
|
||||
onExitFinished: manager._onPopupExitFinished(this)
|
||||
onPopupHeightChanged: manager._onPopupHeightChanged(this)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,36 +68,6 @@ QtObject {
|
||||
destroyTimer.restart();
|
||||
}
|
||||
|
||||
property Timer createTimer: Timer {
|
||||
interval: createDelayMs
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: manager._processCreateQueue()
|
||||
}
|
||||
|
||||
function _processCreateQueue() {
|
||||
createBusy = false;
|
||||
if (pendingCreates.length === 0)
|
||||
return;
|
||||
const wrapper = pendingCreates.shift();
|
||||
if (wrapper)
|
||||
_doInsertNewestAtTop(wrapper);
|
||||
if (pendingCreates.length > 0) {
|
||||
createBusy = true;
|
||||
createTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
function _scheduleCreate(wrapper) {
|
||||
if (!wrapper)
|
||||
return;
|
||||
pendingCreates.push(wrapper);
|
||||
if (!createBusy) {
|
||||
createBusy = true;
|
||||
createTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
sweeper: Timer {
|
||||
interval: 500
|
||||
running: false
|
||||
@@ -126,14 +93,10 @@ QtObject {
|
||||
}
|
||||
if (toRemove.length) {
|
||||
popupWindows = popupWindows.filter(p => toRemove.indexOf(p) === -1);
|
||||
const survivors = _active().sort((a, b) => a.screenY - b.screenY);
|
||||
for (let k = 0; k < survivors.length; ++k) {
|
||||
survivors[k].screenY = topMargin + k * baseNotificationHeight;
|
||||
}
|
||||
_repositionAll();
|
||||
}
|
||||
if (popupWindows.length === 0) {
|
||||
if (popupWindows.length === 0)
|
||||
sweeper.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,105 +108,29 @@ QtObject {
|
||||
return p && p.status !== Component.Null && !p._isDestroying && p.hasValidData;
|
||||
}
|
||||
|
||||
function _canMakeRoomFor(wrapper) {
|
||||
const activeWindows = _active();
|
||||
if (activeWindows.length < maxTargetNotifications) {
|
||||
return true;
|
||||
}
|
||||
if (!wrapper || !wrapper.notification) {
|
||||
return false;
|
||||
}
|
||||
const incomingUrgency = wrapper.urgency || 0;
|
||||
for (const p of activeWindows) {
|
||||
if (!p.notificationData || !p.notificationData.notification) {
|
||||
continue;
|
||||
}
|
||||
const existingUrgency = p.notificationData.urgency || 0;
|
||||
if (existingUrgency < incomingUrgency) {
|
||||
return true;
|
||||
}
|
||||
if (existingUrgency === incomingUrgency) {
|
||||
const timer = p.notificationData.timer;
|
||||
if (timer && !timer.running) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function _makeRoomForNew(wrapper) {
|
||||
const activeWindows = _active();
|
||||
if (activeWindows.length < maxTargetNotifications) {
|
||||
return;
|
||||
}
|
||||
const toRemove = _selectPopupToRemove(activeWindows, wrapper);
|
||||
if (toRemove && !toRemove.exiting) {
|
||||
toRemove.notificationData.removedByLimit = true;
|
||||
toRemove.notificationData.popup = false;
|
||||
if (toRemove.notificationData.timer) {
|
||||
toRemove.notificationData.timer.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _selectPopupToRemove(activeWindows, incomingWrapper) {
|
||||
const sortedWindows = activeWindows.slice().sort((a, b) => {
|
||||
const aUrgency = (a.notificationData) ? a.notificationData.urgency || 0 : 0;
|
||||
const bUrgency = (b.notificationData) ? b.notificationData.urgency || 0 : 0;
|
||||
if (aUrgency !== bUrgency) {
|
||||
return aUrgency - bUrgency;
|
||||
}
|
||||
const aTimer = a.notificationData && a.notificationData.timer;
|
||||
const bTimer = b.notificationData && b.notificationData.timer;
|
||||
const aRunning = aTimer && aTimer.running;
|
||||
const bRunning = bTimer && bTimer.running;
|
||||
if (aRunning !== bRunning) {
|
||||
return aRunning ? 1 : -1;
|
||||
}
|
||||
return b.screenY - a.screenY;
|
||||
});
|
||||
return sortedWindows[0];
|
||||
}
|
||||
|
||||
function _sync(newWrappers) {
|
||||
for (const w of newWrappers) {
|
||||
if (w && !_hasWindowFor(w)) {
|
||||
insertNewestAtTop(w);
|
||||
}
|
||||
}
|
||||
for (const p of popupWindows.slice()) {
|
||||
if (!_isValidWindow(p)) {
|
||||
if (!_isValidWindow(p) || p.exiting)
|
||||
continue;
|
||||
}
|
||||
if (p.notificationData && newWrappers.indexOf(p.notificationData) === -1 && !p.exiting) {
|
||||
if (p.notificationData && newWrappers.indexOf(p.notificationData) === -1) {
|
||||
p.notificationData.removedByLimit = true;
|
||||
p.notificationData.popup = false;
|
||||
}
|
||||
}
|
||||
for (const w of newWrappers) {
|
||||
if (w && !_hasWindowFor(w))
|
||||
_insertAtTop(w);
|
||||
}
|
||||
}
|
||||
|
||||
function insertNewestAtTop(wrapper) {
|
||||
if (!wrapper)
|
||||
return;
|
||||
if (createBusy || pendingCreates.length > 0) {
|
||||
_scheduleCreate(wrapper);
|
||||
return;
|
||||
}
|
||||
_doInsertNewestAtTop(wrapper);
|
||||
function _popupHeight(p) {
|
||||
return (p.alignedHeight || p.implicitHeight || (baseNotificationHeight - popupSpacing)) + popupSpacing;
|
||||
}
|
||||
|
||||
function _doInsertNewestAtTop(wrapper) {
|
||||
function _insertAtTop(wrapper) {
|
||||
if (!wrapper)
|
||||
return;
|
||||
for (const p of popupWindows) {
|
||||
if (!_isValidWindow(p))
|
||||
continue;
|
||||
if (p.exiting)
|
||||
continue;
|
||||
p.screenY = p.screenY + baseNotificationHeight;
|
||||
}
|
||||
const notificationId = wrapper && wrapper.notification ? wrapper.notification.id : "";
|
||||
const notificationId = wrapper?.notification ? wrapper.notification.id : "";
|
||||
const win = popupComponent.createObject(null, {
|
||||
"notificationData": wrapper,
|
||||
"notificationId": notificationId,
|
||||
@@ -256,72 +143,70 @@ QtObject {
|
||||
win.destroy();
|
||||
return;
|
||||
}
|
||||
popupWindows.push(win);
|
||||
createBusy = true;
|
||||
createTimer.restart();
|
||||
popupWindows.unshift(win);
|
||||
_repositionAll();
|
||||
if (!sweeper.running)
|
||||
sweeper.start();
|
||||
}
|
||||
|
||||
function _active() {
|
||||
return popupWindows.filter(p => _isValidWindow(p) && p.notificationData && p.notificationData.popup && !p.exiting);
|
||||
}
|
||||
function _repositionAll() {
|
||||
const active = popupWindows.filter(p => _isValidWindow(p) && p.notificationData?.popup && !p.exiting);
|
||||
|
||||
function _bottom() {
|
||||
let b = null;
|
||||
let maxY = -1;
|
||||
for (const p of _active()) {
|
||||
if (p.screenY > maxY) {
|
||||
maxY = p.screenY;
|
||||
b = p;
|
||||
}
|
||||
const pinnedSlots = [];
|
||||
for (const p of active) {
|
||||
if (!p.hovered)
|
||||
continue;
|
||||
pinnedSlots.push({
|
||||
y: p.screenY,
|
||||
end: p.screenY + _popupHeight(p)
|
||||
});
|
||||
}
|
||||
pinnedSlots.sort((a, b) => a.y - b.y);
|
||||
|
||||
let currentY = topMargin;
|
||||
for (const win of active) {
|
||||
if (win.hovered)
|
||||
continue;
|
||||
for (const slot of pinnedSlots) {
|
||||
if (currentY >= slot.y - 1 && currentY < slot.end)
|
||||
currentY = slot.end;
|
||||
}
|
||||
win.screenY = currentY;
|
||||
currentY += _popupHeight(win);
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
function _onPopupEntered(p) {
|
||||
}
|
||||
|
||||
function _onPopupExitStarted(p) {
|
||||
if (!p)
|
||||
function _onPopupHeightChanged(p) {
|
||||
if (!p || p.exiting || p._isDestroying)
|
||||
return;
|
||||
const survivors = _active().sort((a, b) => a.screenY - b.screenY);
|
||||
for (let k = 0; k < survivors.length; ++k)
|
||||
survivors[k].screenY = topMargin + k * baseNotificationHeight;
|
||||
if (popupWindows.indexOf(p) === -1)
|
||||
return;
|
||||
_repositionAll();
|
||||
}
|
||||
|
||||
function _onPopupExitFinished(p) {
|
||||
if (!p) {
|
||||
if (!p)
|
||||
return;
|
||||
}
|
||||
const windowId = p.toString();
|
||||
if (destroyingWindows.has(windowId)) {
|
||||
if (destroyingWindows.has(windowId))
|
||||
return;
|
||||
}
|
||||
destroyingWindows.add(windowId);
|
||||
const i = popupWindows.indexOf(p);
|
||||
if (i !== -1) {
|
||||
popupWindows.splice(i, 1);
|
||||
popupWindows = popupWindows.slice();
|
||||
}
|
||||
if (NotificationService.releaseWrapper && p.notificationData) {
|
||||
if (NotificationService.releaseWrapper && p.notificationData)
|
||||
NotificationService.releaseWrapper(p.notificationData);
|
||||
}
|
||||
_scheduleDestroy(p);
|
||||
Qt.callLater(() => destroyingWindows.delete(windowId));
|
||||
const survivors = _active().sort((a, b) => a.screenY - b.screenY);
|
||||
for (let k = 0; k < survivors.length; ++k) {
|
||||
survivors[k].screenY = topMargin + k * baseNotificationHeight;
|
||||
}
|
||||
_repositionAll();
|
||||
}
|
||||
|
||||
function cleanupAllWindows() {
|
||||
sweeper.stop();
|
||||
destroyTimer.stop();
|
||||
createTimer.stop();
|
||||
pendingDestroys = [];
|
||||
pendingCreates = [];
|
||||
createBusy = false;
|
||||
for (const p of popupWindows.slice()) {
|
||||
if (p) {
|
||||
try {
|
||||
|
||||
@@ -6,6 +6,25 @@ import qs.Modules.Settings.Widgets
|
||||
Item {
|
||||
id: root
|
||||
|
||||
Component.onCompleted: {
|
||||
if (SettingsData._pendingExpandNotificationRules) {
|
||||
SettingsData._pendingExpandNotificationRules = false;
|
||||
notificationRulesCard.userToggledCollapse = true;
|
||||
notificationRulesCard.expanded = true;
|
||||
SettingsData._pendingNotificationRuleIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
readonly property var mutedRules: {
|
||||
var rules = SettingsData.notificationRules || [];
|
||||
var out = [];
|
||||
for (var i = 0; i < rules.length; i++) {
|
||||
if ((rules[i].action || "").toString().toLowerCase() === "mute")
|
||||
out.push({ rule: rules[i], index: i });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
readonly property var timeoutOptions: [
|
||||
{
|
||||
text: I18n.tr("Never"),
|
||||
@@ -201,22 +220,33 @@ Item {
|
||||
return I18n.tr("Top Left", "screen position option");
|
||||
case SettingsData.Position.Right:
|
||||
return I18n.tr("Bottom Right", "screen position option");
|
||||
case SettingsData.Position.BottomCenter:
|
||||
return I18n.tr("Bottom Center", "screen position option");
|
||||
default:
|
||||
return I18n.tr("Top Right", "screen position option");
|
||||
}
|
||||
}
|
||||
options: [I18n.tr("Top Right", "screen position option"), I18n.tr("Top Left", "screen position option"), I18n.tr("Top Center", "screen position option"), I18n.tr("Bottom Right", "screen position option"), I18n.tr("Bottom Left", "screen position option")]
|
||||
options: [I18n.tr("Top Right", "screen position option"), I18n.tr("Top Left", "screen position option"), I18n.tr("Top Center", "screen position option"), I18n.tr("Bottom Center", "screen position option"), I18n.tr("Bottom Right", "screen position option"), I18n.tr("Bottom Left", "screen position option")]
|
||||
onValueChanged: value => {
|
||||
if (value === I18n.tr("Top Right", "screen position option")) {
|
||||
switch (value) {
|
||||
case I18n.tr("Top Right", "screen position option"):
|
||||
SettingsData.set("notificationPopupPosition", SettingsData.Position.Top);
|
||||
} else if (value === I18n.tr("Top Left", "screen position option")) {
|
||||
break;
|
||||
case I18n.tr("Top Left", "screen position option"):
|
||||
SettingsData.set("notificationPopupPosition", SettingsData.Position.Left);
|
||||
} else if (value === I18n.tr("Top Center", "screen position option")) {
|
||||
break;
|
||||
case I18n.tr("Top Center", "screen position option"):
|
||||
SettingsData.set("notificationPopupPosition", -1);
|
||||
} else if (value === I18n.tr("Bottom Right", "screen position option")) {
|
||||
break;
|
||||
case I18n.tr("Bottom Center", "screen position option"):
|
||||
SettingsData.set("notificationPopupPosition", SettingsData.Position.BottomCenter);
|
||||
break;
|
||||
case I18n.tr("Bottom Right", "screen position option"):
|
||||
SettingsData.set("notificationPopupPosition", SettingsData.Position.Right);
|
||||
} else if (value === I18n.tr("Bottom Left", "screen position option")) {
|
||||
break;
|
||||
case I18n.tr("Bottom Left", "screen position option"):
|
||||
SettingsData.set("notificationPopupPosition", SettingsData.Position.Bottom);
|
||||
break;
|
||||
}
|
||||
SettingsData.sendTestNotifications();
|
||||
}
|
||||
@@ -239,6 +269,95 @@ Item {
|
||||
checked: SettingsData.notificationCompactMode
|
||||
onToggled: checked => SettingsData.set("notificationCompactMode", checked)
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "notificationPopupShadowEnabled"
|
||||
tags: ["notification", "popup", "shadow", "radius", "rounded"]
|
||||
text: I18n.tr("Popup Shadow")
|
||||
description: I18n.tr("Show drop shadow on notification popups")
|
||||
checked: SettingsData.notificationPopupShadowEnabled
|
||||
onToggled: checked => SettingsData.set("notificationPopupShadowEnabled", checked)
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "notificationPopupPrivacyMode"
|
||||
tags: ["notification", "popup", "privacy", "body", "content", "hide"]
|
||||
text: I18n.tr("Privacy Mode")
|
||||
description: I18n.tr("Hide notification content until expanded; popups show collapsed by default")
|
||||
checked: SettingsData.notificationPopupPrivacyMode
|
||||
onToggled: checked => SettingsData.set("notificationPopupPrivacyMode", checked)
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: notificationAnimationColumn.implicitHeight + Theme.spacingM * 2
|
||||
|
||||
Column {
|
||||
id: notificationAnimationColumn
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
x: Theme.spacingM
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Animation Speed")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Control animation duration for notification popups and history")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
DankButtonGroup {
|
||||
id: notificationSpeedGroup
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
buttonPadding: parent.width < 480 ? Theme.spacingS : Theme.spacingM
|
||||
minButtonWidth: parent.width < 480 ? 44 : 56
|
||||
textSize: parent.width < 480 ? Theme.fontSizeSmall : Theme.fontSizeMedium
|
||||
model: [I18n.tr("None"), I18n.tr("Short"), I18n.tr("Medium"), I18n.tr("Long"), I18n.tr("Custom")]
|
||||
selectionMode: "single"
|
||||
currentIndex: SettingsData.notificationAnimationSpeed
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (!selected)
|
||||
return;
|
||||
SettingsData.set("notificationAnimationSpeed", index);
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onNotificationAnimationSpeedChanged() {
|
||||
notificationSpeedGroup.currentIndex = SettingsData.notificationAnimationSpeed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsSliderRow {
|
||||
settingKey: "notificationCustomAnimationDuration"
|
||||
tags: ["notification", "animation", "duration", "custom", "speed"]
|
||||
text: I18n.tr("Duration")
|
||||
description: I18n.tr("Base duration for animations (drag to use Custom)")
|
||||
minimum: 100
|
||||
maximum: 800
|
||||
value: Theme.notificationAnimationBaseDuration
|
||||
unit: "ms"
|
||||
defaultValue: 400
|
||||
onSliderValueChanged: newValue => {
|
||||
if (SettingsData.notificationAnimationSpeed !== SettingsData.AnimationSpeed.Custom) {
|
||||
SettingsData.set("notificationAnimationSpeed", SettingsData.AnimationSpeed.Custom);
|
||||
}
|
||||
SettingsData.set("notificationCustomAnimationDuration", newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
@@ -258,6 +377,7 @@ Item {
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
id: notificationRulesCard
|
||||
width: parent.width
|
||||
iconName: "rule_settings"
|
||||
title: I18n.tr("Notification Rules")
|
||||
@@ -282,7 +402,11 @@ Item {
|
||||
iconSize: 20
|
||||
backgroundColor: Theme.surfaceContainer
|
||||
iconColor: Theme.primary
|
||||
onClicked: SettingsData.addNotificationRule()
|
||||
onClicked: {
|
||||
SettingsData.addNotificationRule();
|
||||
notificationRulesCard.userToggledCollapse = true;
|
||||
notificationRulesCard.expanded = true;
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -291,7 +415,7 @@ Item {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Create rules to mute, ignore, hide from history, or override notification priority.")
|
||||
text: I18n.tr("Create rules to mute, ignore, hide from history, or override notification priority. Default only overrides priority; notifications still show normally.")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
@@ -407,6 +531,7 @@ Item {
|
||||
width: parent.width
|
||||
compactMode: true
|
||||
dropdownWidth: parent.width
|
||||
popupWidth: 165
|
||||
currentValue: root.getRuleOptionLabel(root.notificationRuleFieldOptions, modelData.field, root.notificationRuleFieldOptions[0].label)
|
||||
options: root.notificationRuleFieldOptions.map(o => o.label)
|
||||
onValueChanged: value => SettingsData.updateNotificationRuleField(index, "field", root.getRuleOptionValue(root.notificationRuleFieldOptions, value, "appName"))
|
||||
@@ -447,6 +572,7 @@ Item {
|
||||
width: parent.width
|
||||
compactMode: true
|
||||
dropdownWidth: parent.width
|
||||
popupWidth: 170
|
||||
currentValue: root.getRuleOptionLabel(root.notificationRuleActionOptions, modelData.action, root.notificationRuleActionOptions[0].label)
|
||||
options: root.notificationRuleActionOptions.map(o => o.label)
|
||||
onValueChanged: value => SettingsData.updateNotificationRuleField(index, "action", root.getRuleOptionValue(root.notificationRuleActionOptions, value, "default"))
|
||||
@@ -467,6 +593,7 @@ Item {
|
||||
width: parent.width
|
||||
compactMode: true
|
||||
dropdownWidth: parent.width
|
||||
popupWidth: 165
|
||||
currentValue: root.getRuleOptionLabel(root.notificationRuleUrgencyOptions, modelData.urgency, root.notificationRuleUrgencyOptions[0].label)
|
||||
options: root.notificationRuleUrgencyOptions.map(o => o.label)
|
||||
onValueChanged: value => SettingsData.updateNotificationRuleField(index, "urgency", root.getRuleOptionValue(root.notificationRuleUrgencyOptions, value, "default"))
|
||||
@@ -479,6 +606,95 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "volume_off"
|
||||
title: I18n.tr("Muted Apps")
|
||||
settingKey: "mutedApps"
|
||||
tags: ["notification", "mute", "unmute", "popup"]
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: mutedRules.length > 0 ? I18n.tr("Apps with notification popups muted. Unmute or delete to remove.") : I18n.tr("No apps muted. Right-click a notification and choose \"Mute popups\" to add one here.")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
bottomPadding: Theme.spacingS
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: mutedRules
|
||||
|
||||
delegate: Rectangle {
|
||||
width: parent.width
|
||||
height: mutedRow.implicitHeight + Theme.spacingS * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, 0.5)
|
||||
|
||||
Row {
|
||||
id: mutedRow
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
id: mutedAppLabel
|
||||
text: (modelData.rule && modelData.rule.pattern) ? modelData.rule.pattern : I18n.tr("Unknown")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
width: Math.max(0, parent.width - parent.spacing - mutedAppLabel.width - unmuteBtn.width - deleteBtn.width - Theme.spacingS * 5)
|
||||
height: 1
|
||||
}
|
||||
|
||||
DankButton {
|
||||
id: unmuteBtn
|
||||
text: I18n.tr("Unmute")
|
||||
backgroundColor: Theme.surfaceContainer
|
||||
textColor: Theme.primary
|
||||
onClicked: SettingsData.removeNotificationRule(modelData.index)
|
||||
}
|
||||
|
||||
Item {
|
||||
id: deleteBtn
|
||||
width: 28
|
||||
height: 28
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: deleteArea.containsMouse ? Theme.withAlpha(Theme.error, 0.2) : "transparent"
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "delete"
|
||||
size: 18
|
||||
color: deleteArea.containsMouse ? Theme.error : Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: deleteArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: SettingsData.removeNotificationRule(modelData.index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "lock"
|
||||
|
||||
@@ -22,7 +22,7 @@ Singleton {
|
||||
|
||||
property list<NotifWrapper> notificationQueue: []
|
||||
property list<NotifWrapper> visibleNotifications: []
|
||||
property int maxVisibleNotifications: 3
|
||||
property int maxVisibleNotifications: 4
|
||||
property bool addGateBusy: false
|
||||
property int enterAnimMs: 400
|
||||
property int seqCounter: 0
|
||||
@@ -158,10 +158,7 @@ Singleton {
|
||||
continue;
|
||||
const urg = typeof item.urgency === "number" ? item.urgency : 1;
|
||||
const body = item.body || "";
|
||||
let htmlBody = item.htmlBody || "";
|
||||
if (!htmlBody && body) {
|
||||
htmlBody = (body.includes('<') && body.includes('>')) ? body : Markdown2Html.markdownToHtml(body);
|
||||
}
|
||||
const htmlBody = item.htmlBody || _resolveHtmlBody(body);
|
||||
loaded.push({
|
||||
id: item.id || "",
|
||||
summary: item.summary || "",
|
||||
@@ -251,9 +248,15 @@ Singleton {
|
||||
const timeStr = SettingsData.use24HourClock ? date.toLocaleTimeString(Qt.locale(), "HH:mm") : date.toLocaleTimeString(Qt.locale(), "h:mm AP");
|
||||
if (daysDiff === 0)
|
||||
return timeStr;
|
||||
if (daysDiff === 1)
|
||||
return I18n.tr("yesterday") + ", " + timeStr;
|
||||
return I18n.tr("%1 days ago").arg(daysDiff);
|
||||
try {
|
||||
const localeName = (typeof Qt !== "undefined" && Qt.locale) ? Qt.locale().name : "en-US";
|
||||
const weekday = date.toLocaleDateString(localeName, {
|
||||
weekday: "long"
|
||||
});
|
||||
return weekday + ", " + timeStr;
|
||||
} catch (e) {
|
||||
return timeStr;
|
||||
}
|
||||
}
|
||||
|
||||
function _nowSec() {
|
||||
@@ -484,7 +487,7 @@ Singleton {
|
||||
|
||||
Timer {
|
||||
id: addGate
|
||||
interval: enterAnimMs + 50
|
||||
interval: 80
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
@@ -688,11 +691,15 @@ Singleton {
|
||||
return formatTime(time);
|
||||
}
|
||||
|
||||
if (daysDiff === 1) {
|
||||
return `yesterday, ${formatTime(time)}`;
|
||||
try {
|
||||
const localeName = (typeof Qt !== "undefined" && Qt.locale) ? Qt.locale().name : "en-US";
|
||||
const weekday = time.toLocaleDateString(localeName, {
|
||||
weekday: "long"
|
||||
});
|
||||
return `${weekday}, ${formatTime(time)}`;
|
||||
} catch (e) {
|
||||
return formatTime(time);
|
||||
}
|
||||
|
||||
return `${daysDiff} days ago`;
|
||||
}
|
||||
|
||||
function formatTime(date) {
|
||||
@@ -715,13 +722,7 @@ Singleton {
|
||||
required property Notification notification
|
||||
readonly property string summary: notification?.summary ?? ""
|
||||
readonly property string body: notification?.body ?? ""
|
||||
readonly property string htmlBody: {
|
||||
if (!body)
|
||||
return "";
|
||||
if (body.includes('<') && body.includes('>'))
|
||||
return body;
|
||||
return Markdown2Html.markdownToHtml(body);
|
||||
}
|
||||
readonly property string htmlBody: root._resolveHtmlBody(body)
|
||||
readonly property string appIcon: notification?.appIcon ?? ""
|
||||
readonly property string appName: {
|
||||
if (!notification)
|
||||
@@ -837,39 +838,54 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
function processQueue() {
|
||||
if (addGateBusy) {
|
||||
return;
|
||||
}
|
||||
if (popupsDisabled) {
|
||||
return;
|
||||
}
|
||||
if (SessionData.doNotDisturb) {
|
||||
return;
|
||||
}
|
||||
if (notificationQueue.length === 0) {
|
||||
return;
|
||||
}
|
||||
property bool _processingQueue: false
|
||||
|
||||
const activePopupCount = visibleNotifications.filter(n => n && n.popup).length;
|
||||
if (activePopupCount >= 4) {
|
||||
function processQueue() {
|
||||
if (addGateBusy || _processingQueue)
|
||||
return;
|
||||
}
|
||||
if (popupsDisabled)
|
||||
return;
|
||||
if (SessionData.doNotDisturb)
|
||||
return;
|
||||
if (notificationQueue.length === 0)
|
||||
return;
|
||||
|
||||
_processingQueue = true;
|
||||
|
||||
const next = notificationQueue.shift();
|
||||
if (!next)
|
||||
if (!next) {
|
||||
_processingQueue = false;
|
||||
return;
|
||||
}
|
||||
|
||||
next.seq = ++seqCounter;
|
||||
visibleNotifications = [...visibleNotifications, next];
|
||||
|
||||
const activePopups = visibleNotifications.filter(n => n && n.popup);
|
||||
let evicted = null;
|
||||
if (activePopups.length >= maxVisibleNotifications) {
|
||||
const unhovered = activePopups.filter(n => n.timer?.running);
|
||||
const pool = unhovered.length > 0 ? unhovered : activePopups;
|
||||
evicted = pool.reduce((min, n) => (n.seq < min.seq) ? n : min, pool[0]);
|
||||
if (evicted)
|
||||
evicted.removedByLimit = true;
|
||||
}
|
||||
|
||||
if (evicted) {
|
||||
visibleNotifications = [...visibleNotifications.filter(n => n !== evicted), next];
|
||||
} else {
|
||||
visibleNotifications = [...visibleNotifications, next];
|
||||
}
|
||||
|
||||
if (evicted)
|
||||
evicted.popup = false;
|
||||
next.popup = true;
|
||||
|
||||
if (next.timer.interval > 0) {
|
||||
if (next.timer.interval > 0)
|
||||
next.timer.start();
|
||||
}
|
||||
|
||||
addGateBusy = true;
|
||||
addGate.restart();
|
||||
_processingQueue = false;
|
||||
}
|
||||
|
||||
function removeFromVisibleNotifications(wrapper) {
|
||||
@@ -890,6 +906,96 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
function _decodeEntities(s) {
|
||||
s = s.replace(/&#(\d+);/g, (_, n) => String.fromCodePoint(parseInt(n, 10)));
|
||||
s = s.replace(/&#x([0-9a-fA-F]+);/g, (_, n) => String.fromCodePoint(parseInt(n, 16)));
|
||||
return s.replace(/&([a-zA-Z][a-zA-Z0-9]*);/g, (match, name) => {
|
||||
switch (name) {
|
||||
case "amp":
|
||||
return "&";
|
||||
case "lt":
|
||||
return "<";
|
||||
case "gt":
|
||||
return ">";
|
||||
case "quot":
|
||||
return "\"";
|
||||
case "apos":
|
||||
return "'";
|
||||
case "nbsp":
|
||||
return "\u00A0";
|
||||
case "ndash":
|
||||
return "\u2013";
|
||||
case "mdash":
|
||||
return "\u2014";
|
||||
case "lsquo":
|
||||
return "\u2018";
|
||||
case "rsquo":
|
||||
return "\u2019";
|
||||
case "ldquo":
|
||||
return "\u201C";
|
||||
case "rdquo":
|
||||
return "\u201D";
|
||||
case "bull":
|
||||
return "\u2022";
|
||||
case "hellip":
|
||||
return "\u2026";
|
||||
case "trade":
|
||||
return "\u2122";
|
||||
case "copy":
|
||||
return "\u00A9";
|
||||
case "reg":
|
||||
return "\u00AE";
|
||||
case "deg":
|
||||
return "\u00B0";
|
||||
case "plusmn":
|
||||
return "\u00B1";
|
||||
case "times":
|
||||
return "\u00D7";
|
||||
case "divide":
|
||||
return "\u00F7";
|
||||
case "micro":
|
||||
return "\u00B5";
|
||||
case "middot":
|
||||
return "\u00B7";
|
||||
case "laquo":
|
||||
return "\u00AB";
|
||||
case "raquo":
|
||||
return "\u00BB";
|
||||
case "larr":
|
||||
return "\u2190";
|
||||
case "rarr":
|
||||
return "\u2192";
|
||||
case "uarr":
|
||||
return "\u2191";
|
||||
case "darr":
|
||||
return "\u2193";
|
||||
default:
|
||||
return match;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function _resolveHtmlBody(body) {
|
||||
if (!body)
|
||||
return "";
|
||||
if (/<\/?[a-z][\s\S]*>/i.test(body))
|
||||
return body;
|
||||
|
||||
// Decode percent-encoded URLs (e.g. https%3A%2F%2F → https://)
|
||||
body = body.replace(/\bhttps?%3A%2F%2F[^\s]+/gi, match => {
|
||||
try { return decodeURIComponent(match); }
|
||||
catch (e) { return match; }
|
||||
});
|
||||
|
||||
if (/&(#\d+|#x[0-9a-fA-F]+|[a-zA-Z][a-zA-Z0-9]+);/.test(body)) {
|
||||
const decoded = _decodeEntities(body);
|
||||
if (/<\/?[a-z][\s\S]*>/i.test(decoded))
|
||||
return decoded;
|
||||
return Markdown2Html.markdownToHtml(decoded);
|
||||
}
|
||||
return Markdown2Html.markdownToHtml(body);
|
||||
}
|
||||
|
||||
function getGroupKey(wrapper) {
|
||||
if (wrapper.desktopEntry && wrapper.desktopEntry !== "") {
|
||||
return wrapper.desktopEntry.toLowerCase();
|
||||
|
||||
@@ -55,6 +55,10 @@ Item {
|
||||
|
||||
signal valueChanged(string value)
|
||||
|
||||
function closeDropdownMenu() {
|
||||
dropdownMenu.close();
|
||||
}
|
||||
|
||||
width: compactMode ? dropdownWidth : parent.width
|
||||
implicitHeight: compactMode ? 40 : Math.max(60, labelColumn.implicitHeight + Theme.spacingM)
|
||||
|
||||
@@ -409,7 +413,7 @@ Item {
|
||||
onClicked: {
|
||||
root.currentValue = delegateRoot.modelData;
|
||||
root.valueChanged(delegateRoot.modelData);
|
||||
dropdownMenu.close();
|
||||
root.closeDropdownMenu();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,10 +25,12 @@ Item {
|
||||
property real animationOffset: Theme.spacingL
|
||||
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
|
||||
property bool suspendShadowWhileResizing: false
|
||||
property bool shouldBeVisible: false
|
||||
property var customKeyboardFocus: null
|
||||
property bool backgroundInteractive: true
|
||||
property bool contentHandlesKeys: false
|
||||
property bool _resizeActive: false
|
||||
|
||||
property real storedBarThickness: Theme.barHeight - 4
|
||||
property real storedBarSpacing: 4
|
||||
@@ -185,6 +187,26 @@ Item {
|
||||
readonly property real alignedWidth: Theme.px(popupWidth, dpr)
|
||||
readonly property real alignedHeight: Theme.px(popupHeight, dpr)
|
||||
|
||||
onAlignedHeightChanged: {
|
||||
if (!suspendShadowWhileResizing || !shouldBeVisible)
|
||||
return;
|
||||
_resizeActive = true;
|
||||
resizeSettleTimer.restart();
|
||||
}
|
||||
onShouldBeVisibleChanged: {
|
||||
if (!shouldBeVisible) {
|
||||
_resizeActive = false;
|
||||
resizeSettleTimer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: resizeSettleTimer
|
||||
interval: 80
|
||||
repeat: false
|
||||
onTriggered: root._resizeActive = false
|
||||
}
|
||||
|
||||
readonly property real alignedX: Theme.snap((() => {
|
||||
const useAutoGaps = storedBarConfig?.popupGapsAuto !== undefined ? storedBarConfig.popupGapsAuto : true;
|
||||
const manualGapValue = storedBarConfig?.popupGapsManual !== undefined ? storedBarConfig.popupGapsManual : 4;
|
||||
@@ -440,7 +462,7 @@ Item {
|
||||
readonly property real effectiveShadowAlpha: Math.max(0, Math.min(1, shadowBaseAlpha * popupSurfaceAlpha))
|
||||
readonly property int blurMax: 64
|
||||
|
||||
layer.enabled: Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
||||
layer.enabled: Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" && !(root.suspendShadowWhileResizing && root._resizeActive)
|
||||
layer.smooth: false
|
||||
layer.textureSize: root.dpr > 1 ? Qt.size(Math.ceil(width * root.dpr), Math.ceil(height * root.dpr)) : Qt.size(0, 0)
|
||||
|
||||
|
||||
@@ -4677,6 +4677,50 @@
|
||||
],
|
||||
"description": "Use smaller notification cards"
|
||||
},
|
||||
{
|
||||
"section": "notificationAnimationSpeed",
|
||||
"label": "Animation Speed",
|
||||
"tabIndex": 17,
|
||||
"category": "Notifications",
|
||||
"keywords": [
|
||||
"alert",
|
||||
"animate",
|
||||
"animation",
|
||||
"duration",
|
||||
"fast",
|
||||
"messages",
|
||||
"motion",
|
||||
"notif",
|
||||
"notification",
|
||||
"notifications",
|
||||
"popup",
|
||||
"speed",
|
||||
"toast"
|
||||
],
|
||||
"description": "Control animation duration for notification popups and history"
|
||||
},
|
||||
{
|
||||
"section": "notificationCustomAnimationDuration",
|
||||
"label": "Animation Duration",
|
||||
"tabIndex": 17,
|
||||
"category": "Notifications",
|
||||
"keywords": [
|
||||
"alert",
|
||||
"animate",
|
||||
"animation",
|
||||
"custom",
|
||||
"duration",
|
||||
"messages",
|
||||
"ms",
|
||||
"notif",
|
||||
"notification",
|
||||
"notifications",
|
||||
"popup",
|
||||
"speed",
|
||||
"toast"
|
||||
],
|
||||
"description": "Base duration for notification animations"
|
||||
},
|
||||
{
|
||||
"section": "notificationHistorySaveCritical",
|
||||
"label": "Critical Priority",
|
||||
|
||||
Reference in New Issue
Block a user