summaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/lock/NotifGroup.qml2
-rw-r--r--modules/sidebar/Content.qml26
-rw-r--r--modules/sidebar/Notif.qml166
-rw-r--r--modules/sidebar/NotifDock.qml208
-rw-r--r--modules/sidebar/NotifGroup.qml216
-rw-r--r--modules/sidebar/Props.qml7
-rw-r--r--modules/sidebar/Wrapper.qml8
7 files changed, 626 insertions, 7 deletions
diff --git a/modules/lock/NotifGroup.qml b/modules/lock/NotifGroup.qml
index 344c39a..3669e5e 100644
--- a/modules/lock/NotifGroup.qml
+++ b/modules/lock/NotifGroup.qml
@@ -32,7 +32,7 @@ StyledRect {
color: root.urgency === "critical" ? Colours.palette.m3secondaryContainer : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2)
RetainableLock {
- object: root.notifs[0]?.notiftication ?? null
+ object: root.notifs[0]?.notification ?? null
locked: true
}
diff --git a/modules/sidebar/Content.qml b/modules/sidebar/Content.qml
index 13829bd..2ea8e55 100644
--- a/modules/sidebar/Content.qml
+++ b/modules/sidebar/Content.qml
@@ -1,3 +1,5 @@
+import qs.components
+import qs.services
import qs.config
import QtQuick
import QtQuick.Layouts
@@ -5,15 +7,33 @@ import QtQuick.Layouts
Item {
id: root
+ required property Props props
required property var visibilities
- implicitWidth: layout.implicitWidth
- implicitHeight: layout.implicitHeight
-
ColumnLayout {
id: layout
anchors.fill: parent
spacing: Appearance.spacing.normal
+
+ StyledRect {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ radius: Appearance.rounding.normal
+ color: Colours.tPalette.m3surfaceContainerLow
+
+ NotifDock {
+ props: root.props
+ }
+ }
+
+ StyledRect {
+ Layout.topMargin: Appearance.padding.large - layout.spacing
+ Layout.fillWidth: true
+ implicitHeight: 1
+
+ color: Colours.tPalette.m3outlineVariant
+ }
}
}
diff --git a/modules/sidebar/Notif.qml b/modules/sidebar/Notif.qml
new file mode 100644
index 0000000..8b96792
--- /dev/null
+++ b/modules/sidebar/Notif.qml
@@ -0,0 +1,166 @@
+pragma ComponentBehavior: Bound
+
+import qs.components
+import qs.components.controls
+import qs.services
+import qs.config
+import Quickshell
+import QtQuick
+import QtQuick.Layouts
+
+StyledRect {
+ id: root
+
+ required property Notifs.Notif modelData
+ required property Props props
+ required property bool expanded
+
+ Layout.fillWidth: true
+ implicitHeight: expanded ? summary.implicitHeight + expandedContent.implicitHeight + expandedContent.anchors.topMargin + Appearance.padding.normal * 2 : summary.implicitHeight
+
+ radius: Appearance.rounding.small
+ color: {
+ const c = root.modelData.urgency === "critical" ? Colours.palette.m3secondaryContainer : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2);
+ return expanded ? c : Qt.alpha(c, 0);
+ }
+
+ states: State {
+ name: "expanded"
+ when: root.expanded
+
+ PropertyChanges {
+ summary.anchors.margins: Appearance.padding.normal
+ dummySummary.anchors.margins: Appearance.padding.normal
+ compactBody.anchors.margins: Appearance.padding.normal
+ timeStr.anchors.margins: Appearance.padding.normal
+ expandedContent.anchors.margins: Appearance.padding.normal
+ summary.width: root.width - Appearance.padding.normal * 2 - timeStr.implicitWidth - Appearance.spacing.small
+ }
+ }
+
+ transitions: Transition {
+ Anim {
+ properties: "margins,width"
+ }
+ }
+
+ ParallelAnimation {
+ running: true
+
+ Anim {
+ target: root
+ property: "opacity"
+ from: 0
+ to: 1
+ }
+ Anim {
+ target: root
+ property: "scale"
+ from: 0.7
+ to: 1
+ }
+ // Anim {
+ // target: root.Layout
+ // property: "preferredHeight"
+ // from: 0
+ // to: root.implicitHeight
+ // }
+ }
+
+ RetainableLock {
+ object: root.modelData.notification
+ locked: true
+ }
+
+ StyledText {
+ id: summary
+
+ anchors.top: parent.top
+ anchors.left: parent.left
+
+ width: parent.width
+ text: root.modelData.summary
+ color: root.modelData.urgency === "critical" ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface
+ elide: Text.ElideRight
+ }
+
+ StyledText {
+ id: dummySummary
+
+ anchors.top: parent.top
+ anchors.left: parent.left
+
+ visible: false
+ text: root.modelData.summary
+ }
+
+ WrappedLoader {
+ id: compactBody
+
+ shouldBeActive: !root.expanded
+ anchors.top: parent.top
+ anchors.left: dummySummary.right
+ anchors.right: parent.right
+ anchors.leftMargin: Appearance.spacing.small
+
+ sourceComponent: StyledText {
+ text: root.modelData.body.replace(/\n/g, " ")
+ color: root.modelData.urgency === "critical" ? Colours.palette.m3secondary : Colours.palette.m3outline
+ elide: Text.ElideRight
+ }
+ }
+
+ WrappedLoader {
+ id: timeStr
+
+ shouldBeActive: root.expanded
+ anchors.top: parent.top
+ anchors.right: parent.right
+
+ sourceComponent: StyledText {
+ animate: true
+ text: root.modelData.timeStr
+ color: Colours.palette.m3outline
+ font.pointSize: Appearance.font.size.small
+ }
+ }
+
+ WrappedLoader {
+ id: expandedContent
+
+ shouldBeActive: root.expanded
+ anchors.top: summary.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.topMargin: Appearance.spacing.small / 2
+
+ sourceComponent: ColumnLayout {
+ spacing: Math.floor(Appearance.spacing.small / 2)
+
+ StyledText {
+ Layout.fillWidth: true
+ text: root.modelData.body || qsTr("No body here! :/")
+ color: root.modelData.urgency === "critical" ? Colours.palette.m3secondary : Colours.palette.m3outline
+ wrapMode: Text.WordWrap
+ }
+ }
+ }
+
+ Behavior on implicitHeight {
+ Anim {
+ duration: Appearance.anim.durations.expressiveDefaultSpatial
+ easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
+ }
+ }
+
+ component WrappedLoader: Loader {
+ required property bool shouldBeActive
+
+ opacity: shouldBeActive ? 1 : 0
+ active: opacity > 0
+
+ Behavior on opacity {
+ Anim {}
+ }
+ }
+}
diff --git a/modules/sidebar/NotifDock.qml b/modules/sidebar/NotifDock.qml
new file mode 100644
index 0000000..36b6665
--- /dev/null
+++ b/modules/sidebar/NotifDock.qml
@@ -0,0 +1,208 @@
+pragma ComponentBehavior: Bound
+
+import qs.components
+import qs.components.controls
+import qs.components.containers
+import qs.components.effects
+import qs.services
+import qs.config
+import Quickshell
+import Quickshell.Widgets
+import QtQuick
+import QtQuick.Layouts
+
+Item {
+ id: root
+
+ required property Props props
+
+ anchors.fill: parent
+ anchors.margins: Appearance.padding.normal
+
+ Component.onCompleted: Notifs.list.forEach(n => n.popup = false)
+
+ StyledText {
+ id: title
+
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.margins: Appearance.padding.small
+
+ text: Notifs.list.length > 0 ? qsTr("%1 notification%2").arg(Notifs.list.length).arg(Notifs.list.length === 1 ? "" : "s") : qsTr("Notifications")
+ color: Colours.palette.m3outline
+ font.pointSize: Appearance.font.size.normal
+ font.family: Appearance.font.family.mono
+ font.weight: 500
+ elide: Text.ElideRight
+ }
+
+ ClippingRectangle {
+ id: clipRect
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: title.bottom
+ anchors.bottom: parent.bottom
+ anchors.topMargin: Appearance.spacing.smaller
+
+ radius: Appearance.rounding.small
+ color: "transparent"
+
+ Loader {
+ anchors.centerIn: parent
+ asynchronous: true
+ active: opacity > 0
+ opacity: Notifs.list.length > 0 ? 0 : 1
+
+ sourceComponent: ColumnLayout {
+ spacing: Appearance.spacing.large
+
+ Image {
+ asynchronous: true
+ source: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/dino.png`)
+ fillMode: Image.PreserveAspectFit
+ sourceSize.width: clipRect.width * 0.8
+
+ layer.enabled: true
+ layer.effect: Colouriser {
+ colorizationColor: Colours.palette.m3outlineVariant
+ brightness: 1
+ }
+ }
+
+ StyledText {
+ Layout.alignment: Qt.AlignHCenter
+ text: qsTr("No Notifications")
+ color: Colours.palette.m3outlineVariant
+ font.pointSize: Appearance.font.size.large
+ font.family: Appearance.font.family.mono
+ font.weight: 500
+ }
+ }
+
+ Behavior on opacity {
+ Anim {
+ duration: Appearance.anim.durations.extraLarge
+ }
+ }
+ }
+
+ StyledListView {
+ anchors.fill: parent
+
+ spacing: Appearance.spacing.small
+ clip: true
+
+ model: ScriptModel {
+ values: [...new Set(Notifs.list.map(notif => notif.appName))].reverse()
+ }
+
+ delegate: NotifGroup {
+ props: root.props
+ }
+
+ add: Transition {
+ Anim {
+ property: "opacity"
+ from: 0
+ to: 1
+ }
+ Anim {
+ property: "scale"
+ from: 0
+ to: 1
+ duration: Appearance.anim.durations.expressiveDefaultSpatial
+ easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
+ }
+ }
+
+ remove: Transition {
+ Anim {
+ property: "opacity"
+ to: 0
+ }
+ Anim {
+ property: "scale"
+ to: 0.6
+ }
+ }
+
+ move: Transition {
+ Anim {
+ properties: "opacity,scale"
+ to: 1
+ }
+ Anim {
+ property: "y"
+ duration: Appearance.anim.durations.expressiveDefaultSpatial
+ easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
+ }
+ }
+
+ displaced: Transition {
+ Anim {
+ properties: "opacity,scale"
+ to: 1
+ }
+ Anim {
+ property: "y"
+ duration: Appearance.anim.durations.expressiveDefaultSpatial
+ easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
+ }
+ }
+ }
+ }
+
+ Timer {
+ id: clearTimer
+
+ repeat: true
+ interval: 50
+ onTriggered: {
+ Notifs.list[0]?.notification.dismiss();
+ if (Notifs.list.length === 0)
+ stop();
+ }
+ }
+
+ Loader {
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.margins: Appearance.padding.normal
+
+ scale: Notifs.list.length > 0 ? 1 : 0.5
+ opacity: Notifs.list.length > 0 ? 1 : 0
+ active: opacity > 0
+
+ sourceComponent: IconButton {
+ id: clearBtn
+
+ icon: "clear_all"
+ radius: Appearance.rounding.normal
+ padding: Appearance.padding.normal
+ font.pointSize: Math.round(Appearance.font.size.large * 1.3)
+ onClicked: clearTimer.start()
+
+ Elevation {
+ anchors.fill: parent
+ radius: parent.radius
+ z: -1
+ level: clearBtn.stateLayer.containsMouse ? 4 : 3
+ }
+ }
+
+ Behavior on scale {
+ Anim {
+ duration: Appearance.anim.durations.expressiveFastSpatial
+ easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
+ }
+ }
+
+ Behavior on opacity {
+ Anim {
+ duration: Appearance.anim.durations.expressiveFastSpatial
+ }
+ }
+ }
+}
diff --git a/modules/sidebar/NotifGroup.qml b/modules/sidebar/NotifGroup.qml
new file mode 100644
index 0000000..154b530
--- /dev/null
+++ b/modules/sidebar/NotifGroup.qml
@@ -0,0 +1,216 @@
+pragma ComponentBehavior: Bound
+
+import qs.components
+import qs.components.controls
+import qs.components.effects
+import qs.services
+import qs.config
+import qs.utils
+import Quickshell
+import Quickshell.Widgets
+import Quickshell.Services.Notifications
+import QtQuick
+import QtQuick.Layouts
+
+StyledRect {
+ id: root
+
+ required property string modelData
+ required property Props props
+
+ readonly property list<var> notifs: Notifs.list.filter(notif => notif.appName === modelData).reverse()
+ readonly property string image: notifs.find(n => n.image.length > 0)?.image ?? ""
+ readonly property string appIcon: notifs.find(n => n.appIcon.length > 0)?.appIcon ?? ""
+ readonly property string urgency: notifs.some(n => n.urgency === NotificationUrgency.Critical) ? "critical" : notifs.some(n => n.urgency === NotificationUrgency.Normal) ? "normal" : "low"
+
+ readonly property bool expanded: props.expandedNotifs.includes(modelData)
+
+ anchors.left: parent?.left
+ anchors.right: parent?.right
+ implicitHeight: content.implicitHeight + Appearance.padding.normal * 2
+
+ clip: true
+ radius: Appearance.rounding.normal
+ color: root.urgency === "critical" ? Colours.palette.m3secondaryContainer : Colours.layer(Colours.palette.m3surfaceContainer, 2)
+
+ RowLayout {
+ id: content
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.margins: Appearance.padding.normal
+
+ spacing: Appearance.spacing.normal
+
+ Item {
+ Layout.alignment: Qt.AlignLeft | Qt.AlignTop
+ implicitWidth: Config.notifs.sizes.image
+ implicitHeight: Config.notifs.sizes.image
+
+ Component {
+ id: imageComp
+
+ Image {
+ source: Qt.resolvedUrl(root.image)
+ fillMode: Image.PreserveAspectCrop
+ cache: false
+ asynchronous: true
+ width: Config.notifs.sizes.image
+ height: Config.notifs.sizes.image
+ }
+ }
+
+ Component {
+ id: appIconComp
+
+ ColouredIcon {
+ implicitSize: Math.round(Config.notifs.sizes.image * 0.6)
+ source: Quickshell.iconPath(root.appIcon)
+ colour: root.urgency === "critical" ? Colours.palette.m3onError : root.urgency === "low" ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer
+ layer.enabled: root.appIcon.endsWith("symbolic")
+ }
+ }
+
+ Component {
+ id: materialIconComp
+
+ MaterialIcon {
+ text: Icons.getNotifIcon(root.notifs[0]?.summary, root.urgency)
+ color: root.urgency === "critical" ? Colours.palette.m3onError : root.urgency === "low" ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer
+ font.pointSize: Appearance.font.size.large
+ }
+ }
+
+ ClippingRectangle {
+ anchors.fill: parent
+ color: root.urgency === "critical" ? Colours.palette.m3error : root.urgency === "low" ? Colours.layer(Colours.palette.m3surfaceContainerHigh, 3) : Colours.palette.m3secondaryContainer
+ radius: Appearance.rounding.full
+
+ Loader {
+ anchors.centerIn: parent
+ asynchronous: true
+ sourceComponent: root.image ? imageComp : root.appIcon ? appIconComp : materialIconComp
+ }
+ }
+
+ Loader {
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ asynchronous: true
+ active: root.appIcon && root.image
+
+ sourceComponent: StyledRect {
+ implicitWidth: Config.notifs.sizes.badge
+ implicitHeight: Config.notifs.sizes.badge
+
+ color: root.urgency === "critical" ? Colours.palette.m3error : root.urgency === "low" ? Colours.palette.m3surfaceContainerHigh : Colours.palette.m3secondaryContainer
+ radius: Appearance.rounding.full
+
+ ColouredIcon {
+ anchors.centerIn: parent
+ implicitSize: Math.round(Config.notifs.sizes.badge * 0.6)
+ source: Quickshell.iconPath(root.appIcon)
+ colour: root.urgency === "critical" ? Colours.palette.m3onError : root.urgency === "low" ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer
+ layer.enabled: root.appIcon.endsWith("symbolic")
+ }
+ }
+ }
+ }
+
+ ColumnLayout {
+ Layout.topMargin: -Appearance.padding.small
+ Layout.bottomMargin: -Appearance.padding.small / 2
+ Layout.fillWidth: true
+ spacing: Math.round(Appearance.spacing.small / 2)
+
+ RowLayout {
+ Layout.bottomMargin: -parent.spacing
+ Layout.fillWidth: true
+ spacing: Appearance.spacing.smaller
+
+ StyledText {
+ Layout.fillWidth: true
+ text: root.modelData
+ color: Colours.palette.m3onSurfaceVariant
+ font.pointSize: Appearance.font.size.small
+ elide: Text.ElideRight
+ }
+
+ StyledText {
+ animate: true
+ text: root.notifs[0]?.timeStr ?? ""
+ color: Colours.palette.m3outline
+ font.pointSize: Appearance.font.size.small
+ }
+
+ StyledRect {
+ implicitWidth: expandBtn.implicitWidth + Appearance.padding.smaller * 2
+ implicitHeight: groupCount.implicitHeight + Appearance.padding.small
+
+ color: root.urgency === "critical" ? Colours.palette.m3error : Colours.layer(Colours.palette.m3surfaceContainerHigh, 3)
+ radius: Appearance.rounding.full
+
+ StateLayer {
+ color: root.urgency === "critical" ? Colours.palette.m3onError : Colours.palette.m3onSurface
+
+ function onClicked(): void {
+ if (root.expanded)
+ root.props.expandedNotifs.splice(root.props.expandedNotifs.indexOf(root.modelData), 1);
+ else
+ root.props.expandedNotifs.push(root.modelData);
+ }
+ }
+
+ RowLayout {
+ id: expandBtn
+
+ anchors.centerIn: parent
+ spacing: Appearance.spacing.small / 2
+
+ StyledText {
+ id: groupCount
+
+ Layout.leftMargin: Appearance.padding.small / 2
+ animate: true
+ text: root.notifs.length
+ color: root.urgency === "critical" ? Colours.palette.m3onError : Colours.palette.m3onSurface
+ font.pointSize: Appearance.font.size.small
+ }
+
+ MaterialIcon {
+ Layout.rightMargin: -Appearance.padding.small / 2
+ animate: true
+ text: root.expanded ? "expand_less" : "expand_more"
+ color: root.urgency === "critical" ? Colours.palette.m3onError : Colours.palette.m3onSurface
+ }
+ }
+ }
+ }
+
+ Repeater {
+ id: notifList
+
+ model: ScriptModel {
+ values: root.expanded ? root.notifs : root.notifs.slice(0, Config.notifs.groupPreviewNum)
+ }
+
+ Layout.fillWidth: true
+
+ Notif {
+ id: notif
+
+ props: root.props
+ expanded: root.expanded
+ }
+ }
+ }
+ }
+
+ // Behavior on implicitHeight {
+ // Anim {
+ // duration: Appearance.anim.durations.expressiveDefaultSpatial
+ // easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
+ // }
+ // }
+}
diff --git a/modules/sidebar/Props.qml b/modules/sidebar/Props.qml
new file mode 100644
index 0000000..4613942
--- /dev/null
+++ b/modules/sidebar/Props.qml
@@ -0,0 +1,7 @@
+import Quickshell
+
+PersistentProperties {
+ property list<string> expandedNotifs: []
+
+ reloadableId: "sidebar"
+}
diff --git a/modules/sidebar/Wrapper.qml b/modules/sidebar/Wrapper.qml
index ddbb39c..9303c6b 100644
--- a/modules/sidebar/Wrapper.qml
+++ b/modules/sidebar/Wrapper.qml
@@ -9,10 +9,10 @@ Item {
required property var visibilities
required property var panels
+ readonly property Props props: Props {}
visible: width > 0
implicitWidth: 0
- implicitHeight: 0
states: State {
name: "visible"
@@ -51,15 +51,17 @@ Item {
id: content
anchors.top: parent.top
+ anchors.bottom: parent.bottom
anchors.left: parent.left
- anchors.right: parent.right
anchors.margins: Appearance.padding.large
+ anchors.bottomMargin: 0
- visible: false
active: true
Component.onCompleted: active = Qt.binding(() => (root.visibilities.sidebar && Config.sidebar.enabled) || root.visible)
sourceComponent: Content {
+ implicitWidth: Config.sidebar.sizes.width - Appearance.padding.large * 2
+ props: root.props
visibilities: root.visibilities
}
}