1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-04 04:42:05 -04:00

notifications: Update Material 3 baselines

- New right-click to mute option
- New independent Notification Animation settings
This commit is contained in:
purian23
2026-02-12 22:16:16 -05:00
committed by bbedward
parent 8399d64c2d
commit 0ffeed3ff0
11 changed files with 350 additions and 48 deletions

View File

@@ -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{

View File

@@ -501,6 +501,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
@@ -2152,6 +2154,24 @@ Singleton {
saveSettings();
}
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"
});
notificationRules = rules;
saveSettings();
}
function updateNotificationRule(index, ruleData) {
var rules = JSON.parse(JSON.stringify(notificationRules || []));
if (index < 0 || index >= rules.length)

View File

@@ -776,6 +776,37 @@ 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.4);
}
readonly property int notificationCollapseDuration: {
const base = notificationAnimationBaseDuration;
return base === 0 ? 0 : Math.round(base * 1.1);
}
readonly property int popoutAnimationDuration: {
if (typeof SettingsData === "undefined")
return 150;

View File

@@ -325,6 +325,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 },

View File

@@ -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()

View File

@@ -1,4 +1,5 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Services.Notifications
import qs.Common
@@ -215,26 +216,22 @@ Rectangle {
spacing: compactMode ? 1 : 2
StyledText {
width: parent.width
text: {
const timeStr = (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.timeStr) || "";
const appName = (notificationGroup && notificationGroup.appName) || "";
return timeStr.length > 0 ? `${appName} ${timeStr}` : appName;
}
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
text: (notificationGroup && notificationGroup.latestNotification && 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
}
StyledText {
text: (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.summary) || ""
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
width: parent.width
text: (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.timeStr) || ""
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
elide: Text.ElideRight
maximumLineCount: 1
visible: text.length > 0
@@ -541,7 +538,7 @@ Rectangle {
StyledText {
id: expandedActionText
text: {
const baseText = modelData.text || "View";
const baseText = modelData.text || "Open";
if (keyboardNavigationActive && (isGroupSelected || selectedNotificationIndex >= 0))
return `${baseText} (${index + 1})`;
return baseText;
@@ -624,7 +621,7 @@ Rectangle {
StyledText {
id: collapsedActionText
text: {
const baseText = modelData.text || "View";
const baseText = modelData.text || "Open";
if (keyboardNavigationActive && isGroupSelected) {
return `${baseText} (${index + 1})`;
}
@@ -733,9 +730,9 @@ Rectangle {
Behavior on height {
enabled: 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: root.expanded ? Theme.expressiveCurves.emphasizedDecel : Theme.expressiveCurves.emphasizedAccel
onRunningChanged: {
if (running) {
root.isAnimating = true;
@@ -746,4 +743,71 @@ Rectangle {
}
}
}
Menu {
id: notificationCardContextMenu
width: 220
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
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 {
text: 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 || "";
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: 10
onClicked: mouse => {
if (mouse.button === Qt.RightButton && notificationGroup) {
notificationCardContextMenu.popup();
}
}
}
}

View File

@@ -20,7 +20,7 @@ DankPopout {
}
}
popupWidth: 400
popupWidth: triggerScreen ? Math.min(500, Math.max(380, triggerScreen.width - 48)) : 400
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 400
positioning: ""
animationScaleCollapsed: 1.0

View File

@@ -99,7 +99,7 @@ PanelWindow {
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
implicitWidth: 400
implicitWidth: screen ? Math.min(420, Math.max(320, screen.width * 0.25)) : 400
implicitHeight: {
if (!descriptionExpanded)
return basePopupHeight;
@@ -404,17 +404,11 @@ PanelWindow {
spacing: compactMode ? 1 : 2
StyledText {
width: parent.width
text: {
if (!notificationData)
return "";
const appName = notificationData.appName || "";
const timeStr = notificationData.timeStr || "";
return timeStr.length > 0 ? appName + " • " + timeStr : appName;
}
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
text: notificationData ? (notificationData.summary || "") : ""
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
width: parent.width
elide: Text.ElideRight
horizontalAlignment: Text.AlignLeft
maximumLineCount: 1
@@ -422,11 +416,11 @@ PanelWindow {
}
StyledText {
text: notificationData ? (notificationData.summary || "") : ""
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
width: parent.width
text: notificationData ? (notificationData.timeStr || "") : ""
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
elide: Text.ElideRight
horizontalAlignment: Text.AlignLeft
maximumLineCount: 1
@@ -511,7 +505,7 @@ PanelWindow {
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
@@ -598,7 +592,7 @@ PanelWindow {
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) {
notificationData.actions[0].invoke();
@@ -704,7 +698,7 @@ PanelWindow {
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
onStopped: {
@@ -735,7 +729,7 @@ PanelWindow {
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 +739,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 +749,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 +813,64 @@ PanelWindow {
easing.bezierCurve: Theme.expressiveCurves.standardDecel
}
}
Menu {
id: popupContextMenu
width: 220
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
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 {
text: 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 || "";
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);
}
}
}
}

View File

@@ -239,6 +239,77 @@ Item {
checked: SettingsData.notificationCompactMode
onToggled: checked => SettingsData.set("notificationCompactMode", 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 {

View File

@@ -251,9 +251,13 @@ 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() {
@@ -688,11 +692,13 @@ 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) {
@@ -852,7 +858,7 @@ Singleton {
}
const activePopupCount = visibleNotifications.filter(n => n && n.popup).length;
if (activePopupCount >= 4) {
if (activePopupCount >= maxVisibleNotifications) {
return;
}

View File

@@ -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",