summaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/BatteryMonitor.qml54
-rw-r--r--modules/Shortcuts.qml21
-rw-r--r--modules/drawers/Panels.qml26
-rw-r--r--modules/utilities/toasts/ToastItem.qml135
-rw-r--r--modules/utilities/toasts/Toasts.qml142
5 files changed, 370 insertions, 8 deletions
diff --git a/modules/BatteryMonitor.qml b/modules/BatteryMonitor.qml
new file mode 100644
index 0000000..f407edf
--- /dev/null
+++ b/modules/BatteryMonitor.qml
@@ -0,0 +1,54 @@
+import qs.config
+import Caelestia
+import Quickshell
+import Quickshell.Services.UPower
+import QtQuick
+
+Scope {
+ id: root
+
+ readonly property list<var> warnLevels: [...Config.general.battery.warnLevels].sort((a, b) => b.level - a.level)
+
+ Connections {
+ target: UPower
+
+ function onOnBatteryChanged(): void {
+ if (UPower.onBattery) {
+ Toaster.toast(qsTr("Charger unplugged"), qsTr("Battery is now on AC"), "power_off");
+ } else {
+ Toaster.toast(qsTr("Charger plugged in"), qsTr("Battery is charging"), "battery_android_frame_bolt");
+ for (const level of root.warnLevels)
+ level.warned = false;
+ }
+ }
+ }
+
+ Connections {
+ target: UPower.displayDevice
+
+ function onPercentageChanged(): void {
+ if (!UPower.onBattery)
+ return;
+
+ const p = UPower.displayDevice.percentage;
+ for (const level of root.warnLevels) {
+ if (p <= level.level && !level.warned) {
+ level.warned = true;
+ Toaster.toast(level.title ?? qsTr("Battery warning"), level.message ?? qsTr("Battery level is low"), level.icon ?? "battery_android_alert", level.critical ? Toast.Error : Toast.Warning);
+ }
+ }
+
+ if (!hibernateTimer.running && p <= Config.general.battery.criticalLevel) {
+ Toaster.toast(qsTr("Hibernating in 5 seconds"), qsTr("Hibernating to prevent data loss"), "battery_android_alert", Toast.Error);
+ hibernateTimer.start();
+ }
+ }
+ }
+
+ Timer {
+ id: hibernateTimer
+
+ interval: 5000
+ onTriggered: Quickshell.execDetached(["systemctl", "hibernate"])
+ }
+}
diff --git a/modules/Shortcuts.qml b/modules/Shortcuts.qml
index bf1cded..a62b827 100644
--- a/modules/Shortcuts.qml
+++ b/modules/Shortcuts.qml
@@ -1,6 +1,7 @@
import qs.components.misc
import qs.modules.controlcenter
import qs.services
+import Caelestia
import Quickshell
import Quickshell.Io
@@ -95,4 +96,24 @@ Scope {
WindowFactory.create();
}
}
+
+ IpcHandler {
+ target: "toaster"
+
+ function info(title: string, message: string, icon: string): void {
+ Toaster.toast(title, message, icon, Toast.Info);
+ }
+
+ function success(title: string, message: string, icon: string): void {
+ Toaster.toast(title, message, icon, Toast.Success);
+ }
+
+ function warn(title: string, message: string, icon: string): void {
+ Toaster.toast(title, message, icon, Toast.Warning);
+ }
+
+ function error(title: string, message: string, icon: string): void {
+ Toaster.toast(title, message, icon, Toast.Error);
+ }
+ }
}
diff --git a/modules/drawers/Panels.qml b/modules/drawers/Panels.qml
index 7896ddb..4ce1182 100644
--- a/modules/drawers/Panels.qml
+++ b/modules/drawers/Panels.qml
@@ -6,6 +6,7 @@ import qs.modules.launcher as Launcher
import qs.modules.dashboard as Dashboard
import qs.modules.bar.popouts as BarPopouts
import qs.modules.utilities as Utilities
+import qs.modules.utilities.toasts as Toasts
import qs.modules.sidebar as Sidebar
import Quickshell
import QtQuick
@@ -17,14 +18,15 @@ Item {
required property PersistentProperties visibilities
required property Item bar
- readonly property Osd.Wrapper osd: osd
- readonly property Notifications.Wrapper notifications: notifications
- readonly property Session.Wrapper session: session
- readonly property Launcher.Wrapper launcher: launcher
- readonly property Dashboard.Wrapper dashboard: dashboard
- readonly property BarPopouts.Wrapper popouts: popouts
- readonly property Utilities.Wrapper utilities: utilities
- readonly property Sidebar.Wrapper sidebar: sidebar
+ readonly property alias osd: osd
+ readonly property alias notifications: notifications
+ readonly property alias session: session
+ readonly property alias launcher: launcher
+ readonly property alias dashboard: dashboard
+ readonly property alias popouts: popouts
+ readonly property alias utilities: utilities
+ readonly property alias toasts: toasts
+ readonly property alias sidebar: sidebar
anchors.fill: parent
anchors.margins: Config.border.thickness
@@ -112,6 +114,14 @@ Item {
anchors.right: parent.right
}
+ Toasts.Toasts {
+ id: toasts
+
+ anchors.bottom: sidebar.visible ? parent.bottom : utilities.top
+ anchors.right: sidebar.left
+ anchors.margins: Appearance.padding.normal
+ }
+
Sidebar.Wrapper {
id: sidebar
diff --git a/modules/utilities/toasts/ToastItem.qml b/modules/utilities/toasts/ToastItem.qml
new file mode 100644
index 0000000..481b831
--- /dev/null
+++ b/modules/utilities/toasts/ToastItem.qml
@@ -0,0 +1,135 @@
+import qs.components
+import qs.components.effects
+import qs.services
+import qs.config
+import Caelestia
+import QtQuick
+import QtQuick.Layouts
+
+StyledRect {
+ id: root
+
+ required property Toast modelData
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ implicitHeight: layout.implicitHeight + Appearance.padding.smaller * 2
+
+ radius: Appearance.rounding.normal
+ color: {
+ if (root.modelData.type === Toast.Success)
+ return Colours.palette.m3successContainer;
+ if (root.modelData.type === Toast.Warning)
+ return Colours.palette.m3secondary;
+ if (root.modelData.type === Toast.Error)
+ return Colours.palette.m3errorContainer;
+ return Colours.palette.m3surface;
+ }
+
+ border.width: 1
+ border.color: {
+ let colour = Colours.palette.m3outlineVariant;
+ if (root.modelData.type === Toast.Success)
+ colour = Colours.palette.m3success;
+ if (root.modelData.type === Toast.Warning)
+ colour = Colours.palette.m3secondaryContainer;
+ if (root.modelData.type === Toast.Error)
+ colour = Colours.palette.m3error;
+ return Qt.alpha(colour, 0.3);
+ }
+
+ Elevation {
+ anchors.fill: parent
+ radius: parent.radius
+ opacity: parent.opacity
+ z: -1
+ level: 3
+ }
+
+ RowLayout {
+ id: layout
+
+ anchors.fill: parent
+ anchors.margins: Appearance.padding.smaller
+ anchors.leftMargin: Appearance.padding.normal
+ anchors.rightMargin: Appearance.padding.normal
+ spacing: Appearance.spacing.normal
+
+ StyledRect {
+ radius: Appearance.rounding.normal
+ color: {
+ if (root.modelData.type === Toast.Success)
+ return Colours.palette.m3success;
+ if (root.modelData.type === Toast.Warning)
+ return Colours.palette.m3secondaryContainer;
+ if (root.modelData.type === Toast.Error)
+ return Colours.palette.m3error;
+ return Colours.palette.m3surfaceContainerHigh;
+ }
+
+ implicitWidth: implicitHeight
+ implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2
+
+ MaterialIcon {
+ id: icon
+
+ anchors.centerIn: parent
+ text: root.modelData.icon
+ color: {
+ if (root.modelData.type === Toast.Success)
+ return Colours.palette.m3onSuccess;
+ if (root.modelData.type === Toast.Warning)
+ return Colours.palette.m3onSecondaryContainer;
+ if (root.modelData.type === Toast.Error)
+ return Colours.palette.m3onError;
+ return Colours.palette.m3onSurfaceVariant;
+ }
+ font.pointSize: Appearance.font.size.large
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 0
+
+ StyledText {
+ id: title
+
+ Layout.fillWidth: true
+ text: root.modelData.title
+ color: {
+ if (root.modelData.type === Toast.Success)
+ return Colours.palette.m3onSuccessContainer;
+ if (root.modelData.type === Toast.Warning)
+ return Colours.palette.m3onSecondary;
+ if (root.modelData.type === Toast.Error)
+ return Colours.palette.m3onErrorContainer;
+ return Colours.palette.m3onSurface;
+ }
+ font.pointSize: Appearance.font.size.normal
+ elide: Text.ElideRight
+ }
+
+ StyledText {
+ Layout.fillWidth: true
+ textFormat: Text.StyledText
+ text: root.modelData.message
+ color: {
+ if (root.modelData.type === Toast.Success)
+ return Colours.palette.m3onSuccessContainer;
+ if (root.modelData.type === Toast.Warning)
+ return Colours.palette.m3onSecondary;
+ if (root.modelData.type === Toast.Error)
+ return Colours.palette.m3onErrorContainer;
+ return Colours.palette.m3onSurface;
+ }
+ opacity: 0.8
+ elide: Text.ElideRight
+ }
+ }
+ }
+
+ Behavior on border.color {
+ CAnim {}
+ }
+}
diff --git a/modules/utilities/toasts/Toasts.qml b/modules/utilities/toasts/Toasts.qml
new file mode 100644
index 0000000..c9a8d4d
--- /dev/null
+++ b/modules/utilities/toasts/Toasts.qml
@@ -0,0 +1,142 @@
+pragma ComponentBehavior: Bound
+
+import qs.components
+import qs.config
+import Caelestia
+import Quickshell
+import QtQuick
+import QtQuick.Layouts
+
+Item {
+ id: root
+
+ readonly property int spacing: Appearance.spacing.small
+ property bool flag
+
+ implicitWidth: Config.utilities.sizes.width - Appearance.padding.normal * 2
+ implicitHeight: {
+ let h = -spacing;
+ for (let i = 0; i < repeater.count; i++) {
+ const item = repeater.itemAt(i);
+ if (!item.modelData.closed && !item.previewHidden)
+ h += item.implicitHeight + spacing;
+ }
+ return h;
+ }
+
+ Repeater {
+ id: repeater
+
+ model: ScriptModel {
+ values: {
+ const toasts = [];
+ let count = 0;
+ for (const toast of Toaster.toasts) {
+ toasts.push(toast);
+ if (!toast.closed) {
+ count++;
+ if (count > Config.utilities.maxToasts)
+ break;
+ }
+ }
+ return toasts;
+ }
+ onValuesChanged: root.flagChanged()
+ }
+
+ MouseArea {
+ id: toast
+
+ required property int index
+ required property Toast modelData
+
+ readonly property bool previewHidden: {
+ let extraHidden = 0;
+ for (let i = 0; i < index; i++)
+ if (Toaster.toasts[i].closed)
+ extraHidden++;
+ return index >= Config.utilities.maxToasts + extraHidden;
+ }
+
+ onPreviewHiddenChanged: {
+ if (initAnim.running && previewHidden)
+ initAnim.stop();
+ }
+
+ opacity: modelData.closed || previewHidden ? 0 : 1
+ scale: modelData.closed || previewHidden ? 0.7 : 1
+
+ anchors.bottomMargin: {
+ root.flag; // Force update
+ let y = 0;
+ for (let i = 0; i < index; i++) {
+ const item = repeater.itemAt(i);
+ if (item && !item.modelData.closed && !item.previewHidden)
+ y += item.implicitHeight + root.spacing;
+ }
+ return y;
+ }
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ implicitHeight: toastInner.implicitHeight
+
+ acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
+ onClicked: modelData.close()
+
+ Component.onCompleted: modelData.lock(this)
+
+ Anim {
+ id: initAnim
+
+ Component.onCompleted: running = !toast.previewHidden
+
+ target: toast
+ properties: "opacity,scale"
+ from: 0
+ to: 1
+ duration: Appearance.anim.durations.expressiveDefaultSpatial
+ easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
+ }
+
+ ParallelAnimation {
+ running: toast.modelData.closed
+ onStarted: toast.anchors.bottomMargin = toast.anchors.bottomMargin
+ onFinished: toast.modelData.unlock(toast)
+
+ Anim {
+ target: toast
+ property: "opacity"
+ to: 0
+ }
+ Anim {
+ target: toast
+ property: "scale"
+ to: 0.7
+ }
+ }
+
+ ToastItem {
+ id: toastInner
+
+ modelData: toast.modelData
+ }
+
+ Behavior on opacity {
+ Anim {}
+ }
+
+ Behavior on scale {
+ Anim {}
+ }
+
+ Behavior on anchors.bottomMargin {
+ Anim {
+ duration: Appearance.anim.durations.expressiveDefaultSpatial
+ easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
+ }
+ }
+ }
+ }
+}