diff options
Diffstat (limited to 'modules')
| -rw-r--r-- | modules/lock/NotifGroup.qml | 2 | ||||
| -rw-r--r-- | modules/sidebar/Content.qml | 26 | ||||
| -rw-r--r-- | modules/sidebar/Notif.qml | 166 | ||||
| -rw-r--r-- | modules/sidebar/NotifDock.qml | 208 | ||||
| -rw-r--r-- | modules/sidebar/NotifGroup.qml | 216 | ||||
| -rw-r--r-- | modules/sidebar/Props.qml | 7 | ||||
| -rw-r--r-- | modules/sidebar/Wrapper.qml | 8 |
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 } } |