fancy notifs
This commit is contained in:
@@ -45,4 +45,47 @@ top bar:
|
|||||||
y: 767
|
y: 767
|
||||||
without:
|
without:
|
||||||
x: 1154
|
x: 1154
|
||||||
y: 504
|
y: 504
|
||||||
|
|
||||||
|
notifications:
|
||||||
|
panel:
|
||||||
|
margin top: 24
|
||||||
|
margin right: 24
|
||||||
|
topbar reserved height: 182
|
||||||
|
|
||||||
|
card:
|
||||||
|
width: 420
|
||||||
|
padding: 14
|
||||||
|
border width: 3
|
||||||
|
icon box: 28
|
||||||
|
app icon size: 24
|
||||||
|
content gap: 10
|
||||||
|
line gap: 2
|
||||||
|
action gap: 8
|
||||||
|
action height: 34
|
||||||
|
action border width: 2
|
||||||
|
collapsed body lines: 2
|
||||||
|
expanded body lines: 8
|
||||||
|
stack peek count: 2
|
||||||
|
stack peek height: 36
|
||||||
|
stack peek step: 18
|
||||||
|
drag dismiss threshold: 140
|
||||||
|
|
||||||
|
colors:
|
||||||
|
background: #000000
|
||||||
|
border: #ffffff
|
||||||
|
text: #ffffff
|
||||||
|
hover invert background: #ffffff
|
||||||
|
hover invert text: #000000
|
||||||
|
|
||||||
|
timeouts:
|
||||||
|
fallback default ms: 5000
|
||||||
|
fallback low ms: 4500
|
||||||
|
fallback normal ms: 5000
|
||||||
|
fallback critical ms: 8000
|
||||||
|
|
||||||
|
animation:
|
||||||
|
enter ms: 170
|
||||||
|
exit ms: 140
|
||||||
|
fade ms: 120
|
||||||
|
expand ms: 120
|
||||||
|
|||||||
@@ -1,392 +1,396 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Qt5Compat.GraphicalEffects
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import Quickshell.Services.Notifications
|
||||||
import Quickshell.Widgets
|
import Quickshell.Widgets
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
property NotificationTheme theme
|
||||||
property int notificationId: -1
|
property int notificationId: -1
|
||||||
property string notifTitle: ""
|
property var notifObject: null
|
||||||
property string notifBody: ""
|
property int notifVersion: 0
|
||||||
property string notifUrgency: "Normal"
|
property real createdAtMs: Date.now()
|
||||||
property string notifAppName: ""
|
|
||||||
property string notifAppIcon: ""
|
property bool interactive: true
|
||||||
property string notifImage: ""
|
property bool forceExpanded: false
|
||||||
property int notifTimeoutMs: 5000
|
property bool peekMode: false
|
||||||
property bool notifCritical: false
|
|
||||||
property var notifHints: ({})
|
|
||||||
|
|
||||||
signal closeFinished(int notificationId, string reason)
|
signal closeFinished(int notificationId, string reason)
|
||||||
|
|
||||||
property bool highlighted: hoverHandler.hovered
|
|
||||||
property bool closing: false
|
property bool closing: false
|
||||||
property string closeReason: ""
|
property bool contextOpen: false
|
||||||
property real borderAlpha: 1
|
|
||||||
property real textAlpha: 1
|
|
||||||
property real collapseFactor: 1
|
property real collapseFactor: 1
|
||||||
|
|
||||||
readonly property color normalInk: "#ffffff"
|
readonly property string notifTitle: notifObject ? String(notifObject.summary || "") : ""
|
||||||
readonly property color highlightInk: "#ffc90e"
|
readonly property string notifBody: notifObject ? String(notifObject.body || "") : ""
|
||||||
readonly property color activeInk: highlighted ? highlightInk : normalInk
|
readonly property string notifAppName: notifObject ? String(notifObject.appName || "") : ""
|
||||||
readonly property int padding: 14
|
readonly property string notifAppIcon: notifObject ? String(notifObject.appIcon || "") : ""
|
||||||
readonly property int iconBox: 28
|
readonly property string notifImage: notifObject ? String(notifObject.image || "") : ""
|
||||||
readonly property int appIconSize: 24
|
readonly property bool isExpanded: forceExpanded || contextOpen
|
||||||
readonly property string appIconSource: {
|
|
||||||
if (notifImage && notifImage.length > 0)
|
|
||||||
return notifImage;
|
|
||||||
|
|
||||||
|
readonly property string appIconSource: {
|
||||||
if (!notifAppIcon || notifAppIcon.length === 0)
|
if (!notifAppIcon || notifAppIcon.length === 0)
|
||||||
return "";
|
return "";
|
||||||
|
|
||||||
if (notifAppIcon.indexOf("/") >= 0 || notifAppIcon.indexOf("file://") === 0)
|
if (notifAppIcon.indexOf("/") >= 0 || notifAppIcon.indexOf("file://") === 0)
|
||||||
return notifAppIcon;
|
return notifAppIcon;
|
||||||
|
|
||||||
return Quickshell.iconPath(notifAppIcon);
|
return Quickshell.iconPath(notifAppIcon);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hintString(name) {
|
readonly property string customImageSource: notifImage
|
||||||
if (!notifHints || typeof notifHints !== "object")
|
readonly property string primaryIconSource: customImageSource.length > 0 ? customImageSource : appIconSource
|
||||||
return "";
|
readonly property bool showBadgeIcon: customImageSource.length > 0 && appIconSource.length > 0
|
||||||
const value = notifHints[name];
|
readonly property int iconBoxSize: (theme ? theme.iconBox : 28) + 12
|
||||||
return value === undefined || value === null ? "" : String(value);
|
|
||||||
|
readonly property var notifActions: {
|
||||||
|
if (!notifObject || !notifObject.actions)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
const normalized = [];
|
||||||
|
for (let i = 0; i < notifObject.actions.length; i++) {
|
||||||
|
const actionObject = notifObject.actions[i];
|
||||||
|
if (!actionObject)
|
||||||
|
continue;
|
||||||
|
const actionText = String(actionObject.text || "").trim();
|
||||||
|
if (actionText.length === 0)
|
||||||
|
continue;
|
||||||
|
normalized.push({
|
||||||
|
actionObject: actionObject,
|
||||||
|
actionText: actionText
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
function textBlob() {
|
readonly property int actionCount: notifActions.length
|
||||||
return (notifAppName + " " + notifTitle + " " + notifBody + " " + hintString("category")).toLowerCase();
|
readonly property var contextItems: {
|
||||||
|
const items = [];
|
||||||
|
for (let i = 0; i < notifActions.length; i++)
|
||||||
|
items.push(notifActions[i]);
|
||||||
|
items.push({
|
||||||
|
actionObject: null,
|
||||||
|
actionText: "Dismiss"
|
||||||
|
});
|
||||||
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseVolumePercent() {
|
function urgencyTimeoutMs() {
|
||||||
const source = (notifTitle + " " + notifBody).toUpperCase();
|
if (!notifObject)
|
||||||
const match = source.match(/VOLUME\s*(\d{1,3})%/);
|
return theme.fallbackNormalTimeoutMs;
|
||||||
if (!match || match.length < 2)
|
|
||||||
return -1;
|
|
||||||
|
|
||||||
const parsed = Number(match[1]);
|
const rawSeconds = Number(notifObject.expireTimeout);
|
||||||
if (!Number.isFinite(parsed))
|
if (Number.isFinite(rawSeconds) && rawSeconds > 0)
|
||||||
return -1;
|
return Math.max(1000, Math.round(rawSeconds * 1000));
|
||||||
|
|
||||||
return Math.max(0, Math.min(100, parsed));
|
const urgency = notifObject.urgency;
|
||||||
|
if (urgency === NotificationUrgency.Critical)
|
||||||
|
return theme.fallbackCriticalTimeoutMs;
|
||||||
|
if (urgency === NotificationUrgency.Low)
|
||||||
|
return theme.fallbackLowTimeoutMs;
|
||||||
|
return theme.fallbackNormalTimeoutMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property int volumePercent: parseVolumePercent()
|
function relativeTime() {
|
||||||
readonly property bool isVolumeLayout: volumePercent >= 0
|
const elapsedSeconds = Math.max(0, Math.floor((Date.now() - createdAtMs) / 1000));
|
||||||
|
if (elapsedSeconds < 60)
|
||||||
function soulColor() {
|
return "now";
|
||||||
const full = textBlob();
|
if (elapsedSeconds < 3600)
|
||||||
const urgencyText = String(notifUrgency || "").toLowerCase();
|
return Math.floor(elapsedSeconds / 60) + "m";
|
||||||
|
return Math.floor(elapsedSeconds / 3600) + "h";
|
||||||
if (urgencyText === "critical")
|
|
||||||
return "#fff27a";
|
|
||||||
|
|
||||||
if (full.indexOf("network") >= 0 || full.indexOf("wifi") >= 0 || full.indexOf("ethernet") >= 0 || full.indexOf("bluetooth") >= 0)
|
|
||||||
return "#4ca4ff";
|
|
||||||
|
|
||||||
if (full.indexOf("success") >= 0 || full.indexOf("completed") >= 0 || full.indexOf("saved") >= 0 || full.indexOf("done") >= 0)
|
|
||||||
return "#47d66b";
|
|
||||||
|
|
||||||
return "#ff2a2a";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property color soulInk: soulColor()
|
|
||||||
|
|
||||||
function beginClose(reason) {
|
function beginClose(reason) {
|
||||||
if (closing)
|
if (closing)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
closing = true;
|
closing = true;
|
||||||
closeReason = reason;
|
|
||||||
timeoutTimer.stop();
|
timeoutTimer.stop();
|
||||||
|
closeReason = reason;
|
||||||
if (reason === "click") {
|
closeAnimation.start();
|
||||||
heartFlash.restart();
|
|
||||||
clickClose.start();
|
|
||||||
} else {
|
|
||||||
timeoutClose.start();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
implicitWidth: 420
|
function openContextPanel() {
|
||||||
implicitHeight: Math.max(1, contentColumn.implicitHeight * collapseFactor + padding * 2)
|
if (!interactive || closing || peekMode)
|
||||||
|
return;
|
||||||
|
contextOpen = !contextOpen;
|
||||||
|
restartTimeout();
|
||||||
|
}
|
||||||
|
|
||||||
|
function invokeAction(actionObject) {
|
||||||
|
if (closing)
|
||||||
|
return;
|
||||||
|
if (actionObject)
|
||||||
|
actionObject.invoke();
|
||||||
|
beginClose(actionObject ? "click" : "dismiss");
|
||||||
|
}
|
||||||
|
|
||||||
|
function restartTimeout() {
|
||||||
|
if (closing || isExpanded || peekMode)
|
||||||
|
return;
|
||||||
|
timeoutTimer.interval = urgencyTimeoutMs();
|
||||||
|
timeoutTimer.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
property string closeReason: ""
|
||||||
|
implicitWidth: theme ? theme.stackWidth : 420
|
||||||
|
implicitHeight: peekMode ? (theme ? theme.stackPeekHeight : 36) : Math.max(1, Math.round((contentColumn.implicitHeight + ((theme ? theme.cardPadding : 14) * 2)) * collapseFactor))
|
||||||
width: implicitWidth
|
width: implicitWidth
|
||||||
height: implicitHeight
|
height: implicitHeight
|
||||||
transformOrigin: Item.TopRight
|
opacity: 1
|
||||||
|
|
||||||
SequentialAnimation {
|
SequentialAnimation {
|
||||||
id: entryAnimation
|
id: enterAnimation
|
||||||
running: true
|
running: !peekMode
|
||||||
|
|
||||||
PropertyAction {
|
|
||||||
target: root
|
|
||||||
property: "opacity"
|
|
||||||
value: 0
|
|
||||||
}
|
|
||||||
PropertyAction {
|
|
||||||
target: root
|
|
||||||
property: "scale"
|
|
||||||
value: 0.95
|
|
||||||
}
|
|
||||||
|
|
||||||
|
PropertyAction { target: root; property: "opacity"; value: 0 }
|
||||||
|
PropertyAction { target: root; property: "x"; value: 20 }
|
||||||
ParallelAnimation {
|
ParallelAnimation {
|
||||||
NumberAnimation {
|
NumberAnimation { target: root; property: "opacity"; to: 1; duration: theme ? theme.enterMs : 170; easing.type: Easing.OutCubic }
|
||||||
target: root
|
NumberAnimation { target: root; property: "x"; to: 0; duration: theme ? theme.enterMs : 170; easing.type: Easing.OutCubic }
|
||||||
property: "opacity"
|
|
||||||
to: 0.45
|
|
||||||
duration: 55
|
|
||||||
}
|
|
||||||
NumberAnimation {
|
|
||||||
target: root
|
|
||||||
property: "scale"
|
|
||||||
to: 0.97
|
|
||||||
duration: 55
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ParallelAnimation {
|
|
||||||
NumberAnimation {
|
|
||||||
target: root
|
|
||||||
property: "opacity"
|
|
||||||
to: 0.75
|
|
||||||
duration: 55
|
|
||||||
}
|
|
||||||
NumberAnimation {
|
|
||||||
target: root
|
|
||||||
property: "scale"
|
|
||||||
to: 0.99
|
|
||||||
duration: 55
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ParallelAnimation {
|
|
||||||
NumberAnimation {
|
|
||||||
target: root
|
|
||||||
property: "opacity"
|
|
||||||
to: 1
|
|
||||||
duration: 70
|
|
||||||
}
|
|
||||||
NumberAnimation {
|
|
||||||
target: root
|
|
||||||
property: "scale"
|
|
||||||
to: 1
|
|
||||||
duration: 70
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SequentialAnimation {
|
ParallelAnimation {
|
||||||
id: heartFlash
|
id: closeAnimation
|
||||||
NumberAnimation {
|
NumberAnimation { target: root; property: "opacity"; to: 0; duration: theme ? theme.fadeMs : 120; easing.type: Easing.InCubic }
|
||||||
target: soulFlashOverlay
|
NumberAnimation { target: root; property: "x"; to: width; duration: theme ? theme.exitMs : 140; easing.type: Easing.InCubic }
|
||||||
property: "opacity"
|
onFinished: root.closeFinished(root.notificationId, root.closeReason)
|
||||||
to: 1
|
|
||||||
duration: 35
|
|
||||||
}
|
|
||||||
NumberAnimation {
|
|
||||||
target: soulFlashOverlay
|
|
||||||
property: "opacity"
|
|
||||||
to: 0
|
|
||||||
duration: 75
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SequentialAnimation {
|
|
||||||
id: clickClose
|
|
||||||
ParallelAnimation {
|
|
||||||
NumberAnimation {
|
|
||||||
target: root
|
|
||||||
property: "y"
|
|
||||||
to: -8
|
|
||||||
duration: 160
|
|
||||||
easing.type: Easing.InCubic
|
|
||||||
}
|
|
||||||
NumberAnimation {
|
|
||||||
target: root
|
|
||||||
property: "opacity"
|
|
||||||
to: 0
|
|
||||||
duration: 160
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ScriptAction {
|
|
||||||
script: root.closeFinished(root.notificationId, "click")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SequentialAnimation {
|
|
||||||
id: timeoutClose
|
|
||||||
NumberAnimation {
|
|
||||||
target: root
|
|
||||||
property: "borderAlpha"
|
|
||||||
to: 0.35
|
|
||||||
duration: 90
|
|
||||||
}
|
|
||||||
NumberAnimation {
|
|
||||||
target: root
|
|
||||||
property: "textAlpha"
|
|
||||||
to: 0
|
|
||||||
duration: 110
|
|
||||||
}
|
|
||||||
ParallelAnimation {
|
|
||||||
NumberAnimation {
|
|
||||||
target: root
|
|
||||||
property: "collapseFactor"
|
|
||||||
to: 0
|
|
||||||
duration: 150
|
|
||||||
easing.type: Easing.InCubic
|
|
||||||
}
|
|
||||||
NumberAnimation {
|
|
||||||
target: root
|
|
||||||
property: "opacity"
|
|
||||||
to: 0
|
|
||||||
duration: 150
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ScriptAction {
|
|
||||||
script: root.closeFinished(root.notificationId, "timeout")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: timeoutTimer
|
id: timeoutTimer
|
||||||
interval: root.notifTimeoutMs
|
interval: root.urgencyTimeoutMs()
|
||||||
repeat: false
|
repeat: false
|
||||||
running: !root.notifCritical && root.notifTimeoutMs > 0
|
running: !root.peekMode
|
||||||
onTriggered: root.beginClose("timeout")
|
onTriggered: root.beginClose("timeout")
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: "#000000"
|
color: theme ? theme.panelBackground : "#000000"
|
||||||
border.width: 3
|
border.width: theme ? theme.borderWidth : 3
|
||||||
border.color: Qt.rgba(root.activeInk.r, root.activeInk.g, root.activeInk.b, root.borderAlpha)
|
border.color: theme ? theme.panelBorder : "#ffffff"
|
||||||
radius: 0
|
radius: 0
|
||||||
antialiasing: false
|
antialiasing: false
|
||||||
|
visible: !peekMode
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Rectangle {
|
||||||
id: soulContainer
|
anchors.fill: parent
|
||||||
x: {
|
color: theme ? theme.panelBackground : "#000000"
|
||||||
if (!root.isVolumeLayout)
|
border.width: theme ? theme.borderWidth : 3
|
||||||
return root.padding;
|
border.color: theme ? theme.panelBorder : "#ffffff"
|
||||||
|
radius: 0
|
||||||
const left = root.padding;
|
antialiasing: false
|
||||||
const right = Math.max(left, root.width - root.padding - root.iconBox);
|
visible: peekMode
|
||||||
return left + (right - left) * (root.volumePercent / 100);
|
opacity: 0.95
|
||||||
}
|
|
||||||
y: root.padding + 2
|
|
||||||
width: root.iconBox
|
|
||||||
height: root.iconBox
|
|
||||||
scale: 1
|
|
||||||
|
|
||||||
Behavior on x {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 150
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IconImage {
|
|
||||||
visible: root.appIconSource.length > 0
|
|
||||||
anchors.centerIn: parent
|
|
||||||
implicitSize: root.appIconSize
|
|
||||||
source: root.appIconSource
|
|
||||||
asynchronous: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
visible: root.appIconSource.length === 0
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
Image {
|
|
||||||
id: soulImage
|
|
||||||
anchors.fill: parent
|
|
||||||
source: "../Topbar/topbar/soul_small.png"
|
|
||||||
smooth: false
|
|
||||||
antialiasing: false
|
|
||||||
}
|
|
||||||
|
|
||||||
ColorOverlay {
|
|
||||||
anchors.fill: soulImage
|
|
||||||
source: soulImage
|
|
||||||
color: root.soulInk
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: soulFlashOverlay
|
|
||||||
anchors.fill: parent
|
|
||||||
color: "#ffffff"
|
|
||||||
opacity: 0
|
|
||||||
radius: 0
|
|
||||||
antialiasing: false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
id: contentColumn
|
id: contentColumn
|
||||||
x: root.padding + root.iconBox + 10
|
visible: !peekMode
|
||||||
y: root.padding - 1
|
x: (theme ? theme.cardPadding : 14)
|
||||||
width: root.width - x - root.padding
|
y: (theme ? theme.cardPadding : 14)
|
||||||
spacing: 2
|
width: root.width - ((theme ? theme.cardPadding : 14) * 2)
|
||||||
|
spacing: theme ? theme.lineGap : 2
|
||||||
|
|
||||||
Text {
|
Row {
|
||||||
visible: !root.isVolumeLayout
|
id: mainRow
|
||||||
text: String(root.notifTitle || "")
|
|
||||||
color: Qt.rgba(root.activeInk.r, root.activeInk.g, root.activeInk.b, root.textAlpha)
|
|
||||||
font.family: "8bitoperator JVE"
|
|
||||||
font.pixelSize: 28
|
|
||||||
font.letterSpacing: 1
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
elide: Text.ElideRight
|
|
||||||
renderType: Text.NativeRendering
|
|
||||||
font.hintingPreference: Font.PreferNoHinting
|
|
||||||
smooth: false
|
|
||||||
antialiasing: false
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
spacing: theme ? theme.contentGap : 10
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: root.iconBoxSize
|
||||||
|
height: root.iconBoxSize
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
anchors.fill: parent
|
||||||
|
source: root.primaryIconSource
|
||||||
|
visible: root.primaryIconSource.length > 0
|
||||||
|
asynchronous: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: root.primaryIconSource.length === 0
|
||||||
|
color: "#000000"
|
||||||
|
border.width: 1
|
||||||
|
border.color: theme ? theme.panelBorder : "#ffffff"
|
||||||
|
radius: 0
|
||||||
|
antialiasing: false
|
||||||
|
}
|
||||||
|
|
||||||
|
Image {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width - 2
|
||||||
|
height: parent.height - 2
|
||||||
|
source: "../Topbar/topbar/soul_small.png"
|
||||||
|
visible: root.primaryIconSource.length === 0
|
||||||
|
smooth: false
|
||||||
|
antialiasing: false
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
}
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
visible: root.showBadgeIcon
|
||||||
|
width: Math.max(12, Math.round(parent.width * 0.42))
|
||||||
|
height: Math.max(12, Math.round(parent.height * 0.42))
|
||||||
|
source: root.appIconSource
|
||||||
|
x: parent.width - width
|
||||||
|
y: parent.height - height
|
||||||
|
asynchronous: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: textColumn
|
||||||
|
width: parent.width - root.iconBoxSize - (theme ? theme.contentGap : 10)
|
||||||
|
spacing: theme ? theme.lineGap : 2
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width - timeLabel.implicitWidth - 6
|
||||||
|
text: root.notifTitle
|
||||||
|
color: theme ? theme.panelText : "#ffffff"
|
||||||
|
elide: Text.ElideRight
|
||||||
|
maximumLineCount: 1
|
||||||
|
font.family: theme ? theme.fontFamily : "8bitoperator JVE"
|
||||||
|
font.pixelSize: 24
|
||||||
|
font.letterSpacing: theme ? theme.fontLetterSpacing : 1
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
font.hintingPreference: Font.PreferNoHinting
|
||||||
|
smooth: false
|
||||||
|
antialiasing: false
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: timeLabel
|
||||||
|
text: root.relativeTime()
|
||||||
|
color: theme ? theme.panelText : "#ffffff"
|
||||||
|
font.family: theme ? theme.fontFamily : "8bitoperator JVE"
|
||||||
|
font.pixelSize: 16
|
||||||
|
font.letterSpacing: theme ? theme.fontLetterSpacing : 1
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
font.hintingPreference: Font.PreferNoHinting
|
||||||
|
smooth: false
|
||||||
|
antialiasing: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: root.notifBody
|
||||||
|
color: theme ? theme.panelText : "#ffffff"
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
maximumLineCount: root.isExpanded ? (theme ? theme.expandedBodyLines : 8) : (theme ? theme.collapsedBodyLines : 2)
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.family: theme ? theme.fontFamily : "8bitoperator JVE"
|
||||||
|
font.pixelSize: 22
|
||||||
|
font.letterSpacing: theme ? theme.fontLetterSpacing : 1
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
font.hintingPreference: Font.PreferNoHinting
|
||||||
|
smooth: false
|
||||||
|
antialiasing: false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Column {
|
||||||
visible: !root.isVolumeLayout
|
visible: root.contextOpen
|
||||||
text: String(root.notifBody || "")
|
|
||||||
color: Qt.rgba(root.activeInk.r, root.activeInk.g, root.activeInk.b, root.textAlpha)
|
|
||||||
font.family: "8bitoperator JVE"
|
|
||||||
font.pixelSize: 24
|
|
||||||
font.letterSpacing: 1
|
|
||||||
wrapMode: Text.Wrap
|
|
||||||
maximumLineCount: 4
|
|
||||||
elide: Text.ElideRight
|
|
||||||
textFormat: Text.PlainText
|
|
||||||
renderType: Text.NativeRendering
|
|
||||||
font.hintingPreference: Font.PreferNoHinting
|
|
||||||
smooth: false
|
|
||||||
antialiasing: false
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
}
|
spacing: theme ? theme.actionGap : 8
|
||||||
|
|
||||||
Text {
|
Repeater {
|
||||||
visible: root.isVolumeLayout
|
model: root.contextItems
|
||||||
text: "VOLUME " + root.volumePercent + "%"
|
|
||||||
color: Qt.rgba(root.activeInk.r, root.activeInk.g, root.activeInk.b, root.textAlpha)
|
|
||||||
font.family: "8bitoperator JVE"
|
|
||||||
font.pixelSize: 30
|
|
||||||
font.letterSpacing: 1
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
elide: Text.ElideRight
|
|
||||||
renderType: Text.NativeRendering
|
|
||||||
font.hintingPreference: Font.PreferNoHinting
|
|
||||||
smooth: false
|
|
||||||
antialiasing: false
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: theme ? theme.actionHeight : 34
|
||||||
|
color: actionHover.hovered ? (theme ? theme.hoverInvertBackground : "#ffffff") : (theme ? theme.panelBackground : "#000000")
|
||||||
|
border.width: theme ? theme.actionBorderWidth : 2
|
||||||
|
border.color: theme ? theme.panelBorder : "#ffffff"
|
||||||
|
radius: 0
|
||||||
|
antialiasing: false
|
||||||
|
|
||||||
|
HoverHandler { id: actionHover }
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: modelData.actionText
|
||||||
|
color: actionHover.hovered ? (theme ? theme.hoverInvertText : "#000000") : (theme ? theme.panelText : "#ffffff")
|
||||||
|
font.family: theme ? theme.fontFamily : "8bitoperator JVE"
|
||||||
|
font.pixelSize: 18
|
||||||
|
font.letterSpacing: theme ? theme.fontLetterSpacing : 1
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
font.hintingPreference: Font.PreferNoHinting
|
||||||
|
smooth: false
|
||||||
|
antialiasing: false
|
||||||
|
}
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
enabled: root.interactive && !root.closing
|
||||||
|
onTapped: root.invokeAction(modelData.actionObject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HoverHandler {
|
DragHandler {
|
||||||
id: hoverHandler
|
id: swipeHandler
|
||||||
|
enabled: root.interactive && !root.peekMode && !root.closing
|
||||||
|
xAxis.enabled: true
|
||||||
|
yAxis.enabled: false
|
||||||
|
|
||||||
|
onTranslationChanged: {
|
||||||
|
root.x = Math.max(0, translation.x);
|
||||||
|
}
|
||||||
|
|
||||||
|
onActiveChanged: {
|
||||||
|
if (active)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const threshold = theme ? theme.dragDismissThreshold : 140;
|
||||||
|
if (root.x >= threshold) {
|
||||||
|
root.beginClose("drag");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
snapBackAnimation.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NumberAnimation {
|
||||||
|
id: snapBackAnimation
|
||||||
|
target: root
|
||||||
|
property: "x"
|
||||||
|
to: 0
|
||||||
|
duration: theme ? theme.expandMs : 120
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
}
|
}
|
||||||
|
|
||||||
TapHandler {
|
TapHandler {
|
||||||
acceptedButtons: Qt.LeftButton
|
acceptedButtons: Qt.LeftButton
|
||||||
enabled: root.highlighted && !root.closing
|
enabled: root.interactive && !root.peekMode && !root.closing
|
||||||
onTapped: root.beginClose("click")
|
onTapped: root.openContextPanel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
acceptedButtons: Qt.RightButton
|
||||||
|
enabled: root.interactive && !root.peekMode && !root.closing
|
||||||
|
onTapped: root.beginClose("dismiss")
|
||||||
|
}
|
||||||
|
|
||||||
|
onNotifObjectChanged: restartTimeout()
|
||||||
|
onNotifVersionChanged: restartTimeout()
|
||||||
|
onContextOpenChanged: {
|
||||||
|
if (!contextOpen)
|
||||||
|
restartTimeout();
|
||||||
|
else
|
||||||
|
timeoutTimer.stop();
|
||||||
|
}
|
||||||
|
Component.onCompleted: restartTimeout()
|
||||||
}
|
}
|
||||||
|
|||||||
179
Shell/Notifications/NotificationGroup.qml
Normal file
179
Shell/Notifications/NotificationGroup.qml
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import QtQuick
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property NotificationTheme theme
|
||||||
|
property var groupData: null
|
||||||
|
property bool expanded: false
|
||||||
|
property bool peekEnabled: false
|
||||||
|
|
||||||
|
signal closeFinished(int notificationId, string reason)
|
||||||
|
signal expandRequested(string groupKey, bool expanded)
|
||||||
|
|
||||||
|
readonly property var entries: groupData && groupData.notifications ? groupData.notifications : []
|
||||||
|
readonly property int entryCount: entries.length
|
||||||
|
readonly property bool showCompressedStack: !expanded && entryCount > 1
|
||||||
|
readonly property int visiblePeekCount: showCompressedStack && peekEnabled ? Math.min(theme ? theme.stackPeekCount : 2, Math.max(0, entryCount - 1)) : 0
|
||||||
|
|
||||||
|
width: theme ? theme.stackWidth : 420
|
||||||
|
implicitHeight: {
|
||||||
|
if (expanded)
|
||||||
|
return expandedColumn.implicitHeight;
|
||||||
|
|
||||||
|
const base = newestCard.implicitHeight;
|
||||||
|
const step = theme ? theme.stackPeekStep : 18;
|
||||||
|
return base + visiblePeekCount * step;
|
||||||
|
}
|
||||||
|
height: implicitHeight
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: compressedStack
|
||||||
|
visible: !root.expanded
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.visiblePeekCount
|
||||||
|
|
||||||
|
NotificationCard {
|
||||||
|
property int peekIndex: index
|
||||||
|
|
||||||
|
theme: root.theme
|
||||||
|
notificationId: root.entries[peekIndex + 1].notificationId
|
||||||
|
notifObject: root.entries[peekIndex + 1].notifObject
|
||||||
|
notifVersion: root.entries[peekIndex + 1].notifVersion
|
||||||
|
createdAtMs: root.entries[peekIndex + 1].createdAt
|
||||||
|
peekMode: true
|
||||||
|
interactive: false
|
||||||
|
width: root.width
|
||||||
|
y: Math.max(0, newestCard.implicitHeight - (theme ? theme.stackPeekHeight : 36) + (theme ? theme.stackPeekStep : 18) * (peekIndex + 1))
|
||||||
|
z: 10 - peekIndex
|
||||||
|
|
||||||
|
onCloseFinished: function (closedNotificationId, reason) {
|
||||||
|
root.closeFinished(closedNotificationId, reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationCard {
|
||||||
|
id: newestCard
|
||||||
|
theme: root.theme
|
||||||
|
notificationId: root.entryCount > 0 ? root.entries[0].notificationId : -1
|
||||||
|
notifObject: root.entryCount > 0 ? root.entries[0].notifObject : null
|
||||||
|
notifVersion: root.entryCount > 0 ? root.entries[0].notifVersion : 0
|
||||||
|
createdAtMs: root.entryCount > 0 ? root.entries[0].createdAt : Date.now()
|
||||||
|
interactive: root.entryCount <= 1
|
||||||
|
width: root.width
|
||||||
|
y: 0
|
||||||
|
z: 20
|
||||||
|
|
||||||
|
onCloseFinished: function (closedNotificationId, reason) {
|
||||||
|
root.closeFinished(closedNotificationId, reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
visible: root.entryCount > 1 && root.peekEnabled
|
||||||
|
width: 82
|
||||||
|
height: 18
|
||||||
|
x: root.width - width - 18
|
||||||
|
y: newestCard.implicitHeight + 1
|
||||||
|
color: theme ? theme.panelBackground : "#000000"
|
||||||
|
border.width: 2
|
||||||
|
border.color: theme ? theme.panelBorder : "#ffffff"
|
||||||
|
radius: 0
|
||||||
|
antialiasing: false
|
||||||
|
z: 25
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "+" + String(Math.max(0, root.entryCount - 1))
|
||||||
|
color: theme ? theme.panelText : "#ffffff"
|
||||||
|
font.family: theme ? theme.fontFamily : "8bitoperator JVE"
|
||||||
|
font.pixelSize: 14
|
||||||
|
font.letterSpacing: theme ? theme.fontLetterSpacing : 1
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
font.hintingPreference: Font.PreferNoHinting
|
||||||
|
smooth: false
|
||||||
|
antialiasing: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
enabled: root.entryCount > 1
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
onTapped: root.expandRequested(root.groupData.groupKey, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
enabled: root.entryCount > 1
|
||||||
|
acceptedButtons: Qt.RightButton
|
||||||
|
onTapped: {
|
||||||
|
if (root.entryCount > 0)
|
||||||
|
root.closeFinished(root.entries[0].notificationId, "dismiss");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: expandedColumn
|
||||||
|
visible: root.expanded
|
||||||
|
width: parent.width
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.entries
|
||||||
|
|
||||||
|
NotificationCard {
|
||||||
|
theme: root.theme
|
||||||
|
notificationId: modelData.notificationId
|
||||||
|
notifObject: modelData.notifObject
|
||||||
|
notifVersion: modelData.notifVersion
|
||||||
|
createdAtMs: modelData.createdAt
|
||||||
|
width: root.width
|
||||||
|
forceExpanded: true
|
||||||
|
|
||||||
|
onCloseFinished: function (closedNotificationId, reason) {
|
||||||
|
root.closeFinished(closedNotificationId, reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
visible: root.entryCount > 1
|
||||||
|
width: parent.width
|
||||||
|
height: theme ? theme.actionHeight : 34
|
||||||
|
color: theme ? theme.panelBackground : "#000000"
|
||||||
|
border.width: theme ? theme.actionBorderWidth : 2
|
||||||
|
border.color: theme ? theme.panelBorder : "#ffffff"
|
||||||
|
radius: 0
|
||||||
|
antialiasing: false
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "Collapse"
|
||||||
|
color: theme ? theme.panelText : "#ffffff"
|
||||||
|
font.family: theme ? theme.fontFamily : "8bitoperator JVE"
|
||||||
|
font.pixelSize: 18
|
||||||
|
font.letterSpacing: theme ? theme.fontLetterSpacing : 1
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
font.hintingPreference: Font.PreferNoHinting
|
||||||
|
smooth: false
|
||||||
|
antialiasing: false
|
||||||
|
}
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
onTapped: root.expandRequested(root.groupData.groupKey, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on implicitHeight {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: theme ? theme.expandMs : 120
|
||||||
|
easing.type: Easing.InOutCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,8 +13,8 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
margins {
|
margins {
|
||||||
top: 24
|
top: theme.panelMarginTop
|
||||||
right: 24
|
right: theme.panelMarginRight
|
||||||
}
|
}
|
||||||
|
|
||||||
// Top layer keeps notifications above normal windows while still usually
|
// Top layer keeps notifications above normal windows while still usually
|
||||||
@@ -30,13 +30,18 @@ PanelWindow {
|
|||||||
visible: true
|
visible: true
|
||||||
color: "#00000000"
|
color: "#00000000"
|
||||||
|
|
||||||
property int stackWidth: 420
|
NotificationTheme {
|
||||||
property int menuReservedHeight: 182
|
id: theme
|
||||||
|
}
|
||||||
|
|
||||||
|
property int stackWidth: theme.stackWidth
|
||||||
|
property int menuReservedHeight: theme.topbarReservedHeight
|
||||||
|
readonly property bool stackPreviewEnabled: Boolean(ShellStateManager.global("notifications.stackPreviewEnabled", true))
|
||||||
|
|
||||||
readonly property int screenHeight: screen ? screen.height : 1080
|
readonly property int screenHeight: screen ? screen.height : 1080
|
||||||
readonly property bool topbarOpen: ShellStateManager.shellOpen
|
readonly property bool topbarOpen: ShellStateManager.shellOpen
|
||||||
readonly property int stackOffsetY: topbarOpen ? menuReservedHeight + 24 : 0
|
readonly property int stackOffsetY: topbarOpen ? menuReservedHeight + theme.panelMarginTop : 0
|
||||||
readonly property int maxStackHeight: Math.max(120, screenHeight - 24 - stackOffsetY - 24)
|
readonly property int maxStackHeight: Math.max(120, screenHeight - theme.panelMarginTop - stackOffsetY - theme.panelMarginTop)
|
||||||
|
|
||||||
implicitWidth: stackWidth
|
implicitWidth: stackWidth
|
||||||
implicitHeight: stackOffsetY + notificationViewport.height
|
implicitHeight: stackOffsetY + notificationViewport.height
|
||||||
@@ -87,22 +92,20 @@ PanelWindow {
|
|||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: notificationModel.notifications
|
model: notificationModel.groupedNotifications
|
||||||
|
|
||||||
NotificationCard {
|
NotificationGroup {
|
||||||
notificationId: rowNotificationId
|
theme: theme
|
||||||
notifTitle: rowTitle
|
groupData: modelData
|
||||||
notifBody: rowBody
|
expanded: notificationModel.groupIsExpanded(modelData.groupKey)
|
||||||
notifUrgency: rowUrgency
|
peekEnabled: notificationLayer.stackPreviewEnabled
|
||||||
notifAppName: rowAppName
|
|
||||||
notifAppIcon: rowAppIcon
|
|
||||||
notifImage: rowImage
|
|
||||||
notifTimeoutMs: rowTimeoutMs
|
|
||||||
notifCritical: rowCritical
|
|
||||||
notifHints: rowHints
|
|
||||||
|
|
||||||
width: notificationLayer.stackWidth
|
width: notificationLayer.stackWidth
|
||||||
|
|
||||||
|
onExpandRequested: function (groupKey, shouldExpand) {
|
||||||
|
notificationModel.setGroupExpanded(groupKey, shouldExpand);
|
||||||
|
}
|
||||||
|
|
||||||
onCloseFinished: function (closedNotificationId, reason) {
|
onCloseFinished: function (closedNotificationId, reason) {
|
||||||
notificationModel.closeById(closedNotificationId, reason);
|
notificationModel.closeById(closedNotificationId, reason);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,14 @@ QtObject {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
property ListModel notifications: ListModel {}
|
property ListModel notifications: ListModel {}
|
||||||
|
property var groupedNotifications: []
|
||||||
|
property var groupExpansion: ({})
|
||||||
|
property int versionCounter: 0
|
||||||
|
|
||||||
|
function nextVersion() {
|
||||||
|
versionCounter += 1;
|
||||||
|
return versionCounter;
|
||||||
|
}
|
||||||
|
|
||||||
function indexOfId(notificationId) {
|
function indexOfId(notificationId) {
|
||||||
for (let i = 0; i < notifications.count; i++) {
|
for (let i = 0; i < notifications.count; i++) {
|
||||||
@@ -15,16 +23,104 @@ QtObject {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function timeoutMsFor(notificationObject) {
|
function normalizedString(value) {
|
||||||
const isCritical = notificationObject.urgency === NotificationUrgency.Critical;
|
if (value === undefined || value === null)
|
||||||
if (isCritical)
|
return "";
|
||||||
return -1;
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
const rawSeconds = Number(notificationObject.expireTimeout);
|
function hintString(notificationObject, name) {
|
||||||
if (!Number.isFinite(rawSeconds) || rawSeconds <= 0)
|
if (!notificationObject || !notificationObject.hints)
|
||||||
return 5000;
|
return "";
|
||||||
|
const value = notificationObject.hints[name];
|
||||||
|
return value === undefined || value === null ? "" : String(value);
|
||||||
|
}
|
||||||
|
|
||||||
return Math.max(1000, Math.round(rawSeconds * 1000));
|
function groupKeyFor(notificationObject) {
|
||||||
|
const desktopEntry = normalizedString(hintString(notificationObject, "desktop-entry")).trim().toLowerCase();
|
||||||
|
if (desktopEntry.length > 0)
|
||||||
|
return "desktop:" + desktopEntry;
|
||||||
|
|
||||||
|
const appName = normalizedString(notificationObject ? notificationObject.appName : "").trim().toLowerCase();
|
||||||
|
if (appName.length > 0)
|
||||||
|
return "app:" + appName;
|
||||||
|
|
||||||
|
const appIcon = normalizedString(notificationObject ? notificationObject.appIcon : "").trim().toLowerCase();
|
||||||
|
if (appIcon.length > 0)
|
||||||
|
return "icon:" + appIcon;
|
||||||
|
|
||||||
|
const image = normalizedString(notificationObject ? notificationObject.image : "").trim().toLowerCase();
|
||||||
|
if (image.length > 0)
|
||||||
|
return "image:" + image;
|
||||||
|
|
||||||
|
return "id:" + String(notificationObject ? notificationObject.id : Date.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortRowsNewestFirst(left, right) {
|
||||||
|
return (right.createdAt || 0) - (left.createdAt || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupSortNewestFirst(left, right) {
|
||||||
|
return (right.newestAt || 0) - (left.newestAt || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rebuildGroups() {
|
||||||
|
const grouped = ({});
|
||||||
|
|
||||||
|
for (let i = 0; i < notifications.count; i++) {
|
||||||
|
const row = notifications.get(i);
|
||||||
|
const key = row.rowGroupKey;
|
||||||
|
if (!grouped[key]) {
|
||||||
|
grouped[key] = {
|
||||||
|
groupKey: key,
|
||||||
|
appName: row.rowAppName,
|
||||||
|
appIcon: row.rowAppIcon,
|
||||||
|
desktopEntry: row.rowDesktopEntry,
|
||||||
|
newestAt: row.rowCreatedAt,
|
||||||
|
notifications: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
grouped[key].notifications.push({
|
||||||
|
notificationId: row.rowNotificationId,
|
||||||
|
notifObject: row.rowObject,
|
||||||
|
notifVersion: row.rowVersion,
|
||||||
|
createdAt: row.rowCreatedAt,
|
||||||
|
appName: row.rowAppName,
|
||||||
|
appIcon: row.rowAppIcon,
|
||||||
|
image: row.rowImage
|
||||||
|
});
|
||||||
|
|
||||||
|
if (row.rowCreatedAt > grouped[key].newestAt)
|
||||||
|
grouped[key].newestAt = row.rowCreatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const groups = [];
|
||||||
|
for (const groupKey in grouped) {
|
||||||
|
if (!grouped.hasOwnProperty(groupKey))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const group = grouped[groupKey];
|
||||||
|
group.notifications.sort(sortRowsNewestFirst);
|
||||||
|
groups.push(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
groups.sort(groupSortNewestFirst);
|
||||||
|
groupedNotifications = groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupIsExpanded(groupKey) {
|
||||||
|
return Boolean(groupExpansion[groupKey]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setGroupExpanded(groupKey, expanded) {
|
||||||
|
const next = Object.assign({}, groupExpansion);
|
||||||
|
next[groupKey] = Boolean(expanded);
|
||||||
|
groupExpansion = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleGroupExpanded(groupKey) {
|
||||||
|
setGroupExpanded(groupKey, !groupIsExpanded(groupKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
function addNotification(notificationObject) {
|
function addNotification(notificationObject) {
|
||||||
@@ -33,32 +129,50 @@ QtObject {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingIndex = indexOfId(notificationObject.id);
|
|
||||||
if (existingIndex >= 0)
|
|
||||||
notifications.remove(existingIndex);
|
|
||||||
|
|
||||||
notificationObject.tracked = true;
|
notificationObject.tracked = true;
|
||||||
|
|
||||||
const idCopy = notificationObject.id;
|
const idCopy = notificationObject.id;
|
||||||
|
const objectCopy = notificationObject;
|
||||||
notificationObject.closed.connect(function () {
|
notificationObject.closed.connect(function () {
|
||||||
const rowIndex = indexOfId(idCopy);
|
const rowIndex = indexOfId(idCopy);
|
||||||
if (rowIndex >= 0)
|
if (rowIndex >= 0 && notifications.get(rowIndex).rowObject === objectCopy) {
|
||||||
notifications.remove(rowIndex);
|
notifications.remove(rowIndex);
|
||||||
|
rebuildGroups();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const nowMs = Date.now();
|
||||||
|
const groupKey = groupKeyFor(notificationObject);
|
||||||
|
|
||||||
|
const existingIndex = indexOfId(notificationObject.id);
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
notifications.set(existingIndex, {
|
||||||
|
rowNotificationId: notificationObject.id,
|
||||||
|
rowObject: notificationObject,
|
||||||
|
rowVersion: nextVersion(),
|
||||||
|
rowCreatedAt: nowMs,
|
||||||
|
rowGroupKey: groupKey,
|
||||||
|
rowAppName: normalizedString(notificationObject.appName),
|
||||||
|
rowAppIcon: normalizedString(notificationObject.appIcon),
|
||||||
|
rowImage: normalizedString(notificationObject.image),
|
||||||
|
rowDesktopEntry: hintString(notificationObject, "desktop-entry")
|
||||||
|
});
|
||||||
|
rebuildGroups();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
notifications.append({
|
notifications.append({
|
||||||
rowNotificationId: notificationObject.id,
|
rowNotificationId: notificationObject.id,
|
||||||
rowTitle: String(notificationObject.summary || ""),
|
rowObject: notificationObject,
|
||||||
rowBody: String(notificationObject.body || ""),
|
rowVersion: nextVersion(),
|
||||||
rowUrgency: NotificationUrgency.toString(notificationObject.urgency),
|
rowCreatedAt: nowMs,
|
||||||
rowAppName: String(notificationObject.appName || ""),
|
rowGroupKey: groupKey,
|
||||||
rowAppIcon: String(notificationObject.appIcon || ""),
|
rowAppName: normalizedString(notificationObject.appName),
|
||||||
rowImage: String(notificationObject.image || ""),
|
rowAppIcon: normalizedString(notificationObject.appIcon),
|
||||||
rowTimeoutMs: timeoutMsFor(notificationObject),
|
rowImage: normalizedString(notificationObject.image),
|
||||||
rowCritical: notificationObject.urgency === NotificationUrgency.Critical,
|
rowDesktopEntry: hintString(notificationObject, "desktop-entry")
|
||||||
rowHints: notificationObject.hints || ({}),
|
|
||||||
rowObject: notificationObject
|
|
||||||
});
|
});
|
||||||
|
rebuildGroups();
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeById(notificationId, reason) {
|
function closeById(notificationId, reason) {
|
||||||
@@ -70,13 +184,15 @@ QtObject {
|
|||||||
const notifObject = row.rowObject;
|
const notifObject = row.rowObject;
|
||||||
|
|
||||||
if (notifObject) {
|
if (notifObject) {
|
||||||
if (reason === "timeout")
|
if (reason === "timeout") {
|
||||||
notifObject.expire();
|
notifObject.expire();
|
||||||
else
|
} else if (reason === "click" || reason === "dismiss" || reason === "drag") {
|
||||||
notifObject.dismiss();
|
notifObject.dismiss();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
notifications.remove(rowIndex);
|
notifications.remove(rowIndex);
|
||||||
|
rebuildGroups();
|
||||||
}
|
}
|
||||||
|
|
||||||
property NotificationServer server: NotificationServer {
|
property NotificationServer server: NotificationServer {
|
||||||
@@ -84,6 +200,7 @@ QtObject {
|
|||||||
bodySupported: true
|
bodySupported: true
|
||||||
bodyMarkupSupported: false
|
bodyMarkupSupported: false
|
||||||
bodyHyperlinksSupported: false
|
bodyHyperlinksSupported: false
|
||||||
|
actionsSupported: true
|
||||||
|
|
||||||
// NotificationServer is Quickshell's DBus implementation for
|
// NotificationServer is Quickshell's DBus implementation for
|
||||||
// org.freedesktop.Notifications, so this is the notification source.
|
// org.freedesktop.Notifications, so this is the notification source.
|
||||||
|
|||||||
46
Shell/Notifications/NotificationTheme.qml
Normal file
46
Shell/Notifications/NotificationTheme.qml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import QtQuick
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: theme
|
||||||
|
|
||||||
|
readonly property string fontFamily: "8bitoperator JVE"
|
||||||
|
readonly property int fontLetterSpacing: 1
|
||||||
|
|
||||||
|
readonly property color panelBackground: "#000000"
|
||||||
|
readonly property color panelBorder: "#ffffff"
|
||||||
|
readonly property color panelText: "#ffffff"
|
||||||
|
readonly property color hoverInvertBackground: "#ffffff"
|
||||||
|
readonly property color hoverInvertText: "#000000"
|
||||||
|
|
||||||
|
readonly property int stackWidth: 420
|
||||||
|
readonly property int panelMarginTop: 24
|
||||||
|
readonly property int panelMarginRight: 24
|
||||||
|
readonly property int topbarReservedHeight: 182
|
||||||
|
|
||||||
|
readonly property int cardPadding: 14
|
||||||
|
readonly property int borderWidth: 3
|
||||||
|
readonly property int iconBox: 28
|
||||||
|
readonly property int appIconSize: 24
|
||||||
|
readonly property int contentGap: 10
|
||||||
|
readonly property int lineGap: 2
|
||||||
|
readonly property int actionGap: 8
|
||||||
|
readonly property int actionHeight: 34
|
||||||
|
readonly property int actionBorderWidth: 2
|
||||||
|
readonly property int collapsedBodyLines: 2
|
||||||
|
readonly property int expandedBodyLines: 8
|
||||||
|
|
||||||
|
readonly property int stackPeekCount: 2
|
||||||
|
readonly property int stackPeekHeight: 36
|
||||||
|
readonly property int stackPeekStep: 18
|
||||||
|
|
||||||
|
readonly property int dragDismissThreshold: 140
|
||||||
|
|
||||||
|
readonly property int fallbackLowTimeoutMs: 4500
|
||||||
|
readonly property int fallbackNormalTimeoutMs: 5000
|
||||||
|
readonly property int fallbackCriticalTimeoutMs: 8000
|
||||||
|
|
||||||
|
readonly property int enterMs: 170
|
||||||
|
readonly property int exitMs: 140
|
||||||
|
readonly property int fadeMs: 120
|
||||||
|
readonly property int expandMs: 120
|
||||||
|
}
|
||||||
@@ -19,7 +19,9 @@ QtObject {
|
|||||||
property bool appUsageLoaded: false
|
property bool appUsageLoaded: false
|
||||||
property var windowRequests: ({})
|
property var windowRequests: ({})
|
||||||
property var quickSettingsPayload: ({})
|
property var quickSettingsPayload: ({})
|
||||||
property var globals: ({})
|
property var globals: ({
|
||||||
|
"notifications.stackPreviewEnabled": true
|
||||||
|
})
|
||||||
|
|
||||||
signal shellOpened
|
signal shellOpened
|
||||||
signal shellClosed
|
signal shellClosed
|
||||||
@@ -198,10 +200,24 @@ QtObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setGlobal(key, value) {
|
function setGlobal(key, value) {
|
||||||
globals[key] = value;
|
var updated = Object.assign({}, globals);
|
||||||
|
updated[key] = value;
|
||||||
|
globals = updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
function global(key, defaultValue) {
|
function global(key, defaultValue) {
|
||||||
return globals.hasOwnProperty(key) ? globals[key] : defaultValue;
|
return globals.hasOwnProperty(key) ? globals[key] : defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function notificationStackPreviewEnabled() {
|
||||||
|
return Boolean(global("notifications.stackPreviewEnabled", true));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNotificationStackPreviewEnabled(enabled) {
|
||||||
|
setGlobal("notifications.stackPreviewEnabled", Boolean(enabled));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleNotificationStackPreviewEnabled() {
|
||||||
|
setNotificationStackPreviewEnabled(!notificationStackPreviewEnabled());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ Item {
|
|||||||
property int activeSelection: 0
|
property int activeSelection: 0
|
||||||
property bool inBluetoothMenu: false
|
property bool inBluetoothMenu: false
|
||||||
property bool inWallpaperMenu: false
|
property bool inWallpaperMenu: false
|
||||||
|
property bool inNotificationsMenu: false
|
||||||
|
|
||||||
property bool isSelected: false
|
property bool isSelected: false
|
||||||
|
|
||||||
@@ -81,7 +82,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function currentAction() {
|
function currentAction() {
|
||||||
if (root.inBluetoothMenu || root.inWallpaperMenu)
|
if (root.inBluetoothMenu || root.inWallpaperMenu || root.inNotificationsMenu)
|
||||||
return null;
|
return null;
|
||||||
return menuLength() > 0 ? menuAt(activeSelection) : null;
|
return menuLength() > 0 ? menuAt(activeSelection) : null;
|
||||||
}
|
}
|
||||||
@@ -195,6 +196,7 @@ Item {
|
|||||||
|
|
||||||
property int bluetoothActionIndex: 1
|
property int bluetoothActionIndex: 1
|
||||||
property int wallpaperActionIndex: 2
|
property int wallpaperActionIndex: 2
|
||||||
|
property int notificationsActionIndex: 3
|
||||||
property var actions: [
|
property var actions: [
|
||||||
{
|
{
|
||||||
name: "Master Volume",
|
name: "Master Volume",
|
||||||
@@ -218,6 +220,7 @@ Item {
|
|||||||
ent: function () {
|
ent: function () {
|
||||||
root.inBluetoothMenu = true;
|
root.inBluetoothMenu = true;
|
||||||
root.inWallpaperMenu = false;
|
root.inWallpaperMenu = false;
|
||||||
|
root.inNotificationsMenu = false;
|
||||||
root.isSelected = false;
|
root.isSelected = false;
|
||||||
root.activeSelection = 0;
|
root.activeSelection = 0;
|
||||||
root.clampSelection();
|
root.clampSelection();
|
||||||
@@ -231,6 +234,7 @@ Item {
|
|||||||
ent: function () {
|
ent: function () {
|
||||||
root.inWallpaperMenu = true;
|
root.inWallpaperMenu = true;
|
||||||
root.inBluetoothMenu = false;
|
root.inBluetoothMenu = false;
|
||||||
|
root.inNotificationsMenu = false;
|
||||||
root.isSelected = false;
|
root.isSelected = false;
|
||||||
var currentIndex = root.currentWallpaperIndex();
|
var currentIndex = root.currentWallpaperIndex();
|
||||||
root.activeSelection = currentIndex >= 0 ? currentIndex : 0;
|
root.activeSelection = currentIndex >= 0 ? currentIndex : 0;
|
||||||
@@ -241,10 +245,39 @@ Item {
|
|||||||
getState: function () {
|
getState: function () {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Notifications",
|
||||||
|
ent: function () {
|
||||||
|
root.inNotificationsMenu = true;
|
||||||
|
root.inBluetoothMenu = false;
|
||||||
|
root.inWallpaperMenu = false;
|
||||||
|
root.isSelected = false;
|
||||||
|
root.activeSelection = 0;
|
||||||
|
root.clampSelection();
|
||||||
|
},
|
||||||
|
getState: function () {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
property var menuModel: root.inBluetoothMenu ? Bluetooth.devices : (root.inWallpaperMenu ? wallpaperFolderModel : actions)
|
property var notificationActions: [
|
||||||
|
{
|
||||||
|
name: "Stack Preview",
|
||||||
|
ent: function () {
|
||||||
|
if (manager && manager.toggleNotificationStackPreviewEnabled)
|
||||||
|
manager.toggleNotificationStackPreviewEnabled();
|
||||||
|
},
|
||||||
|
getState: function () {
|
||||||
|
if (!manager || !manager.notificationStackPreviewEnabled)
|
||||||
|
return "ON";
|
||||||
|
return manager.notificationStackPreviewEnabled() ? "ON" : "OFF";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
property var menuModel: root.inBluetoothMenu ? Bluetooth.devices : (root.inWallpaperMenu ? wallpaperFolderModel : (root.inNotificationsMenu ? notificationActions : actions))
|
||||||
|
|
||||||
FolderListModel {
|
FolderListModel {
|
||||||
id: wallpaperFolderModel
|
id: wallpaperFolderModel
|
||||||
@@ -320,7 +353,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: root.inBluetoothMenu ? "BLUETOOTH" : (root.inWallpaperMenu ? "WALLPAPER" : "CONFIG")
|
text: root.inBluetoothMenu ? "BLUETOOTH" : (root.inWallpaperMenu ? "WALLPAPER" : (root.inNotificationsMenu ? "NOTIFICATIONS" : "CONFIG"))
|
||||||
font.family: "8bitoperator JVE"
|
font.family: "8bitoperator JVE"
|
||||||
font.pixelSize: 71
|
font.pixelSize: 71
|
||||||
renderType: Text.NativeRendering
|
renderType: Text.NativeRendering
|
||||||
@@ -440,6 +473,10 @@ Item {
|
|||||||
const wallpaperPath = wallpaperPathAt(activeSelection);
|
const wallpaperPath = wallpaperPathAt(activeSelection);
|
||||||
if (wallpaperPath && wallpaperPath.length > 0)
|
if (wallpaperPath && wallpaperPath.length > 0)
|
||||||
root.applyWallpaper(wallpaperPath);
|
root.applyWallpaper(wallpaperPath);
|
||||||
|
} else if (root.inNotificationsMenu) {
|
||||||
|
const notificationAction = menuAt(activeSelection);
|
||||||
|
if (notificationAction && notificationAction.ent)
|
||||||
|
notificationAction.ent();
|
||||||
} else {
|
} else {
|
||||||
const a = currentAction();
|
const a = currentAction();
|
||||||
if (a && a.ent) {
|
if (a && a.ent) {
|
||||||
@@ -459,6 +496,11 @@ Item {
|
|||||||
root.scrollOffset = 0;
|
root.scrollOffset = 0;
|
||||||
root.activeSelection = root.wallpaperActionIndex;
|
root.activeSelection = root.wallpaperActionIndex;
|
||||||
root.clampSelection();
|
root.clampSelection();
|
||||||
|
} else if (root.inNotificationsMenu) {
|
||||||
|
root.inNotificationsMenu = false;
|
||||||
|
root.isSelected = false;
|
||||||
|
root.activeSelection = root.notificationsActionIndex;
|
||||||
|
root.clampSelection();
|
||||||
} else if (root.inBluetoothMenu) {
|
} else if (root.inBluetoothMenu) {
|
||||||
root.inBluetoothMenu = false;
|
root.inBluetoothMenu = false;
|
||||||
root.isSelected = false;
|
root.isSelected = false;
|
||||||
@@ -481,6 +523,7 @@ Item {
|
|||||||
root.isSelected = false;
|
root.isSelected = false;
|
||||||
root.inBluetoothMenu = false;
|
root.inBluetoothMenu = false;
|
||||||
root.inWallpaperMenu = false;
|
root.inWallpaperMenu = false;
|
||||||
|
root.inNotificationsMenu = false;
|
||||||
root.scrollOffset = 0;
|
root.scrollOffset = 0;
|
||||||
root.refreshCurrentWallpaper();
|
root.refreshCurrentWallpaper();
|
||||||
ShellInputManager.registerHandler("quickSettings", handleKey);
|
ShellInputManager.registerHandler("quickSettings", handleKey);
|
||||||
|
|||||||
Reference in New Issue
Block a user