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
+11 -1
View File
@@ -16,6 +16,7 @@ RowLayout {
readonly property int vPadding: 6
required property Wrapper popouts
required property PersistentProperties visibilities
required property PanelWindow bar
function checkPopout(x: real): void {
@@ -26,6 +27,9 @@ RowLayout {
return;
}
if ( visibilities.sidebar )
return;
const id = ch.id;
const top = ch.x;
const item = ch.item;
@@ -56,6 +60,10 @@ RowLayout {
popouts.currentName = "calendar";
popouts.currentCenter = Qt.binding( () => item.mapToItem( root, itemWidth / 2, 0 ).x );
popouts.hasCurrent = true;
} else if ( x > (root.width / 2 + 50) && x < (root.width / 2 - 50) && Config.barConfig.popouts.activeWindow ) {
popouts.currentName = "dash";
popouts.currentCenter = root.width / 2;
popouts.hasCurrent = true;
}
}
@@ -128,7 +136,9 @@ RowLayout {
DelegateChoice {
roleValue: "notifBell"
delegate: WrappedLoader {
sourceComponent: NotifBell {}
sourceComponent: NotifBell {
visibilities: root.visibilities
}
}
}
DelegateChoice {
+9
View File
@@ -7,6 +7,7 @@ import qs.Config
import qs.Modules.Calendar
import qs.Modules.WSOverview
import qs.Modules.Polkit
import qs.Modules.Dashboard
Item {
id: root
@@ -88,6 +89,14 @@ Item {
screen: root.wrapper.screen
}
}
Popout {
name: "dash"
sourceComponent: Dashboard {
wrapper: root.wrapper
}
}
}
component Popout: Loader {
+88
View File
@@ -0,0 +1,88 @@
import Quickshell
import QtQuick.Layouts
import qs.Helpers
import qs.Components
import qs.Modules
import qs.Config
import qs.Modules.Dashboard.Dash
GridLayout {
id: root
required property PersistentProperties state
rowSpacing: 8
columnSpacing: 8
Rect {
Layout.column: 2
Layout.columnSpan: 3
Layout.preferredWidth: 48
Layout.preferredHeight: 48
radius: 8
CachingImage {
path: Quickshell.env("HOME") + "/.face"
}
}
Rect {
Layout.row: 0
Layout.columnSpan: 2
Layout.preferredWidth: Config.dashboard.sizes.weatherWidth
Layout.fillHeight: true
radius: 8
Weather {}
}
Rect {
Layout.row: 1
Layout.preferredWidth: dateTime.implicitWidth
Layout.fillHeight: true
radius: 8
DateTime {
id: dateTime
}
}
Rect {
Layout.row: 1
Layout.column: 1
Layout.columnSpan: 3
Layout.fillWidth: true
Layout.preferredHeight: 100
radius: 8
}
Rect {
Layout.row: 1
Layout.column: 4
Layout.preferredWidth: 100
Layout.fillHeight: true
radius: 8
}
Rect {
Layout.row: 0
Layout.column: 5
Layout.rowSpan: 2
Layout.preferredWidth: 100
Layout.fillHeight: true
radius: 8
}
component Rect: CustomRect {
color: DynamicColors.tPalette.m3surfaceContainer
}
}
+49
View File
@@ -0,0 +1,49 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import qs.Components
import qs.Config
Item {
id: root
anchors.top: parent.top
anchors.bottom: parent.bottom
implicitWidth: 110
ColumnLayout {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 0
CustomText {
Layout.bottomMargin: -(font.pointSize * 0.4)
Layout.alignment: Qt.AlignHCenter
text: Time.hourStr
color: DynamicColors.palette.m3secondary
font.pointSize: 18
font.family: "Rubik"
font.weight: 600
}
CustomText {
Layout.alignment: Qt.AlignHCenter
text: "•••"
color: DynamicColors.palette.m3primary
font.pointSize: 18 * 0.9
font.family: "Rubik"
}
CustomText {
Layout.topMargin: -(font.pointSize * 0.4)
Layout.alignment: Qt.AlignHCenter
text: Time.minuteStr
color: DynamicColors.palette.m3secondary
font.pointSize: 18
font.family: "Rubik"
font.weight: 600
}
}
}
+55
View File
@@ -0,0 +1,55 @@
import QtQuick
import qs.Helpers
import qs.Components
import qs.Config
Item {
id: root
anchors.centerIn: parent
implicitWidth: icon.implicitWidth + info.implicitWidth + info.anchors.leftMargin
Component.onCompleted: Weather.reload()
MaterialIcon {
id: icon
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
animate: true
text: Weather.icon
color: DynamicColors.palette.m3secondary
font.pointSize: 24
}
Column {
id: info
anchors.verticalCenter: parent.verticalCenter
anchors.left: icon.right
spacing: 8
CustomText {
anchors.horizontalCenter: parent.horizontalCenter
animate: true
text: Weather.temp
color: DynamicColors.palette.m3primary
font.pointSize: Appearance.font.size.extraLarge
font.weight: 500
}
CustomText {
anchors.horizontalCenter: parent.horizontalCenter
animate: true
text: Weather.description
elide: Text.ElideRight
width: Math.min(implicitWidth, root.parent.width - icon.implicitWidth - info.anchors.leftMargin - 24 * 2)
}
}
}
+133
View File
@@ -0,0 +1,133 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
import qs.Config
import qs.Modules
Item {
id: root
required property var wrapper
readonly property PersistentProperties state: PersistentProperties {
property int currentTab
property date currentDate: new Date()
reloadableId: "dashboardState"
}
readonly property real nonAnimWidth: view.implicitWidth + viewWrapper.anchors.margins * 2
readonly property real nonAnimHeight: tabs.implicitHeight + tabs.anchors.topMargin + view.implicitHeight + viewWrapper.anchors.margins * 2
implicitWidth: nonAnimWidth
implicitHeight: nonAnimHeight
Tabs {
id: tabs
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
nonAnimWidth: root.nonAnimWidth - anchors.margins * 2
state: root.state
}
ClippingRectangle {
id: viewWrapper
anchors.top: tabs.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
radius: 8
color: "transparent"
Flickable {
id: view
readonly property int currentIndex: root.state.currentTab
readonly property Item currentItem: row.children[currentIndex]
anchors.fill: parent
flickableDirection: Flickable.HorizontalFlick
implicitWidth: currentItem.implicitWidth
implicitHeight: currentItem.implicitHeight
contentX: currentItem.x
contentWidth: row.implicitWidth
contentHeight: row.implicitHeight
onContentXChanged: {
if (!moving)
return;
const x = contentX - currentItem.x;
if (x > currentItem.implicitWidth / 2)
root.state.currentTab = Math.min(root.state.currentTab + 1, tabs.count - 1);
else if (x < -currentItem.implicitWidth / 2)
root.state.currentTab = Math.max(root.state.currentTab - 1, 0);
}
onDragEnded: {
const x = contentX - currentItem.x;
if (x > currentItem.implicitWidth / 10)
root.state.currentTab = Math.min(root.state.currentTab + 1, tabs.count - 1);
else if (x < -currentItem.implicitWidth / 10)
root.state.currentTab = Math.max(root.state.currentTab - 1, 0);
else
contentX = Qt.binding(() => currentItem.x);
}
RowLayout {
id: row
Pane {
index: 0
sourceComponent: Dash {
state: root.state
}
}
}
Behavior on contentX {
Anim {}
}
}
}
Behavior on implicitWidth {
Anim {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
}
}
Behavior on implicitHeight {
Anim {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
}
}
component Pane: Loader {
id: pane
required property int index
Layout.alignment: Qt.AlignTop
Component.onCompleted: active = Qt.binding(() => {
// Always keep current tab loaded
if (pane.index === view.currentIndex)
return true;
const vx = Math.floor(view.visibleArea.xPosition * view.contentWidth);
const vex = Math.floor(vx + view.visibleArea.widthRatio * view.contentWidth);
return (vx >= x && vx <= x + implicitWidth) || (vex >= x && vex <= x + implicitWidth);
})
}
}
+246
View File
@@ -0,0 +1,246 @@
pragma ComponentBehavior: Bound
import qs.Components
import qs.Config
import qs.Helpers
import qs.Modules
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Controls
Item {
id: root
required property real nonAnimWidth
required property PersistentProperties state
readonly property alias count: bar.count
implicitHeight: bar.implicitHeight + indicator.implicitHeight + indicator.anchors.topMargin + separator.implicitHeight
TabBar {
id: bar
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
currentIndex: root.state.currentTab
background: null
onCurrentIndexChanged: root.state.currentTab = currentIndex
Tab {
iconName: "dashboard"
text: qsTr("Dashboard")
}
Tab {
iconName: "queue_music"
text: qsTr("Media")
}
Tab {
iconName: "speed"
text: qsTr("Performance")
}
Tab {
iconName: "cloud"
text: qsTr("Weather")
}
// Tab {
// iconName: "workspaces"
// text: qsTr("Workspaces")
// }
}
Item {
id: indicator
anchors.top: bar.bottom
implicitWidth: bar.currentItem.implicitWidth
implicitHeight: 40
x: {
const tab = bar.currentItem;
const width = (root.nonAnimWidth - bar.spacing * (bar.count - 1)) / bar.count;
return width * tab.TabBar.index + (width - tab.implicitWidth) / 2;
}
clip: true
CustomRect {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
implicitHeight: parent.implicitHeight * 2
color: DynamicColors.palette.m3primary
radius: 1000
}
Behavior on x {
Anim {}
}
Behavior on implicitWidth {
Anim {}
}
}
CustomRect {
id: separator
anchors.top: indicator.bottom
anchors.left: parent.left
anchors.right: parent.right
implicitHeight: 1
color: DynamicColors.palette.m3outlineVariant
}
component Tab: TabButton {
id: tab
required property string iconName
readonly property bool current: TabBar.tabBar.currentItem === this
background: null
contentItem: CustomMouseArea {
id: mouse
implicitWidth: Math.max(icon.width, label.width)
implicitHeight: icon.height + label.height
cursorShape: Qt.PointingHandCursor
onPressed: event => {
root.state.currentTab = tab.TabBar.index;
const stateY = stateWrapper.y;
rippleAnim.x = event.x;
rippleAnim.y = event.y - stateY;
const dist = (ox, oy) => ox * ox + oy * oy;
rippleAnim.radius = Math.sqrt(Math.max(dist(event.x, event.y + stateY), dist(event.x, stateWrapper.height - event.y), dist(width - event.x, event.y + stateY), dist(width - event.x, stateWrapper.height - event.y)));
rippleAnim.restart();
}
function onWheel(event: WheelEvent): void {
if (event.angleDelta.y < 0)
root.state.currentTab = Math.min(root.state.currentTab + 1, bar.count - 1);
else if (event.angleDelta.y > 0)
root.state.currentTab = Math.max(root.state.currentTab - 1, 0);
}
SequentialAnimation {
id: rippleAnim
property real x
property real y
property real radius
PropertyAction {
target: ripple
property: "x"
value: rippleAnim.x
}
PropertyAction {
target: ripple
property: "y"
value: rippleAnim.y
}
PropertyAction {
target: ripple
property: "opacity"
value: 0.08
}
Anim {
target: ripple
properties: "implicitWidth,implicitHeight"
from: 0
to: rippleAnim.radius * 2
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
}
Anim {
target: ripple
property: "opacity"
to: 0
easing.type: Easing.BezierSpline
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
}
}
ClippingRectangle {
id: stateWrapper
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
implicitHeight: parent.height + 8 * 2
color: "transparent"
radius: 8
CustomRect {
id: stateLayer
anchors.fill: parent
color: tab.current ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurface
opacity: mouse.pressed ? 0.1 : tab.hovered ? 0.08 : 0
Behavior on opacity {
Anim {}
}
}
CustomRect {
id: ripple
radius: 1000
color: tab.current ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurface
opacity: 0
transform: Translate {
x: -ripple.width / 2
y: -ripple.height / 2
}
}
}
MaterialIcon {
id: icon
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: label.top
text: tab.iconName
color: tab.current ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurfaceVariant
fill: tab.current ? 1 : 0
font.pointSize: 18
Behavior on fill {
Anim {}
}
}
CustomText {
id: label
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
text: tab.text
color: tab.current ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurfaceVariant
}
}
}
}
+5 -1
View File
@@ -1,3 +1,4 @@
import Quickshell
import Quickshell.Hyprland
import QtQuick
import qs.Config
@@ -7,6 +8,8 @@ import qs.Components
Item {
id: root
required property PersistentProperties visibilities
implicitWidth: 20
anchors.top: parent.top
anchors.bottom: parent.bottom
@@ -33,7 +36,8 @@ Item {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
Hyprland.dispatch("global zshell-nc:toggle-nc");
// Hyprland.dispatch("global zshell-nc:toggle-nc");
root.visibilities.sidebar = !root.visibilities.sidebar;
}
}
}
+138 -143
View File
@@ -11,173 +11,168 @@ import qs.Helpers
import qs.Daemons
import qs.Effects
PanelWindow {
id: root
color: "transparent"
anchors {
top: true
right: true
left: true
bottom: true
}
Scope {
Variants {
model: Quickshell.screens
WlrLayershell.namespace: "ZShell-Notifs"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
property bool centerShown: false
property alias posX: backgroundRect.x
visible: false
mask: Region { item: backgroundRect }
Connections {
target: Hypr
function onFocusedMonitorChanged(): void {
if ( !root.centerShown ) {
root.screen = Hypr.getActiveScreen();
PanelWindow {
id: root
color: "transparent"
anchors {
top: true
right: true
left: true
bottom: true
}
}
}
GlobalShortcut {
appid: "zshell-nc"
name: "toggle-nc"
onPressed: {
root.screen = Hypr.getActiveScreen();
root.centerShown = !root.centerShown;
}
}
WlrLayershell.namespace: "ZShell-Notifs"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
property bool centerShown: false
property alias posX: backgroundRect.x
visible: false
onVisibleChanged: {
if ( root.visible ) {
showAnimation.start();
}
}
mask: Region { item: backgroundRect }
onCenterShownChanged: {
if ( !root.centerShown ) {
closeAnimation.start();
closeTimer.start();
} else {
root.visible = true;
}
}
GlobalShortcut {
appid: "zshell-nc"
name: "toggle-nc"
onPressed: {
root.centerShown = !root.centerShown;
}
}
Keys.onPressed: {
if ( event.key === Qt.Key_Escape ) {
root.centerShown = false;
event.accepted = true;
}
}
onVisibleChanged: {
if ( root.visible ) {
showAnimation.start();
}
}
Timer {
id: closeTimer
interval: 300
onTriggered: {
root.visible = false;
}
}
onCenterShownChanged: {
if ( !root.centerShown ) {
closeAnimation.start();
closeTimer.start();
} else if ( Hypr.getActiveScreen() === root.screen ) {
root.visible = true;
}
}
NumberAnimation {
id: showAnimation
target: backgroundRect
property: "x"
to: Math.round(root.screen.width - backgroundRect.implicitWidth - 10)
from: root.screen.width
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
onStopped: {
focusGrab.active = true;
}
}
Keys.onPressed: {
if ( event.key === Qt.Key_Escape ) {
root.centerShown = false;
event.accepted = true;
}
}
NumberAnimation {
id: closeAnimation
target: backgroundRect
property: "x"
from: root.screen.width - backgroundRect.implicitWidth - 10
to: root.screen.width
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
}
Timer {
id: closeTimer
interval: 300
onTriggered: {
root.visible = false;
}
}
HyprlandFocusGrab {
id: focusGrab
active: false
windows: [ root ]
onCleared: {
root.centerShown = false;
}
}
NumberAnimation {
id: showAnimation
target: backgroundRect
property: "x"
to: Math.round(root.screen.width - backgroundRect.implicitWidth - 10)
from: root.screen.width
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
onStopped: {
focusGrab.active = true;
}
}
TrackedNotification {
centerShown: root.centerShown
screen: root.screen
}
NumberAnimation {
id: closeAnimation
target: backgroundRect
property: "x"
from: root.screen.width - backgroundRect.implicitWidth - 10
to: root.screen.width
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
}
ShadowRect {
anchors.fill: backgroundRect
radius: backgroundRect.radius
}
HyprlandFocusGrab {
id: focusGrab
active: false
windows: [ root ]
onCleared: {
root.centerShown = false;
}
}
Rectangle {
id: backgroundRect
y: 10
x: Screen.width
z: 1
TrackedNotification {
centerShown: root.centerShown
screen: root.screen
}
property color backgroundColor: Config.useDynamicColors ? DynamicColors.tPalette.m3surface : Config.baseBgColor
implicitWidth: 400
implicitHeight: root.height - 20
color: backgroundColor
radius: 8
border.color: "#555555"
border.width: Config.useDynamicColors ? 0 : 1
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 10
NotificationCenterHeader { }
ShadowRect {
anchors.fill: backgroundRect
radius: backgroundRect.radius
}
Rectangle {
color: "#333333"
Layout.preferredHeight: Config.useDynamicColors ? 0 : 1
Layout.fillWidth: true
}
id: backgroundRect
y: 10
x: Screen.width
z: 1
Flickable {
Layout.fillWidth: true
Layout.fillHeight: true
pixelAligned: true
contentHeight: notificationColumn.implicitHeight
clip: true
property color backgroundColor: Config.useDynamicColors ? DynamicColors.tPalette.m3surface : Config.baseBgColor
Column {
id: notificationColumn
width: parent.width
implicitWidth: 400
implicitHeight: root.height - 20
color: backgroundColor
radius: 8
border.color: "#555555"
border.width: Config.useDynamicColors ? 0 : 1
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 10
add: Transition {
NumberAnimation {
properties: "x";
duration: 300;
easing.type: Easing.OutCubic
}
NotificationCenterHeader { }
Rectangle {
color: "#333333"
Layout.preferredHeight: Config.useDynamicColors ? 0 : 1
Layout.fillWidth: true
}
move: Transition {
NumberAnimation {
properties: "x";
duration: 200;
easing.type: Easing.OutCubic
Flickable {
Layout.fillWidth: true
Layout.fillHeight: true
pixelAligned: true
contentHeight: notificationColumn.implicitHeight
clip: true
Column {
id: notificationColumn
width: parent.width
spacing: 10
add: Transition {
NumberAnimation {
properties: "x";
duration: 300;
easing.type: Easing.OutCubic
}
}
move: Transition {
NumberAnimation {
properties: "x";
duration: 200;
easing.type: Easing.OutCubic
}
}
GroupListView { }
}
}
GroupListView { }
}
}
}
+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
}
}
+24 -7
View File
@@ -89,15 +89,31 @@ Scope {
id: contentRow
spacing: 24
IconImage {
source: Quickshell.iconPath(polkitAgent.flow?.iconName, true) ?? ""
implicitSize: 64
mipmap: true
Layout.preferredWidth: implicitSize
Layout.preferredHeight: implicitSize
Item {
Layout.preferredWidth: icon.implicitSize
Layout.preferredHeight: icon.implicitSize
Layout.leftMargin: 16
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
IconImage {
id: icon
anchors.fill: parent
visible: `${source}`.includes("://")
source: Quickshell.iconPath(polkitAgent.flow?.iconName, true) ?? ""
implicitSize: 64
mipmap: true
}
MaterialIcon {
visible: !icon.visible
text: "security"
anchors.fill: parent
font.pointSize: 64
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
ColumnLayout {
@@ -277,6 +293,7 @@ Scope {
Layout.alignment: Qt.AlignRight
onClicked: {
root.shouldShow = false
console.log(icon.source, icon.visible)
polkitAgent.flow.cancelAuthenticationRequest()
passInput.text = ""
}
+15 -13
View File
@@ -4,6 +4,7 @@ import Quickshell.Wayland
import Quickshell.Hyprland
import QtQuick.Layouts
import QtQuick
import qs.Components
import qs.Config
import qs.Daemons
import qs.Helpers
@@ -122,7 +123,7 @@ PanelWindow {
radius: backgroundRect.radius
}
Rectangle {
CustomClippingRect {
id: backgroundRect
implicitWidth: 400
implicitHeight: contentLayout.childrenRect.height + 16
@@ -131,6 +132,19 @@ PanelWindow {
border.color: "#555555"
radius: 8
CustomRect {
anchors.bottom: parent.bottom
anchors.right: parent.right
color: DynamicColors.palette.m3primary
implicitHeight: 4
implicitWidth: ( rootItem.modelData.timer.remainingTime / rootItem.modelData.timer.totalTime ) * parent.width
Behavior on implicitWidth {
Anim {}
}
}
Component.onCompleted: {
root.notifRegions.push( notifRegion.createObject(root, { item: backgroundRect }));
}
@@ -228,18 +242,6 @@ PanelWindow {
ElapsedTimer {
id: timer
}
}
MouseArea {
z: 1
anchors.fill: parent
hoverEnabled: true
onEntered: {
rootItem.modelData.timer.stop();
}
onExited: {
rootItem.modelData.timer.start();
}
}
}
}
+7 -3
View File
@@ -28,9 +28,13 @@ Item {
if ( mouse.button === Qt.LeftButton ) {
root.item.activate();
} else if ( mouse.button === Qt.RightButton ) {
root.popouts.currentName = `traymenu${ root.ind }`;
root.popouts.currentCenter = Qt.binding( () => root.mapToItem( root.loader, root.implicitWidth / 2, 0 ).x );
root.popouts.hasCurrent = true;
if ( visibilities.sidebar ) {
return;
} else {
root.popouts.currentName = `traymenu${ root.ind }`;
root.popouts.currentCenter = Qt.binding( () => root.mapToItem( root.loader, root.implicitWidth / 2, 0 ).x );
root.popouts.hasCurrent = true;
}
}
}
}