393 lines
11 KiB
QML
393 lines
11 KiB
QML
import QtQuick
|
|
import Qt5Compat.GraphicalEffects
|
|
import Quickshell
|
|
import Quickshell.Widgets
|
|
|
|
Item {
|
|
id: root
|
|
|
|
property int notificationId: -1
|
|
property string notifTitle: ""
|
|
property string notifBody: ""
|
|
property string notifUrgency: "Normal"
|
|
property string notifAppName: ""
|
|
property string notifAppIcon: ""
|
|
property string notifImage: ""
|
|
property int notifTimeoutMs: 5000
|
|
property bool notifCritical: false
|
|
property var notifHints: ({})
|
|
|
|
signal closeFinished(int notificationId, string reason)
|
|
|
|
property bool highlighted: hoverHandler.hovered
|
|
property bool closing: false
|
|
property string closeReason: ""
|
|
property real borderAlpha: 1
|
|
property real textAlpha: 1
|
|
property real collapseFactor: 1
|
|
|
|
readonly property color normalInk: "#ffffff"
|
|
readonly property color highlightInk: "#ffc90e"
|
|
readonly property color activeInk: highlighted ? highlightInk : normalInk
|
|
readonly property int padding: 14
|
|
readonly property int iconBox: 28
|
|
readonly property int appIconSize: 24
|
|
readonly property string appIconSource: {
|
|
if (notifImage && notifImage.length > 0)
|
|
return notifImage;
|
|
|
|
if (!notifAppIcon || notifAppIcon.length === 0)
|
|
return "";
|
|
|
|
if (notifAppIcon.indexOf("/") >= 0 || notifAppIcon.indexOf("file://") === 0)
|
|
return notifAppIcon;
|
|
|
|
return Quickshell.iconPath(notifAppIcon);
|
|
}
|
|
|
|
function hintString(name) {
|
|
if (!notifHints || typeof notifHints !== "object")
|
|
return "";
|
|
const value = notifHints[name];
|
|
return value === undefined || value === null ? "" : String(value);
|
|
}
|
|
|
|
function textBlob() {
|
|
return (notifAppName + " " + notifTitle + " " + notifBody + " " + hintString("category")).toLowerCase();
|
|
}
|
|
|
|
function parseVolumePercent() {
|
|
const source = (notifTitle + " " + notifBody).toUpperCase();
|
|
const match = source.match(/VOLUME\s*(\d{1,3})%/);
|
|
if (!match || match.length < 2)
|
|
return -1;
|
|
|
|
const parsed = Number(match[1]);
|
|
if (!Number.isFinite(parsed))
|
|
return -1;
|
|
|
|
return Math.max(0, Math.min(100, parsed));
|
|
}
|
|
|
|
readonly property int volumePercent: parseVolumePercent()
|
|
readonly property bool isVolumeLayout: volumePercent >= 0
|
|
|
|
function soulColor() {
|
|
const full = textBlob();
|
|
const urgencyText = String(notifUrgency || "").toLowerCase();
|
|
|
|
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) {
|
|
if (closing)
|
|
return;
|
|
|
|
closing = true;
|
|
closeReason = reason;
|
|
timeoutTimer.stop();
|
|
|
|
if (reason === "click") {
|
|
heartFlash.restart();
|
|
clickClose.start();
|
|
} else {
|
|
timeoutClose.start();
|
|
}
|
|
}
|
|
|
|
implicitWidth: 420
|
|
implicitHeight: Math.max(1, contentColumn.implicitHeight * collapseFactor + padding * 2)
|
|
width: implicitWidth
|
|
height: implicitHeight
|
|
transformOrigin: Item.TopRight
|
|
|
|
SequentialAnimation {
|
|
id: entryAnimation
|
|
running: true
|
|
|
|
PropertyAction {
|
|
target: root
|
|
property: "opacity"
|
|
value: 0
|
|
}
|
|
PropertyAction {
|
|
target: root
|
|
property: "scale"
|
|
value: 0.95
|
|
}
|
|
|
|
ParallelAnimation {
|
|
NumberAnimation {
|
|
target: root
|
|
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 {
|
|
id: heartFlash
|
|
NumberAnimation {
|
|
target: soulFlashOverlay
|
|
property: "opacity"
|
|
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 {
|
|
id: timeoutTimer
|
|
interval: root.notifTimeoutMs
|
|
repeat: false
|
|
running: !root.notifCritical && root.notifTimeoutMs > 0
|
|
onTriggered: root.beginClose("timeout")
|
|
}
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
color: "#000000"
|
|
border.width: 3
|
|
border.color: Qt.rgba(root.activeInk.r, root.activeInk.g, root.activeInk.b, root.borderAlpha)
|
|
radius: 0
|
|
antialiasing: false
|
|
}
|
|
|
|
Item {
|
|
id: soulContainer
|
|
x: {
|
|
if (!root.isVolumeLayout)
|
|
return root.padding;
|
|
|
|
const left = root.padding;
|
|
const right = Math.max(left, root.width - root.padding - root.iconBox);
|
|
return left + (right - left) * (root.volumePercent / 100);
|
|
}
|
|
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 {
|
|
id: contentColumn
|
|
x: root.padding + root.iconBox + 10
|
|
y: root.padding - 1
|
|
width: root.width - x - root.padding
|
|
spacing: 2
|
|
|
|
Text {
|
|
visible: !root.isVolumeLayout
|
|
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
|
|
}
|
|
|
|
Text {
|
|
visible: !root.isVolumeLayout
|
|
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
|
|
}
|
|
|
|
Text {
|
|
visible: root.isVolumeLayout
|
|
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
|
|
}
|
|
|
|
}
|
|
|
|
HoverHandler {
|
|
id: hoverHandler
|
|
}
|
|
|
|
TapHandler {
|
|
acceptedButtons: Qt.LeftButton
|
|
enabled: root.highlighted && !root.closing
|
|
onTapped: root.beginClose("click")
|
|
}
|
|
}
|