notification changes

This commit is contained in:
Zacharias-Brohn
2026-02-04 14:11:30 +01:00
parent 29606e363a
commit 76e008007e
57 changed files with 4537 additions and 202 deletions
+54
View File
@@ -0,0 +1,54 @@
import qs.Components
import qs.Config
import qs.Modules as Modules
import QtQuick
import QtQuick.Shapes
ShapePath {
id: root
required property Wrapper wrapper
required property var sidebar
readonly property real rounding: 8
readonly property bool flatten: wrapper.height < rounding * 2
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
strokeWidth: -1
fillColor: DynamicColors.palette.m3surface
PathLine {
relativeX: -(root.wrapper.width + root.rounding)
relativeY: 0
}
PathArc {
relativeX: root.rounding
relativeY: root.roundingY
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
}
PathLine {
relativeX: 0
relativeY: root.wrapper.height - root.roundingY * 2
}
PathArc {
relativeX: root.sidebar.notifsRoundingX
relativeY: root.roundingY
radiusX: root.sidebar.notifsRoundingX
radiusY: Math.min(root.rounding, root.wrapper.height)
direction: PathArc.Counterclockwise
}
PathLine {
relativeX: root.wrapper.height > 0 ? root.wrapper.width - root.rounding - root.sidebar.notifsRoundingX : root.wrapper.width
relativeY: 0
}
PathArc {
relativeX: root.rounding
relativeY: root.rounding
radiusX: root.rounding
radiusY: root.rounding
}
Behavior on fillColor {
Modules.CAnim {}
}
}
+203
View File
@@ -0,0 +1,203 @@
import qs.Components
import qs.Config
import qs.Daemons
import Quickshell
import Quickshell.Widgets
import QtQuick
Item {
id: root
required property PersistentProperties visibilities
required property Item panels
readonly property int padding: 8
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
implicitWidth: Config.notifs.sizes.width + padding * 2
implicitHeight: {
const count = list.count;
if (count === 0)
return 0;
let height = (count - 1) * 8;
for (let i = 0; i < count; i++)
height += list.itemAtIndex(i)?.nonAnimHeight ?? 0;
if (visibilities && panels) {
if (visibilities.osd) {
const h = panels.osd.y - 8 * 2 - padding * 2;
if (height > h)
height = h;
}
if (visibilities.session) {
const h = panels.session.y - 8 * 2 - padding * 2;
if (height > h)
height = h;
}
}
return Math.min((QsWindow.window?.screen?.height ?? 0) - 1 * 2, height + padding * 2);
}
ClippingWrapperRectangle {
anchors.fill: parent
anchors.margins: root.padding
color: "transparent"
radius: 8
CustomListView {
id: list
model: ScriptModel {
values: NotifServer.popups.filter(n => !n.closed)
}
anchors.fill: parent
orientation: Qt.Vertical
spacing: 0
cacheBuffer: QsWindow.window?.screen.height ?? 0
delegate: Item {
id: wrapper
required property NotifServer.Notif modelData
required property int index
readonly property alias nonAnimHeight: notif.nonAnimHeight
property int idx
onIndexChanged: {
if (index !== -1)
idx = index;
}
implicitWidth: notif.implicitWidth
implicitHeight: notif.implicitHeight + (idx === 0 ? 0 : 8)
ListView.onRemove: removeAnim.start()
SequentialAnimation {
id: removeAnim
PropertyAction {
target: wrapper
property: "ListView.delayRemove"
value: true
}
PropertyAction {
target: wrapper
property: "enabled"
value: false
}
PropertyAction {
target: wrapper
property: "implicitHeight"
value: 0
}
PropertyAction {
target: wrapper
property: "z"
value: 1
}
Anim {
target: notif
property: "x"
to: (notif.x >= 0 ? Config.notifs.sizes.width : -Config.notifs.sizes.width) * 2
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
}
PropertyAction {
target: wrapper
property: "ListView.delayRemove"
value: false
}
}
ClippingRectangle {
anchors.top: parent.top
anchors.topMargin: wrapper.idx === 0 ? 0 : 8
color: "transparent"
radius: notif.radius
implicitWidth: notif.implicitWidth
implicitHeight: notif.implicitHeight
Notification {
id: notif
modelData: wrapper.modelData
}
}
}
move: Transition {
Anim {
property: "y"
}
}
displaced: Transition {
Anim {
property: "y"
}
}
ExtraIndicator {
anchors.top: parent.top
extra: {
const count = list.count;
if (count === 0)
return 0;
const scrollY = list.contentY;
let height = 0;
for (let i = 0; i < count; i++) {
height += (list.itemAtIndex(i)?.nonAnimHeight ?? 0) + 8;
if (height - 8 >= scrollY)
return i;
}
return count;
}
}
ExtraIndicator {
anchors.bottom: parent.bottom
extra: {
const count = list.count;
if (count === 0)
return 0;
const scrollY = list.contentHeight - (list.contentY + list.height);
let height = 0;
for (let i = count - 1; i >= 0; i--) {
height += (list.itemAtIndex(i)?.nonAnimHeight ?? 0) + 8;
if (height - 8 >= scrollY)
return count - i - 1;
}
return 0;
}
}
}
}
Behavior on implicitHeight {
Anim {}
}
component Anim: NumberAnimation {
easing.type: Easing.BezierSpline
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
}
}
+478
View File
@@ -0,0 +1,478 @@
pragma ComponentBehavior: Bound
import qs.Components
import qs.Config
import qs.Modules
import qs.Daemons
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.Notifications
import QtQuick
import QtQuick.Layouts
CustomRect {
id: root
required property NotifServer.Notif modelData
readonly property bool hasImage: modelData.image.length > 0
readonly property bool hasAppIcon: modelData.appIcon.length > 0
readonly property int nonAnimHeight: summary.implicitHeight + (root.expanded ? appName.height + body.height + actions.height + actions.anchors.topMargin : bodyPreview.height) + inner.anchors.margins * 2
property bool expanded: Config.notifs.openExpanded
color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3secondaryContainer : DynamicColors.tPalette.m3surfaceContainer
radius: Appearance.rounding.normal
implicitWidth: Config.notifs.sizes.width
implicitHeight: inner.implicitHeight
x: Config.notifs.sizes.width
Component.onCompleted: {
x = 0;
modelData.lock(this);
}
Component.onDestruction: modelData.unlock(this)
Behavior on x {
Anim {
easing.bezierCurve: MaterialEasing.expressiveEffects
}
}
MouseArea {
property int startY
anchors.fill: parent
hoverEnabled: true
cursorShape: root.expanded && body.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
preventStealing: true
onEntered: root.modelData.timer.stop()
onExited: {
if (!pressed)
root.modelData.timer.start();
}
drag.target: parent
drag.axis: Drag.XAxis
onPressed: event => {
root.modelData.timer.stop();
startY = event.y;
if (event.button === Qt.MiddleButton)
root.modelData.close();
}
onReleased: event => {
if (!containsMouse)
root.modelData.timer.start();
if (Math.abs(root.x) < Config.notifs.sizes.width * Config.notifs.clearThreshold)
root.x = 0;
else
root.modelData.popup = false;
}
onPositionChanged: event => {
if (pressed) {
const diffY = event.y - startY;
if (Math.abs(diffY) > Config.notifs.expandThreshold)
root.expanded = diffY > 0;
}
}
onClicked: event => {
if (!Config.notifs.actionOnClick || event.button !== Qt.LeftButton)
return;
const actions = root.modelData.actions;
if (actions?.length === 1)
actions[0].invoke();
}
Item {
id: inner
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 8
implicitHeight: root.nonAnimHeight
Behavior on implicitHeight {
Anim {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
}
}
Loader {
id: image
active: root.hasImage
anchors.left: parent.left
anchors.top: parent.top
width: Config.notifs.sizes.image
height: Config.notifs.sizes.image
visible: root.hasImage || root.hasAppIcon
sourceComponent: ClippingRectangle {
radius: 1000
implicitWidth: Config.notifs.sizes.image
implicitHeight: Config.notifs.sizes.image
Image {
anchors.fill: parent
source: Qt.resolvedUrl(root.modelData.image)
fillMode: Image.PreserveAspectCrop
cache: false
asynchronous: true
}
}
}
Loader {
id: appIcon
active: root.hasAppIcon || !root.hasImage
anchors.horizontalCenter: root.hasImage ? undefined : image.horizontalCenter
anchors.verticalCenter: root.hasImage ? undefined : image.verticalCenter
anchors.right: root.hasImage ? image.right : undefined
anchors.bottom: root.hasImage ? image.bottom : undefined
sourceComponent: CustomRect {
radius: 1000
color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 2) : DynamicColors.palette.m3secondaryContainer
implicitWidth: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image
implicitHeight: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image
Loader {
id: icon
active: root.hasAppIcon
anchors.centerIn: parent
width: Math.round(parent.width * 0.6)
height: Math.round(parent.width * 0.6)
sourceComponent: CustomIcon {
anchors.fill: parent
source: Quickshell.iconPath(root.modelData.appIcon)
layer.enabled: root.modelData.appIcon.endsWith("symbolic")
}
}
Loader {
active: !root.hasAppIcon
anchors.centerIn: parent
anchors.horizontalCenterOffset: -18 * 0.02
anchors.verticalCenterOffset: 18 * 0.02
sourceComponent: MaterialIcon {
text: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency)
color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSecondaryContainer
font.pointSize: 18
}
}
}
}
CustomText {
id: appName
anchors.top: parent.top
anchors.left: image.right
anchors.leftMargin: 10
animate: true
text: appNameMetrics.elidedText
maximumLineCount: 1
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: 10
opacity: root.expanded ? 1 : 0
Behavior on opacity {
Anim {}
}
}
TextMetrics {
id: appNameMetrics
text: root.modelData.appName
font.family: appName.font.family
font.pointSize: appName.font.pointSize
elide: Text.ElideRight
elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - 7 * 3
}
CustomText {
id: summary
anchors.top: parent.top
anchors.left: image.right
anchors.leftMargin: 10
animate: true
text: summaryMetrics.elidedText
maximumLineCount: 1
height: implicitHeight
states: State {
name: "expanded"
when: root.expanded
PropertyChanges {
summary.maximumLineCount: undefined
}
AnchorChanges {
target: summary
anchors.top: appName.bottom
}
}
transitions: Transition {
PropertyAction {
target: summary
property: "maximumLineCount"
}
AnchorAnimation {
easing.type: Easing.BezierSpline
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
}
}
Behavior on height {
Anim {}
}
}
TextMetrics {
id: summaryMetrics
text: root.modelData.summary
font.family: summary.font.family
font.pointSize: summary.font.pointSize
elide: Text.ElideRight
elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - 7 * 3
}
CustomText {
id: timeSep
anchors.top: parent.top
anchors.left: summary.right
anchors.leftMargin: 7
text: "•"
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: 10
states: State {
name: "expanded"
when: root.expanded
AnchorChanges {
target: timeSep
anchors.left: appName.right
}
}
transitions: Transition {
AnchorAnimation {
easing.type: Easing.BezierSpline
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
}
}
}
CustomText {
id: time
anchors.top: parent.top
anchors.left: timeSep.right
anchors.leftMargin: 7
animate: true
horizontalAlignment: Text.AlignLeft
text: root.modelData.timeStr
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: 10
}
Item {
id: expandBtn
anchors.right: parent.right
anchors.top: parent.top
implicitWidth: expandIcon.height
implicitHeight: expandIcon.height
StateLayer {
radius: 1000
color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurface
function onClicked() {
root.expanded = !root.expanded;
}
}
MaterialIcon {
id: expandIcon
anchors.centerIn: parent
animate: true
text: root.expanded ? "expand_less" : "expand_more"
font.pointSize: 13
}
}
CustomText {
id: bodyPreview
anchors.left: summary.left
anchors.right: expandBtn.left
anchors.top: summary.bottom
anchors.rightMargin: 7
animate: true
textFormat: Text.MarkdownText
text: bodyPreviewMetrics.elidedText
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: 10
opacity: root.expanded ? 0 : 1
Behavior on opacity {
Anim {}
}
}
TextMetrics {
id: bodyPreviewMetrics
text: root.modelData.body
font.family: bodyPreview.font.family
font.pointSize: bodyPreview.font.pointSize
elide: Text.ElideRight
elideWidth: bodyPreview.width
}
CustomText {
id: body
anchors.left: summary.left
anchors.right: expandBtn.left
anchors.top: summary.bottom
anchors.rightMargin: 7
animate: true
textFormat: Text.MarkdownText
text: root.modelData.body
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: 10
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
height: text ? implicitHeight : 0
onLinkActivated: link => {
if (!root.expanded)
return;
Quickshell.execDetached(["app2unit", "-O", "--", link]);
root.modelData.popup = false;
}
opacity: root.expanded ? 1 : 0
Behavior on opacity {
Anim {}
}
}
RowLayout {
id: actions
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: body.bottom
anchors.topMargin: 7
spacing: 10
opacity: root.expanded ? 1 : 0
Behavior on opacity {
Anim {}
}
Action {
modelData: QtObject {
readonly property string text: qsTr("Close")
function invoke(): void {
root.modelData.close();
}
}
}
Repeater {
model: root.modelData.actions
delegate: Component {
Action {}
}
}
}
}
}
component Action: CustomRect {
id: action
required property var modelData
radius: 1000
color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3secondary : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
Layout.preferredWidth: actionText.width + 8 * 2
Layout.preferredHeight: actionText.height + 4 * 2
implicitWidth: actionText.width + 8 * 2
implicitHeight: actionText.height + 4 * 2
StateLayer {
radius: 1000
color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onSecondary : DynamicColors.palette.m3onSurface
function onClicked(): void {
action.modelData.invoke();
}
}
CustomText {
id: actionText
anchors.centerIn: parent
text: actionTextMetrics.elidedText
color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onSecondary : DynamicColors.palette.m3onSurfaceVariant
font.pointSize: 10
}
TextMetrics {
id: actionTextMetrics
text: action.modelData.text
font.family: actionText.font.family
font.pointSize: actionText.font.pointSize
elide: Text.ElideRight
elideWidth: {
const numActions = root.modelData.actions.length + 1;
return (inner.width - actions.spacing * (numActions - 1)) / numActions - 8 * 2;
}
}
}
}
@@ -0,0 +1,52 @@
import qs.Components
import qs.Config
import qs.Modules as Modules
import QtQuick
import QtQuick.Shapes
ShapePath {
id: root
required property Wrapper wrapper
required property var panels
readonly property real rounding: 8
readonly property real notifsWidthDiff: panels.notifications.width - wrapper.width
readonly property real notifsRoundingX: panels.notifications.height > 0 && notifsWidthDiff < rounding * 2 ? notifsWidthDiff / 2 : rounding
readonly property real utilsWidthDiff: panels.utilities.width - wrapper.width
readonly property real utilsRoundingX: utilsWidthDiff < rounding * 2 ? utilsWidthDiff / 2 : rounding
strokeWidth: -1
fillColor: DynamicColors.palette.m3surface
PathLine {
relativeX: -root.wrapper.width - root.notifsRoundingX
relativeY: 0
}
PathArc {
relativeX: root.notifsRoundingX
relativeY: root.rounding
radiusX: root.notifsRoundingX
radiusY: root.rounding
}
PathLine {
relativeX: 0
relativeY: root.wrapper.height - root.rounding * 2
}
PathArc {
relativeX: -root.utilsRoundingX
relativeY: root.rounding
radiusX: root.utilsRoundingX
radiusY: root.rounding
}
PathLine {
relativeX: root.wrapper.width + root.utilsRoundingX
relativeY: 0
}
Behavior on fillColor {
Modules.CAnim {}
}
}
+39
View File
@@ -0,0 +1,39 @@
import qs.Components
import qs.Config
import QtQuick
import QtQuick.Layouts
Item {
id: root
required property Props props
required property var visibilities
ColumnLayout {
id: layout
anchors.fill: parent
spacing: 8
CustomRect {
Layout.fillWidth: true
Layout.fillHeight: true
radius: 8
color: DynamicColors.tPalette.m3surfaceContainerLow
NotifDock {
props: root.props
visibilities: root.visibilities
}
}
CustomRect {
Layout.topMargin: 8 - layout.spacing
Layout.fillWidth: true
implicitHeight: 1
color: DynamicColors.tPalette.m3outlineVariant
}
}
}
+165
View File
@@ -0,0 +1,165 @@
pragma ComponentBehavior: Bound
import qs.Components
import qs.Config
import qs.Daemons
import qs.Modules
import Quickshell
import QtQuick
import QtQuick.Layouts
CustomRect {
id: root
required property NotifServer.Notif modelData
required property Props props
required property bool expanded
required property var visibilities
readonly property CustomText body: expandedContent.item?.body ?? null
readonly property real nonAnimHeight: expanded ? summary.implicitHeight + expandedContent.implicitHeight + expandedContent.anchors.topMargin + 10 * 2 : summaryHeightMetrics.height
implicitHeight: nonAnimHeight
radius: 6
color: {
const c = root.modelData.urgency === "critical" ? DynamicColors.palette.m3secondaryContainer : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2);
return expanded ? c : Qt.alpha(c, 0);
}
states: State {
name: "expanded"
when: root.expanded
PropertyChanges {
summary.anchors.margins: 10
dummySummary.anchors.margins: 10
compactBody.anchors.margins: 10
timeStr.anchors.margins: 10
expandedContent.anchors.margins: 10
summary.width: root.width - 10 * 2 - timeStr.implicitWidth - 7
summary.maximumLineCount: Number.MAX_SAFE_INTEGER
}
}
transitions: Transition {
Anim {
properties: "margins,width,maximumLineCount"
}
}
TextMetrics {
id: summaryHeightMetrics
font: summary.font
text: " " // Use this height to prevent weird characters from changing the line height
}
CustomText {
id: summary
anchors.top: parent.top
anchors.left: parent.left
width: parent.width
text: root.modelData.summary
color: root.modelData.urgency === "critical" ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurface
elide: Text.ElideRight
wrapMode: Text.WordWrap
maximumLineCount: 1
}
CustomText {
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: 7
sourceComponent: CustomText {
text: root.modelData.body.replace(/\n/g, " ")
color: root.modelData.urgency === "critical" ? DynamicColors.palette.m3secondary : DynamicColors.palette.m3outline
elide: Text.ElideRight
}
}
WrappedLoader {
id: timeStr
shouldBeActive: root.expanded
anchors.top: parent.top
anchors.right: parent.right
sourceComponent: CustomText {
animate: true
text: root.modelData.timeStr
color: DynamicColors.palette.m3outline
font.pointSize: 11
}
}
WrappedLoader {
id: expandedContent
shouldBeActive: root.expanded
anchors.top: summary.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 7 / 2
sourceComponent: ColumnLayout {
readonly property alias body: body
spacing: 10
CustomText {
id: body
Layout.fillWidth: true
textFormat: Text.MarkdownText
text: root.modelData.body.replace(/(.)\n(?!\n)/g, "$1\n\n") || qsTr("No body here! :/")
color: root.modelData.urgency === "critical" ? DynamicColors.palette.m3secondary : DynamicColors.palette.m3outline
wrapMode: Text.WordWrap
onLinkActivated: link => {
Quickshell.execDetached(["app2unit", "-O", "--", link]);
root.visibilities.sidebar = false;
}
}
NotifActionList {
notif: root.modelData
}
}
}
Behavior on implicitHeight {
Anim {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
}
}
component WrappedLoader: Loader {
required property bool shouldBeActive
opacity: shouldBeActive ? 1 : 0
active: opacity > 0
Behavior on opacity {
Anim {}
}
}
}
@@ -0,0 +1,199 @@
pragma ComponentBehavior: Bound
import qs.Components
import qs.Config
import qs.Modules
import qs.Daemons
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
Item {
id: root
required property NotifServer.Notif notif
Layout.fillWidth: true
implicitHeight: flickable.contentHeight
layer.enabled: true
layer.smooth: true
layer.effect: OpacityMask {
maskSource: gradientMask
}
Item {
id: gradientMask
anchors.fill: parent
layer.enabled: true
visible: false
Rectangle {
anchors.fill: parent
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop {
position: 0
color: Qt.rgba(0, 0, 0, 0)
}
GradientStop {
position: 0.1
color: Qt.rgba(0, 0, 0, 1)
}
GradientStop {
position: 0.9
color: Qt.rgba(0, 0, 0, 1)
}
GradientStop {
position: 1
color: Qt.rgba(0, 0, 0, 0)
}
}
}
Rectangle {
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
implicitWidth: parent.width / 2
opacity: flickable.contentX > 0 ? 0 : 1
Behavior on opacity {
Anim {}
}
}
Rectangle {
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
implicitWidth: parent.width / 2
opacity: flickable.contentX < flickable.contentWidth - parent.width ? 0 : 1
Behavior on opacity {
Anim {}
}
}
}
CustomFlickable {
id: flickable
anchors.fill: parent
contentWidth: Math.max(width, actionList.implicitWidth)
contentHeight: actionList.implicitHeight
RowLayout {
id: actionList
anchors.fill: parent
spacing: 7
Repeater {
model: [
{
isClose: true
},
...root.notif.actions,
{
isCopy: true
}
]
CustomRect {
id: action
required property var modelData
Layout.fillWidth: true
Layout.fillHeight: true
implicitWidth: actionInner.implicitWidth + 10 * 2
implicitHeight: actionInner.implicitHeight + 10 * 2
Layout.preferredWidth: implicitWidth + (actionStateLayer.pressed ? 18 : 0)
radius: actionStateLayer.pressed ? 6 / 2 : 6
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 4)
Timer {
id: copyTimer
interval: 3000
onTriggered: actionInner.item.text = "content_copy"
}
StateLayer {
id: actionStateLayer
function onClicked(): void {
if (action.modelData.isClose) {
root.notif.close();
} else if (action.modelData.isCopy) {
Quickshell.clipboardText = root.notif.body;
actionInner.item.text = "inventory";
copyTimer.start();
} else if (action.modelData.invoke) {
action.modelData.invoke();
} else if (!root.notif.resident) {
root.notif.close();
}
}
}
Loader {
id: actionInner
anchors.centerIn: parent
sourceComponent: action.modelData.isClose || action.modelData.isCopy ? iconBtn : root.notif.hasActionIcons ? iconComp : textComp
}
Component {
id: iconBtn
MaterialIcon {
animate: action.modelData.isCopy ?? false
text: action.modelData.isCopy ? "content_copy" : "close"
color: DynamicColors.palette.m3onSurfaceVariant
}
}
Component {
id: iconComp
IconImage {
source: Quickshell.iconPath(action.modelData.identifier)
}
}
Component {
id: textComp
CustomText {
text: action.modelData.text
color: DynamicColors.palette.m3onSurfaceVariant
}
}
Behavior on Layout.preferredWidth {
Anim {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
}
}
Behavior on radius {
Anim {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
}
}
}
}
}
}
}
+199
View File
@@ -0,0 +1,199 @@
pragma ComponentBehavior: Bound
import qs.Components
import qs.Config
import qs.Modules
import qs.Daemons
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
Item {
id: root
required property Props props
required property var visibilities
readonly property int notifCount: NotifServer.list.reduce((acc, n) => n.closed ? acc : acc + 1, 0)
anchors.fill: parent
anchors.margins: 8
Component.onCompleted: NotifServer.list.forEach(n => n.popup = false)
Item {
id: title
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 4
implicitHeight: Math.max(count.implicitHeight, titleText.implicitHeight)
CustomText {
id: count
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: root.notifCount > 0 ? 0 : -width - titleText.anchors.leftMargin
opacity: root.notifCount > 0 ? 1 : 0
text: root.notifCount
color: DynamicColors.palette.m3outline
font.pointSize: 13
font.family: "CaskaydiaCove NF"
font.weight: 500
Behavior on anchors.leftMargin {
Anim {}
}
Behavior on opacity {
Anim {}
}
}
CustomText {
id: titleText
anchors.verticalCenter: parent.verticalCenter
anchors.left: count.right
anchors.right: parent.right
anchors.leftMargin: 7
text: root.notifCount > 0 ? qsTr("notification%1").arg(root.notifCount === 1 ? "" : "s") : qsTr("Notifications")
color: DynamicColors.palette.m3outline
font.pointSize: 13
font.family: "CaskaydiaCove NF"
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: 10
radius: 6
color: "transparent"
Loader {
anchors.centerIn: parent
active: opacity > 0
opacity: root.notifCount > 0 ? 0 : 1
sourceComponent: ColumnLayout {
spacing: 20
Image {
asynchronous: true
source: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/dino.png`)
fillMode: Image.PreserveAspectFit
sourceSize.width: clipRect.width * 0.8
}
CustomText {
Layout.alignment: Qt.AlignHCenter
text: qsTr("No Notifications")
color: DynamicColors.palette.m3outlineVariant
font.pointSize: 18
font.family: "CaskaydiaCove NF"
font.weight: 500
}
}
Behavior on opacity {
Anim {
duration: MaterialEasing.expressiveEffectsTime
}
}
}
CustomFlickable {
id: view
anchors.fill: parent
flickableDirection: Flickable.VerticalFlick
contentWidth: width
contentHeight: notifList.implicitHeight
CustomScrollBar.vertical: CustomScrollBar {
flickable: view
}
NotifDockList {
id: notifList
props: root.props
visibilities: root.visibilities
container: view
}
}
}
Timer {
id: clearTimer
repeat: true
interval: 50
onTriggered: {
let next = null;
for (let i = 0; i < notifList.repeater.count; i++) {
next = notifList.repeater.itemAt(i);
if (!next?.closed)
break;
}
if (next)
next.closeAll();
else
stop();
}
}
Loader {
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: 8
scale: root.notifCount > 0 ? 1 : 0.5
opacity: root.notifCount > 0 ? 1 : 0
active: opacity > 0
sourceComponent: IconButton {
id: clearBtn
icon: "clear_all"
radius: 8
padding: 8
font.pointSize: Math.round(18 * 1.2)
onClicked: clearTimer.start()
Elevation {
anchors.fill: parent
radius: parent.radius
z: -1
level: clearBtn.stateLayer.containsMouse ? 4 : 3
}
}
Behavior on scale {
Anim {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
}
}
Behavior on opacity {
Anim {
duration: MaterialEasing.expressiveEffectsTime
}
}
}
}
@@ -0,0 +1,169 @@
pragma ComponentBehavior: Bound
import qs.Components
import qs.Config
import qs.Modules
import qs.Daemons
import Quickshell
import QtQuick
Item {
id: root
required property Props props
required property Flickable container
required property var visibilities
readonly property alias repeater: repeater
readonly property int spacing: 8
property bool flag
anchors.left: parent.left
anchors.right: parent.right
implicitHeight: {
const item = repeater.itemAt(repeater.count - 1);
return item ? item.y + item.implicitHeight : 0;
}
Repeater {
id: repeater
model: ScriptModel {
values: {
const map = new Map();
for (const n of NotifServer.notClosed)
map.set(n.appName, null);
for (const n of NotifServer.list)
map.set(n.appName, null);
console.log(map.keys())
return [...map.keys()];
}
onValuesChanged: root.flagChanged()
}
MouseArea {
id: notif
required property int index
required property string modelData
readonly property bool closed: notifInner.notifCount === 0
readonly property alias nonAnimHeight: notifInner.nonAnimHeight
property int startY
function closeAll(): void {
for (const n of NotifServer.notClosed.filter(n => n.appName === modelData))
n.close();
}
y: {
root.flag; // Force update
let y = 0;
for (let i = 0; i < index; i++) {
const item = repeater.itemAt(i);
if (!item.closed)
y += item.nonAnimHeight + root.spacing;
}
return y;
}
containmentMask: QtObject {
function contains(p: point): bool {
if (!root.container.contains(notif.mapToItem(root.container, p)))
return false;
return notifInner.contains(p);
}
}
implicitWidth: root.width
implicitHeight: notifInner.implicitHeight
hoverEnabled: true
cursorShape: pressed ? Qt.ClosedHandCursor : undefined
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
preventStealing: true
enabled: !closed
drag.target: this
drag.axis: Drag.XAxis
onPressed: event => {
startY = event.y;
if (event.button === Qt.RightButton)
notifInner.toggleExpand(!notifInner.expanded);
else if (event.button === Qt.MiddleButton)
closeAll();
}
onPositionChanged: event => {
if (pressed) {
const diffY = event.y - startY;
if (Math.abs(diffY) > Config.notifs.expandThreshold)
notifInner.toggleExpand(diffY > 0);
}
}
onReleased: event => {
if (Math.abs(x) < width * Config.notifs.clearThreshold)
x = 0;
else
closeAll();
}
ParallelAnimation {
running: true
Anim {
target: notif
property: "opacity"
from: 0
to: 1
}
Anim {
target: notif
property: "scale"
from: 0
to: 1
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
}
}
ParallelAnimation {
running: notif.closed
Anim {
target: notif
property: "opacity"
to: 0
}
Anim {
target: notif
property: "scale"
to: 0.6
}
}
NotifGroup {
id: notifInner
modelData: notif.modelData
props: root.props
container: root.container
visibilities: root.visibilities
}
Behavior on x {
Anim {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
}
}
Behavior on y {
Anim {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
}
}
}
}
}
@@ -0,0 +1,239 @@
pragma ComponentBehavior: Bound
import qs.Components
import qs.Config
import qs.Modules
import qs.Daemons
import qs.Helpers
import Quickshell
import Quickshell.Services.Notifications
import QtQuick
import QtQuick.Layouts
CustomRect {
id: root
required property string modelData
required property Props props
required property Flickable container
required property var visibilities
readonly property list<var> notifs: NotifServer.list.filter(n => n.appName === modelData)
readonly property int notifCount: notifs.reduce((acc, n) => n.closed ? acc : acc + 1, 0)
readonly property string image: notifs.find(n => !n.closed && n.image.length > 0)?.image ?? ""
readonly property string appIcon: notifs.find(n => !n.closed && n.appIcon.length > 0)?.appIcon ?? ""
readonly property int urgency: notifs.some(n => !n.closed && n.urgency === NotificationUrgency.Critical) ? NotificationUrgency.Critical : notifs.some(n => n.urgency === NotificationUrgency.Normal) ? NotificationUrgency.Normal : NotificationUrgency.Low
readonly property int nonAnimHeight: {
const headerHeight = header.implicitHeight + (root.expanded ? Math.round(7 / 2) : 0);
const columnHeight = headerHeight + notifList.nonAnimHeight + column.Layout.topMargin + column.Layout.bottomMargin;
return Math.round(Math.max(Config.notifs.sizes.image, columnHeight) + 10 * 2);
}
readonly property bool expanded: props.expandedNotifs.includes(modelData)
function toggleExpand(expand: bool): void {
if (expand) {
if (!expanded)
props.expandedNotifs.push(modelData);
} else if (expanded) {
props.expandedNotifs.splice(props.expandedNotifs.indexOf(modelData), 1);
}
}
Component.onDestruction: {
if (notifCount === 0 && expanded)
props.expandedNotifs.splice(props.expandedNotifs.indexOf(modelData), 1);
}
anchors.left: parent?.left
anchors.right: parent?.right
implicitHeight: content.implicitHeight + 10 * 2
clip: true
radius: 8
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainer, 2)
RowLayout {
id: content
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 10
spacing: 10
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
CustomIcon {
implicitSize: Math.round(Config.notifs.sizes.image * 0.6)
source: Quickshell.iconPath(root.appIcon)
layer.enabled: root.appIcon.endsWith("symbolic")
}
}
Component {
id: materialIconComp
MaterialIcon {
text: Icons.getNotifIcon(root.notifs[0]?.summary, root.urgency)
color: root.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onError : root.urgency === NotificationUrgency.Low ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSecondaryContainer
font.pointSize: 18
}
}
CustomClippingRect {
anchors.fill: parent
color: root.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3error : root.urgency === NotificationUrgency.Low ? DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 3) : DynamicColors.palette.m3secondaryContainer
radius: 1000
Loader {
anchors.centerIn: parent
sourceComponent: root.image ? imageComp : root.appIcon ? appIconComp : materialIconComp
}
}
Loader {
anchors.right: parent.right
anchors.bottom: parent.bottom
active: root.appIcon && root.image
sourceComponent: CustomRect {
implicitWidth: Config.notifs.sizes.badge
implicitHeight: Config.notifs.sizes.badge
color: root.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3error : root.urgency === NotificationUrgency.Low ? DynamicColors.palette.m3surfaceContainerHigh : DynamicColors.palette.m3secondaryContainer
radius: 1000
CustomIcon {
anchors.centerIn: parent
implicitSize: Math.round(Config.notifs.sizes.badge * 0.6)
source: Quickshell.iconPath(root.appIcon)
layer.enabled: root.appIcon.endsWith("symbolic")
}
}
}
}
ColumnLayout {
id: column
Layout.topMargin: -10
Layout.bottomMargin: -10 / 2
Layout.fillWidth: true
spacing: 0
RowLayout {
id: header
Layout.bottomMargin: root.expanded ? Math.round(7 / 2) : 0
Layout.fillWidth: true
spacing: 5
CustomText {
Layout.fillWidth: true
text: root.modelData
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: 11
elide: Text.ElideRight
}
CustomText {
animate: true
text: root.notifs.find(n => !n.closed)?.timeStr ?? ""
color: DynamicColors.palette.m3outline
font.pointSize: 11
}
CustomRect {
implicitWidth: expandBtn.implicitWidth + 7 * 2
implicitHeight: groupCount.implicitHeight + 10
color: root.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3error : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 3)
radius: 1000
StateLayer {
color: root.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onError : DynamicColors.palette.m3onSurface
function onClicked(): void {
root.toggleExpand(!root.expanded);
}
}
RowLayout {
id: expandBtn
anchors.centerIn: parent
spacing: 7 / 2
CustomText {
id: groupCount
Layout.leftMargin: 10 / 2
animate: true
text: root.notifCount
color: root.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onError : DynamicColors.palette.m3onSurface
font.pointSize: 11
}
MaterialIcon {
Layout.rightMargin: -10 / 2
text: "expand_more"
color: root.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onError : DynamicColors.palette.m3onSurface
rotation: root.expanded ? 180 : 0
Layout.topMargin: root.expanded ? -Math.floor(7 / 2) : 0
Behavior on rotation {
Anim {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
}
}
Behavior on Layout.topMargin {
Anim {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
}
}
}
}
}
Behavior on Layout.bottomMargin {
Anim {}
}
}
NotifGroupList {
id: notifList
props: root.props
notifs: root.notifs
expanded: root.expanded
container: root.container
visibilities: root.visibilities
onRequestToggleExpand: expand => root.toggleExpand(expand)
}
}
}
}
@@ -0,0 +1,214 @@
pragma ComponentBehavior: Bound
import qs.Components
import qs.Config
import qs.Modules
import qs.Daemons
import Quickshell
import QtQuick
import QtQuick.Layouts
Item {
id: root
required property Props props
required property list<var> notifs
required property bool expanded
required property Flickable container
required property var visibilities
readonly property real nonAnimHeight: {
let h = -root.spacing;
for (let i = 0; i < repeater.count; i++) {
const item = repeater.itemAt(i);
if (!item.modelData.closed && !item.previewHidden)
h += item.nonAnimHeight + root.spacing;
}
return h;
}
readonly property int spacing: Math.round(7 / 2)
property bool showAllNotifs
property bool flag
signal requestToggleExpand(expand: bool)
onExpandedChanged: {
if (expanded) {
clearTimer.stop();
showAllNotifs = true;
} else {
clearTimer.start();
}
}
Layout.fillWidth: true
implicitHeight: nonAnimHeight
Timer {
id: clearTimer
interval: MaterialEasing.standardTime
onTriggered: root.showAllNotifs = false
}
Repeater {
id: repeater
model: ScriptModel {
values: root.showAllNotifs ? root.notifs : root.notifs.slice(0, Config.notifs.groupPreviewNum + 1)
onValuesChanged: root.flagChanged()
}
MouseArea {
id: notif
required property int index
required property NotifServer.Notif modelData
readonly property alias nonAnimHeight: notifInner.nonAnimHeight
readonly property bool previewHidden: {
if (root.expanded)
return false;
let extraHidden = 0;
for (let i = 0; i < index; i++)
if (root.notifs[i].closed)
extraHidden++;
return index >= Config.notifs.groupPreviewNum + extraHidden;
}
property int startY
y: {
root.flag; // Force update
let y = 0;
for (let i = 0; i < index; i++) {
const item = repeater.itemAt(i);
if (!item.modelData.closed && !item.previewHidden)
y += item.nonAnimHeight + root.spacing;
}
return y;
}
containmentMask: QtObject {
function contains(p: point): bool {
if (!root.container.contains(notif.mapToItem(root.container, p)))
return false;
return notifInner.contains(p);
}
}
opacity: previewHidden ? 0 : 1
scale: previewHidden ? 0.7 : 1
implicitWidth: root.width
implicitHeight: notifInner.implicitHeight
hoverEnabled: true
cursorShape: notifInner.body?.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
preventStealing: !root.expanded
enabled: !modelData.closed
drag.target: this
drag.axis: Drag.XAxis
onPressed: event => {
startY = event.y;
if (event.button === Qt.RightButton)
root.requestToggleExpand(!root.expanded);
else if (event.button === Qt.MiddleButton)
modelData.close();
}
onPositionChanged: event => {
if (pressed && !root.expanded) {
const diffY = event.y - startY;
if (Math.abs(diffY) > Config.notifs.expandThreshold)
root.requestToggleExpand(diffY > 0);
}
}
onReleased: event => {
if (Math.abs(x) < width * Config.notifs.clearThreshold)
x = 0;
else
modelData.close();
}
Component.onCompleted: modelData.lock(this)
Component.onDestruction: modelData.unlock(this)
ParallelAnimation {
Component.onCompleted: running = !notif.previewHidden
Anim {
target: notif
property: "opacity"
from: 0
to: 1
}
Anim {
target: notif
property: "scale"
from: 0.7
to: 1
}
}
ParallelAnimation {
running: notif.modelData.closed
onFinished: notif.modelData.unlock(notif)
Anim {
target: notif
property: "opacity"
to: 0
}
Anim {
target: notif
property: "x"
to: notif.x >= 0 ? notif.width : -notif.width
}
}
Notif {
id: notifInner
anchors.fill: parent
modelData: notif.modelData
props: root.props
expanded: root.expanded
visibilities: root.visibilities
}
Behavior on opacity {
Anim {}
}
Behavior on scale {
Anim {}
}
Behavior on x {
Anim {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
}
}
Behavior on y {
Anim {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
}
}
}
}
Behavior on implicitHeight {
Anim {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
}
}
}
+7
View File
@@ -0,0 +1,7 @@
import Quickshell
PersistentProperties {
property list<string> expandedNotifs: []
reloadableId: "sidebar"
}
@@ -0,0 +1,55 @@
import qs.Components
import qs.Config
import qs.Modules as Modules
import QtQuick
import QtQuick.Shapes
ShapePath {
id: root
required property Wrapper wrapper
required property var sidebar
readonly property real rounding: 8
readonly property bool flatten: wrapper.height < rounding * 2
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
strokeWidth: -1
fillColor: DynamicColors.palette.m3surface
PathLine {
relativeX: -(root.wrapper.width + root.rounding)
relativeY: 0
}
PathArc {
relativeX: root.rounding
relativeY: -root.roundingY
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
direction: PathArc.Counterclockwise
}
PathLine {
relativeX: 0
relativeY: -(root.wrapper.height - root.roundingY * 2)
}
PathArc {
relativeX: root.sidebar.utilsRoundingX
relativeY: -root.roundingY
radiusX: root.sidebar.utilsRoundingX
radiusY: Math.min(root.rounding, root.wrapper.height)
}
PathLine {
relativeX: root.wrapper.height > 0 ? root.wrapper.width - root.rounding - root.sidebar.utilsRoundingX : root.wrapper.width
relativeY: 0
}
PathArc {
relativeX: root.rounding
relativeY: -root.rounding
radiusX: root.rounding
radiusY: root.rounding
direction: PathArc.Counterclockwise
}
Behavior on fillColor {
Modules.CAnim {}
}
}
@@ -0,0 +1,57 @@
import qs.Components
import qs.Config
import qs.Modules
import qs.Daemons
import Quickshell
import Quickshell.Bluetooth
import QtQuick
import QtQuick.Layouts
CustomRect {
id: root
required property var visibilities
required property Item popouts
Layout.fillWidth: true
implicitHeight: layout.implicitHeight + 18 * 2
radius: 8
color: DynamicColors.tPalette.m3surfaceContainer
ColumnLayout {
id: layout
anchors.fill: parent
anchors.margins: 18
spacing: 10
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: 7
Toggle {
icon: "notifications_off"
checked: NotifServer.dnd
onClicked: NotifServer.dnd = !NotifServer.dnd
}
}
}
component Toggle: IconButton {
Layout.fillWidth: true
Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? 18 : internalChecked ? 7 : 0)
radius: stateLayer.pressed ? 6 / 2 : internalChecked ? 6 : 8
inactiveColour: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 2)
toggle: true
radiusAnim.duration: MaterialEasing.expressiveEffectsTime
radiusAnim.easing.bezierCurve: MaterialEasing.expressiveEffects
Behavior on Layout.preferredWidth {
Anim {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
}
}
}
}
@@ -0,0 +1,29 @@
import qs.Modules.Notifications.Sidebar.Utils.Cards
import qs.Config
import QtQuick
import QtQuick.Layouts
Item {
id: root
required property var props
required property var visibilities
required property Item popouts
implicitWidth: layout.implicitWidth
implicitHeight: layout.implicitHeight
ColumnLayout {
id: layout
anchors.fill: parent
spacing: 8
IdleInhibit {}
Toggles {
visibilities: root.visibilities
popouts: root.popouts
}
}
}
@@ -0,0 +1,125 @@
import qs.Components
import qs.Config
import qs.Modules as Modules
import qs.Helpers
import QtQuick
import QtQuick.Layouts
CustomRect {
id: root
Layout.fillWidth: true
implicitHeight: layout.implicitHeight + (IdleInhibitor.enabled ? activeChip.implicitHeight + activeChip.anchors.topMargin : 0) + 18 * 2
radius: 8
color: DynamicColors.tPalette.m3surfaceContainer
clip: true
RowLayout {
id: layout
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 18
spacing: 10
CustomRect {
implicitWidth: implicitHeight
implicitHeight: icon.implicitHeight + 7 * 2
radius: 1000
color: IdleInhibitor.enabled ? DynamicColors.palette.m3secondary : DynamicColors.palette.m3secondaryContainer
MaterialIcon {
id: icon
anchors.centerIn: parent
text: "coffee"
color: IdleInhibitor.enabled ? DynamicColors.palette.m3onSecondary : DynamicColors.palette.m3onSecondaryContainer
font.pointSize: 18
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
CustomText {
Layout.fillWidth: true
text: qsTr("Keep Awake")
font.pointSize: 13
elide: Text.ElideRight
}
CustomText {
Layout.fillWidth: true
text: IdleInhibitor.enabled ? qsTr("Preventing sleep mode") : qsTr("Normal power management")
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: 11
elide: Text.ElideRight
}
}
CustomSwitch {
checked: IdleInhibitor.enabled
onToggled: IdleInhibitor.enabled = checked
}
}
Loader {
id: activeChip
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.topMargin: 20
anchors.bottomMargin: IdleInhibitor.enabled ? 18 : -implicitHeight
anchors.leftMargin: 18
opacity: IdleInhibitor.enabled ? 1 : 0
scale: IdleInhibitor.enabled ? 1 : 0.5
Component.onCompleted: active = Qt.binding(() => opacity > 0)
sourceComponent: CustomRect {
implicitWidth: activeText.implicitWidth + 10 * 2
implicitHeight: activeText.implicitHeight + 10 * 2
radius: 1000
color: DynamicColors.palette.m3primary
CustomText {
id: activeText
anchors.centerIn: parent
text: qsTr("Active since %1").arg(Qt.formatTime(IdleInhibitor.enabledSince, Config.services.useTwelveHourClock ? "hh:mm a" : "hh:mm"))
color: DynamicColors.palette.m3onPrimary
font.pointSize: Math.round(11 * 0.9)
}
}
Behavior on anchors.bottomMargin {
Modules.Anim {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
}
}
Behavior on opacity {
Modules.Anim {
duration: MaterialEasing.expressiveEffectsTime
}
}
Behavior on scale {
Modules.Anim {}
}
}
Behavior on implicitHeight {
Modules.Anim {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
}
}
}
@@ -0,0 +1,97 @@
pragma ComponentBehavior: Bound
import qs.Components
import qs.Config
import qs.Modules as Modules
import Quickshell
import QtQuick
Item {
id: root
required property var visibilities
required property Item sidebar
required property Item popouts
readonly property PersistentProperties props: PersistentProperties {
property bool recordingListExpanded: false
property string recordingConfirmDelete
property string recordingMode
reloadableId: "utilities"
}
readonly property bool shouldBeActive: visibilities.sidebar
visible: height > 0
implicitHeight: 0
implicitWidth: sidebar.visible ? sidebar.width : Config.utilities.sizes.width
onStateChanged: {
if (state === "visible" && timer.running) {
timer.triggered();
timer.stop();
}
}
states: State {
name: "visible"
when: root.shouldBeActive
PropertyChanges {
root.implicitHeight: content.implicitHeight + 18 * 2
}
}
transitions: [
Transition {
from: ""
to: "visible"
Modules.Anim {
target: root
property: "implicitHeight"
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
}
},
Transition {
from: "visible"
to: ""
Modules.Anim {
target: root
property: "implicitHeight"
easing.bezierCurve: MaterialEasing.expressiveEffects
}
}
]
Timer {
id: timer
running: true
interval: 1000
onTriggered: {
content.active = Qt.binding(() => root.shouldBeActive || root.visible);
content.visible = true;
}
}
Loader {
id: content
anchors.top: parent.top
anchors.left: parent.left
anchors.margins: 18
visible: false
active: true
sourceComponent: Content {
implicitWidth: root.implicitWidth - 18 * 2
props: root.props
visibilities: root.visibilities
popouts: root.popouts
}
}
}
+69
View File
@@ -0,0 +1,69 @@
pragma ComponentBehavior: Bound
import qs.Components
import qs.Config
import qs.Modules as Modules
import QtQuick
Item {
id: root
required property var visibilities
required property var panels
readonly property Props props: Props {}
visible: width > 0
implicitWidth: 0
states: State {
name: "visible"
when: root.visibilities.sidebar
PropertyChanges {
root.implicitWidth: Config.sidebar.sizes.width
}
}
transitions: [
Transition {
from: ""
to: "visible"
Modules.Anim {
target: root
property: "implicitWidth"
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
}
},
Transition {
from: "visible"
to: ""
Modules.Anim {
target: root
property: "implicitWidth"
easing.bezierCurve: MaterialEasing.expressiveEffects
}
}
]
Loader {
id: content
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.margins: 8
anchors.bottomMargin: 0
active: true
Component.onCompleted: active = Qt.binding(() => (root.visibilities.sidebar && Config.sidebar.enabled) || root.visible)
sourceComponent: Content {
implicitWidth: Config.sidebar.sizes.width - 8 * 2
props: root.props
visibilities: root.visibilities
}
}
}
+40
View File
@@ -0,0 +1,40 @@
import QtQuick
import qs.Components
import qs.Config
import qs.Modules as Modules
Item {
id: root
required property var visibilities
required property Item panels
visible: height > 0
implicitWidth: Math.max(panels.sidebar.width, content.implicitWidth)
implicitHeight: content.implicitHeight
states: State {
name: "hidden"
when: root.visibilities.sidebar
PropertyChanges {
root.implicitHeight: 0
}
}
transitions: Transition {
Modules.Anim {
target: root
property: "implicitHeight"
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
}
}
Content {
id: content
visibilities: root.visibilities
panels: root.panels
}
}