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
+24 -10
View File
@@ -12,15 +12,15 @@ import qs.Config
import qs.Helpers import qs.Helpers
import qs.Drawers import qs.Drawers
Scope { Variants {
Variants {
model: Quickshell.screens model: Quickshell.screens
Scope {
id: scope
required property var modelData
PanelWindow { PanelWindow {
id: bar id: bar
required property var modelData
property bool trayMenuVisible: false property bool trayMenuVisible: false
screen: modelData screen: scope.modelData
color: "transparent" color: "transparent"
property var root: Quickshell.shellDir property var root: Quickshell.shellDir
@@ -53,7 +53,7 @@ Scope {
y: 34 y: 34
property list<Region> nullRegions: [] property list<Region> nullRegions: []
property bool hcurrent: panels.popouts.hasCurrent && panels.popouts.currentName.startsWith("traymenu") property bool hcurrent: ( panels.popouts.hasCurrent && panels.popouts.currentName.startsWith("traymenu") ) || visibilities.sidebar
width: hcurrent ? 0 : bar.width width: hcurrent ? 0 : bar.width
height: hcurrent ? 0 : bar.screen.height - backgroundRect.implicitHeight height: hcurrent ? 0 : bar.screen.height - backgroundRect.implicitHeight
@@ -72,11 +72,19 @@ Scope {
x: modelData.x x: modelData.x
y: modelData.y + backgroundRect.implicitHeight y: modelData.y + backgroundRect.implicitHeight
width: modelData.width width: modelData.width
height: panels.popouts.hasCurrent ? modelData.height : 0 height: modelData.height
intersection: Intersection.Subtract intersection: Intersection.Subtract
} }
} }
PersistentProperties {
id: visibilities
property bool sidebar
Component.onCompleted: Visibilities.load(scope.modelData, this)
}
Item { Item {
anchors.fill: parent anchors.fill: parent
opacity: Config.transparency.enabled ? DynamicColors.transparency.base : 1 opacity: Config.transparency.enabled ? DynamicColors.transparency.base : 1
@@ -114,15 +122,19 @@ Scope {
} }
onPressed: event => { onPressed: event => {
var withinX = mouseX >= panels.popouts.x + 8 && mouseX < panels.popouts.x + panels.popouts.implicitWidth; var traywithinX = mouseX >= panels.popouts.x + 8 && mouseX < panels.popouts.x + panels.popouts.implicitWidth;
var withinY = mouseY >= panels.popouts.y + exclusionZone.implicitHeight && mouseY < panels.popouts.y + exclusionZone.implicitHeight + panels.popouts.implicitHeight; var traywithinY = mouseY >= panels.popouts.y + exclusionZone.implicitHeight && mouseY < panels.popouts.y + exclusionZone.implicitHeight + panels.popouts.implicitHeight;
var sidebarwithinX = mouseX <= bar.width - panels.sidebar.width
console.log(sidebarwithinX)
if ( panels.popouts.hasCurrent ) { if ( panels.popouts.hasCurrent ) {
if ( withinX && withinY ) { if ( traywithinX && traywithinY ) {
} else { } else {
panels.popouts.hasCurrent = false; panels.popouts.hasCurrent = false;
} }
} else if ( visibilities.sidebar && sidebarwithinX ) {
visibilities.sidebar = false;
} }
} }
@@ -130,6 +142,7 @@ Scope {
id: panels id: panels
screen: bar.modelData screen: bar.modelData
bar: backgroundRect bar: backgroundRect
visibilities: visibilities
} }
Rectangle { Rectangle {
@@ -151,6 +164,7 @@ Scope {
anchors.fill: parent anchors.fill: parent
popouts: panels.popouts popouts: panels.popouts
bar: bar bar: bar
visibilities: visibilities
} }
WindowTitle { WindowTitle {
+14
View File
@@ -0,0 +1,14 @@
import QtQuick
import qs.Modules
Flickable {
id: root
maximumFlickVelocity: 3000
rebound: Transition {
Anim {
properties: "x,y"
}
}
}
+10
View File
@@ -0,0 +1,10 @@
pragma ComponentBehavior: Bound
import Quickshell.Widgets
import QtQuick
IconImage {
id: root
asynchronous: true
}
+15
View File
@@ -0,0 +1,15 @@
import QtQuick
import qs.Config
import qs.Modules
ListView {
id: root
maximumFlickVelocity: 3000
rebound: Transition {
Anim {
properties: "x,y"
}
}
}
+189
View File
@@ -0,0 +1,189 @@
import qs.Config
import qs.Modules
import QtQuick
import QtQuick.Templates
ScrollBar {
id: root
required property Flickable flickable
property bool shouldBeActive
property real nonAnimPosition
property bool animating
onHoveredChanged: {
if (hovered)
shouldBeActive = true;
else
shouldBeActive = flickable.moving;
}
property bool _updatingFromFlickable: false
property bool _updatingFromUser: false
// Sync nonAnimPosition with Qt's automatic position binding
onPositionChanged: {
if (_updatingFromUser) {
_updatingFromUser = false;
return;
}
if (position === nonAnimPosition) {
animating = false;
return;
}
if (!animating && !_updatingFromFlickable && !fullMouse.pressed) {
nonAnimPosition = position;
}
}
// Sync nonAnimPosition with flickable when not animating
Connections {
target: flickable
function onContentYChanged() {
if (!animating && !fullMouse.pressed) {
_updatingFromFlickable = true;
const contentHeight = flickable.contentHeight;
const height = flickable.height;
if (contentHeight > height) {
nonAnimPosition = Math.max(0, Math.min(1, flickable.contentY / (contentHeight - height)));
} else {
nonAnimPosition = 0;
}
_updatingFromFlickable = false;
}
}
}
Component.onCompleted: {
if (flickable) {
const contentHeight = flickable.contentHeight;
const height = flickable.height;
if (contentHeight > height) {
nonAnimPosition = Math.max(0, Math.min(1, flickable.contentY / (contentHeight - height)));
}
}
}
implicitWidth: 8
contentItem: CustomRect {
anchors.left: parent.left
anchors.right: parent.right
opacity: {
if (root.size === 1)
return 0;
if (fullMouse.pressed)
return 1;
if (mouse.containsMouse)
return 0.8;
if (root.policy === ScrollBar.AlwaysOn || root.shouldBeActive)
return 0.6;
return 0;
}
radius: 1000
color: DynamicColors.palette.m3secondary
MouseArea {
id: mouse
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
acceptedButtons: Qt.NoButton
}
Behavior on opacity {
Anim {}
}
}
Connections {
target: root.flickable
function onMovingChanged(): void {
if (root.flickable.moving)
root.shouldBeActive = true;
else
hideDelay.restart();
}
}
Timer {
id: hideDelay
interval: 600
onTriggered: root.shouldBeActive = root.flickable.moving || root.hovered
}
CustomMouseArea {
id: fullMouse
anchors.fill: parent
preventStealing: true
onPressed: event => {
root.animating = true;
root._updatingFromUser = true;
const newPos = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2));
root.nonAnimPosition = newPos;
// Update flickable position
// Map scrollbar position [0, 1-size] to contentY [0, maxContentY]
if (root.flickable) {
const contentHeight = root.flickable.contentHeight;
const height = root.flickable.height;
if (contentHeight > height) {
const maxContentY = contentHeight - height;
const maxPos = 1 - root.size;
const contentY = maxPos > 0 ? (newPos / maxPos) * maxContentY : 0;
root.flickable.contentY = Math.max(0, Math.min(maxContentY, contentY));
}
}
}
onPositionChanged: event => {
root._updatingFromUser = true;
const newPos = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2));
root.nonAnimPosition = newPos;
// Update flickable position
// Map scrollbar position [0, 1-size] to contentY [0, maxContentY]
if (root.flickable) {
const contentHeight = root.flickable.contentHeight;
const height = root.flickable.height;
if (contentHeight > height) {
const maxContentY = contentHeight - height;
const maxPos = 1 - root.size;
const contentY = maxPos > 0 ? (newPos / maxPos) * maxContentY : 0;
root.flickable.contentY = Math.max(0, Math.min(maxContentY, contentY));
}
}
}
function onWheel(event: WheelEvent): void {
root.animating = true;
root._updatingFromUser = true;
let newPos = root.nonAnimPosition;
if (event.angleDelta.y > 0)
newPos = Math.max(0, root.nonAnimPosition - 0.1);
else if (event.angleDelta.y < 0)
newPos = Math.min(1 - root.size, root.nonAnimPosition + 0.1);
root.nonAnimPosition = newPos;
// Update flickable position
// Map scrollbar position [0, 1-size] to contentY [0, maxContentY]
if (root.flickable) {
const contentHeight = root.flickable.contentHeight;
const height = root.flickable.height;
if (contentHeight > height) {
const maxContentY = contentHeight - height;
const maxPos = 1 - root.size;
const contentY = maxPos > 0 ? (newPos / maxPos) * maxContentY : 0;
root.flickable.contentY = Math.max(0, Math.min(maxContentY, contentY));
}
}
}
}
Behavior on position {
enabled: !fullMouse.pressed
Anim {}
}
}
+151
View File
@@ -0,0 +1,151 @@
import qs.Config
import qs.Modules
import QtQuick
import QtQuick.Templates
import QtQuick.Shapes
Switch {
id: root
property int cLayer: 1
implicitWidth: implicitIndicatorWidth
implicitHeight: implicitIndicatorHeight
indicator: CustomRect {
radius: 1000
color: root.checked ? DynamicColors.palette.m3primary : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, root.cLayer)
implicitWidth: implicitHeight * 1.7
implicitHeight: 13 + 7 * 2
CustomRect {
readonly property real nonAnimWidth: root.pressed ? implicitHeight * 1.3 : implicitHeight
radius: 1000
color: root.checked ? DynamicColors.palette.m3onPrimary : DynamicColors.layer(DynamicColors.palette.m3outline, root.cLayer + 1)
x: root.checked ? parent.implicitWidth - nonAnimWidth - 10 / 2 : 10 / 2
implicitWidth: nonAnimWidth
implicitHeight: parent.implicitHeight - 10
anchors.verticalCenter: parent.verticalCenter
CustomRect {
anchors.fill: parent
radius: parent.radius
color: root.checked ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurface
opacity: root.pressed ? 0.1 : root.hovered ? 0.08 : 0
Behavior on opacity {
Anim {}
}
}
Shape {
id: icon
property point start1: {
if (root.pressed)
return Qt.point(width * 0.2, height / 2);
if (root.checked)
return Qt.point(width * 0.15, height / 2);
return Qt.point(width * 0.15, height * 0.15);
}
property point end1: {
if (root.pressed) {
if (root.checked)
return Qt.point(width * 0.4, height / 2);
return Qt.point(width * 0.8, height / 2);
}
if (root.checked)
return Qt.point(width * 0.4, height * 0.7);
return Qt.point(width * 0.85, height * 0.85);
}
property point start2: {
if (root.pressed) {
if (root.checked)
return Qt.point(width * 0.4, height / 2);
return Qt.point(width * 0.2, height / 2);
}
if (root.checked)
return Qt.point(width * 0.4, height * 0.7);
return Qt.point(width * 0.15, height * 0.85);
}
property point end2: {
if (root.pressed)
return Qt.point(width * 0.8, height / 2);
if (root.checked)
return Qt.point(width * 0.85, height * 0.2);
return Qt.point(width * 0.85, height * 0.15);
}
anchors.centerIn: parent
width: height
height: parent.implicitHeight - 10 * 2
preferredRendererType: Shape.CurveRenderer
asynchronous: true
ShapePath {
strokeWidth: 20 * 0.15
strokeColor: root.checked ? DynamicColors.palette.m3primary : DynamicColors.palette.m3surfaceContainerHighest
fillColor: "transparent"
capStyle: 1 === 0 ? ShapePath.SquareCap : ShapePath.RoundCap
startX: icon.start1.x
startY: icon.start1.y
PathLine {
x: icon.end1.x
y: icon.end1.y
}
PathMove {
x: icon.start2.x
y: icon.start2.y
}
PathLine {
x: icon.end2.x
y: icon.end2.y
}
Behavior on strokeColor {
CAnim {}
}
}
Behavior on start1 {
PropAnim {}
}
Behavior on end1 {
PropAnim {}
}
Behavior on start2 {
PropAnim {}
}
Behavior on end2 {
PropAnim {}
}
}
Behavior on x {
Anim {}
}
Behavior on implicitWidth {
Anim {}
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
enabled: false
}
component PropAnim: PropertyAnimation {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
easing.type: Easing.BezierSpline
}
}
+18
View File
@@ -0,0 +1,18 @@
import qs.Config
import qs.Modules
import QtQuick
import QtQuick.Effects
RectangularShadow {
property int level
property real dp: [0, 1, 3, 6, 8, 12][level]
color: Qt.alpha(DynamicColors.palette.m3shadow, 0.7)
blur: (dp * 5) ** 0.7
spread: -dp * 0.3 + (dp * 0.1) ** 2
offset.y: dp / 2
Behavior on dp {
Anim {}
}
}
+49
View File
@@ -0,0 +1,49 @@
import qs.Config
import qs.Modules
import QtQuick
CustomRect {
required property int extra
anchors.right: parent.right
anchors.margins: 8
color: DynamicColors.palette.m3tertiary
radius: 8
implicitWidth: count.implicitWidth + 8 * 2
implicitHeight: count.implicitHeight + 4 * 2
opacity: extra > 0 ? 1 : 0
scale: extra > 0 ? 1 : 0.5
Elevation {
anchors.fill: parent
radius: parent.radius
opacity: parent.opacity
z: -1
level: 2
}
CustomText {
id: count
anchors.centerIn: parent
animate: parent.opacity > 0
text: qsTr("+%1").arg(parent.extra)
color: DynamicColors.palette.m3onTertiary
}
Behavior on opacity {
Anim {
duration: MaterialEasing.expressiveEffectsTime
}
}
Behavior on scale {
Anim {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
}
}
}
+82
View File
@@ -0,0 +1,82 @@
import qs.Config
import qs.Modules
import QtQuick
CustomRect {
id: root
enum Type {
Filled,
Tonal,
Text
}
property alias icon: label.text
property bool checked
property bool toggle
property real padding: type === IconButton.Text ? 10 / 2 : 7
property alias font: label.font
property int type: IconButton.Filled
property bool disabled
property alias stateLayer: stateLayer
property alias label: label
property alias radiusAnim: radiusAnim
property bool internalChecked
property color activeColour: type === IconButton.Filled ? DynamicColors.palette.m3primary : DynamicColors.palette.m3secondary
property color inactiveColour: {
if (!toggle && type === IconButton.Filled)
return DynamicColors.palette.m3primary;
return type === IconButton.Filled ? DynamicColors.tPalette.m3surfaceContainer : DynamicColors.palette.m3secondaryContainer;
}
property color activeOnColour: type === IconButton.Filled ? DynamicColors.palette.m3onPrimary : type === IconButton.Tonal ? DynamicColors.palette.m3onSecondary : DynamicColors.palette.m3primary
property color inactiveOnColour: {
if (!toggle && type === IconButton.Filled)
return DynamicColors.palette.m3onPrimary;
return type === IconButton.Tonal ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurfaceVariant;
}
property color disabledColour: Qt.alpha(DynamicColors.palette.m3onSurface, 0.1)
property color disabledOnColour: Qt.alpha(DynamicColors.palette.m3onSurface, 0.38)
signal clicked
onCheckedChanged: internalChecked = checked
radius: internalChecked ? 6 : implicitHeight / 2 * Math.min(1, 1)
color: type === IconButton.Text ? "transparent" : disabled ? disabledColour : internalChecked ? activeColour : inactiveColour
implicitWidth: implicitHeight
implicitHeight: label.implicitHeight + padding * 2
StateLayer {
id: stateLayer
color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour
disabled: root.disabled
function onClicked(): void {
if (root.toggle)
root.internalChecked = !root.internalChecked;
root.clicked();
}
}
MaterialIcon {
id: label
anchors.centerIn: parent
color: root.disabled ? root.disabledOnColour : root.internalChecked ? root.activeOnColour : root.inactiveOnColour
fill: !root.toggle || root.internalChecked ? 1 : 0
Behavior on fill {
Anim {}
}
}
Behavior on radius {
Anim {
id: radiusAnim
}
}
}
+9
View File
@@ -0,0 +1,9 @@
import Quickshell
import QtQuick
ShaderEffect {
required property Item source
required property Item maskSource
fragmentShader: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/shaders/opacitymask.frag.qsb`)
}
+1 -1
View File
@@ -49,7 +49,7 @@ JsonObject {
component Popouts: JsonObject { component Popouts: JsonObject {
property bool tray: true property bool tray: true
property bool audio: true property bool audio: true
property bool activeWindow: false property bool activeWindow: true
property bool resources: true property bool resources: true
property bool clock: true property bool clock: true
} }
+8
View File
@@ -25,6 +25,10 @@ Singleton {
property alias lock: adapter.lock property alias lock: adapter.lock
property alias idle: adapter.idle property alias idle: adapter.idle
property alias overview: adapter.overview property alias overview: adapter.overview
property alias services: adapter.services
property alias notifs: adapter.notifs
property alias sidebar: adapter.sidebar
property alias utilities: adapter.utilities
FileView { FileView {
id: root id: root
@@ -58,6 +62,10 @@ Singleton {
property LockConf lock: LockConf {} property LockConf lock: LockConf {}
property IdleTimeout idle: IdleTimeout {} property IdleTimeout idle: IdleTimeout {}
property Overview overview: Overview {} property Overview overview: Overview {}
property Services services: Services {}
property NotifConfig notifs: NotifConfig {}
property SidebarConfig sidebar: SidebarConfig {}
property UtilConfig utilities: UtilConfig {}
} }
} }
} }
+18
View File
@@ -0,0 +1,18 @@
import Quickshell.Io
JsonObject {
property bool expire: true
property int defaultExpireTimeout: 5000
property real clearThreshold: 0.3
property int expandThreshold: 20
property bool actionOnClick: false
property int groupPreviewNum: 3
property bool openExpanded: false
property Sizes sizes: Sizes {}
component Sizes: JsonObject {
property int width: 400
property int image: 41
property int badge: 20
}
}
+6
View File
@@ -0,0 +1,6 @@
import Quickshell.Io
import QtQuick
JsonObject {
property string weatherLocation: ""
}
+10
View File
@@ -0,0 +1,10 @@
import Quickshell.Io
JsonObject {
property bool enabled: true
property Sizes sizes: Sizes {}
component Sizes: JsonObject {
property int width: 430
}
}
+35
View File
@@ -0,0 +1,35 @@
import Quickshell.Io
JsonObject {
property bool enabled: true
property int maxToasts: 4
property Sizes sizes: Sizes {}
property Toasts toasts: Toasts {}
property Vpn vpn: Vpn {}
component Sizes: JsonObject {
property int width: 430
property int toastWidth: 430
}
component Toasts: JsonObject {
property bool configLoaded: true
property bool chargingChanged: true
property bool gameModeChanged: true
property bool dndChanged: true
property bool audioOutputChanged: true
property bool audioInputChanged: true
property bool capsLockChanged: true
property bool numLockChanged: true
property bool kbLayoutChanged: true
property bool kbLimit: true
property bool vpnChanged: true
property bool nowPlaying: false
}
component Vpn: JsonObject {
property bool enabled: false
property list<var> provider: ["netbird"]
}
}
+64 -15
View File
@@ -4,6 +4,7 @@ pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Services.Notifications import Quickshell.Services.Notifications
import Quickshell.Hyprland
import QtQuick import QtQuick
import ZShell import ZShell
import qs.Modules import qs.Modules
@@ -101,6 +102,40 @@ Singleton {
} }
} }
GlobalShortcut {
name: "clearNotifs"
description: "Clear all notifications"
onPressed: {
for (const notif of root.list.slice())
notif.close();
}
}
IpcHandler {
target: "notifs"
function clear(): void {
for (const notif of root.list.slice())
notif.close();
}
function isDndEnabled(): bool {
return props.dnd;
}
function toggleDnd(): void {
props.dnd = !props.dnd;
}
function enableDnd(): void {
props.dnd = true;
}
function disableDnd(): void {
props.dnd = false;
}
}
component Notif: QtObject { component Notif: QtObject {
id: notif id: notif
@@ -140,10 +175,21 @@ Singleton {
property list<var> actions property list<var> actions
readonly property Timer timer: Timer { readonly property Timer timer: Timer {
running: true property int totalTime: 5000
interval: 5000 property int remainingTime: totalTime
property bool paused: false
running: !paused
repeat: true
interval: 50
onTriggered: { onTriggered: {
remainingTime -= interval;
if ( remainingTime <= 0 ) {
remainingTime = 0;
notif.popup = false; notif.popup = false;
stop();
}
} }
} }
@@ -151,22 +197,14 @@ Singleton {
active: false active: false
PanelWindow { PanelWindow {
implicitWidth: 48 implicitWidth: Config.notifs.sizes.image
implicitHeight: 48 implicitHeight: Config.notifs.sizes.image
color: "transparent" color: "transparent"
mask: Region {} mask: Region {}
visible: false
Image { Image {
anchors.fill: parent function tryCache(): void {
source: Qt.resolvedUrl(notif.image) if (status !== Image.Ready || width != Config.notifs.sizes.image || height != Config.notifs.sizes.image)
fillMode: Image.PreserveAspectCrop
cache: false
asynchronous: true
opacity: 0
onStatusChanged: {
if (status !== Image.Ready)
return; return;
const cacheKey = notif.appName + notif.summary + notif.id; const cacheKey = notif.appName + notif.summary + notif.id;
@@ -183,11 +221,22 @@ Singleton {
const hash = (h2 >>> 0).toString(16).padStart(8, 0) + (h1 >>> 0).toString(16).padStart(8, 0); const hash = (h2 >>> 0).toString(16).padStart(8, 0) + (h1 >>> 0).toString(16).padStart(8, 0);
const cache = `${Paths.notifimagecache}/${hash}.png`; const cache = `${Paths.notifimagecache}/${hash}.png`;
ZShellIo.saveItem(this, Qt.resolvedUrl(cache), () => { ZShell.saveItem(this, Qt.resolvedUrl(cache), () => {
notif.image = cache; notif.image = cache;
notif.dummyImageLoader.active = false; notif.dummyImageLoader.active = false;
}); });
} }
anchors.fill: parent
source: Qt.resolvedUrl(notif.image)
fillMode: Image.PreserveAspectCrop
cache: false
asynchronous: true
opacity: 0
onStatusChanged: tryCache()
onWidthChanged: tryCache()
onHeightChanged: tryCache()
} }
} }
} }
+29
View File
@@ -1,6 +1,9 @@
import QtQuick import QtQuick
import QtQuick.Shapes import QtQuick.Shapes
import qs.Modules as Modules import qs.Modules as Modules
import qs.Modules.Notifications as Notifications
import qs.Modules.Notifications.Sidebar as Sidebar
import qs.Modules.Notifications.Sidebar.Utils as Utils
Shape { Shape {
id: root id: root
@@ -20,4 +23,30 @@ Shape {
startX: wrapper.x - 8 startX: wrapper.x - 8
startY: wrapper.y startY: wrapper.y
} }
Notifications.Background {
wrapper: root.panels.notifications
sidebar: sidebar
startX: root.width
startY: 0
}
Utils.Background {
wrapper: root.panels.utilities
sidebar: sidebar
startX: root.width
startY: root.height
}
Sidebar.Background {
id: sidebar
wrapper: root.panels.sidebar
panels: root.panels
startX: root.width
startY: root.panels.notifications.height
}
} }
+39
View File
@@ -2,6 +2,9 @@ import Quickshell
import QtQuick import QtQuick
import QtQuick.Shapes import QtQuick.Shapes
import qs.Modules as Modules import qs.Modules as Modules
import qs.Modules.Notifications as Notifications
import qs.Modules.Notifications.Sidebar as Sidebar
import qs.Modules.Notifications.Sidebar.Utils as Utils
import qs.Config import qs.Config
Item { Item {
@@ -9,8 +12,12 @@ Item {
required property ShellScreen screen required property ShellScreen screen
required property Item bar required property Item bar
required property PersistentProperties visibilities
readonly property alias popouts: popouts readonly property alias popouts: popouts
readonly property alias sidebar: sidebar
readonly property alias notifications: notifications
readonly property alias utilities: utilities
anchors.fill: parent anchors.fill: parent
// anchors.margins: 8 // anchors.margins: 8
@@ -31,4 +38,36 @@ Item {
return Math.floor( Math.max( off, 0 )); return Math.floor( Math.max( off, 0 ));
} }
} }
Notifications.Wrapper {
id: notifications
visibilities: root.visibilities
panels: root
anchors.top: parent.top
anchors.right: parent.right
}
Utils.Wrapper {
id: utilities
visibilities: root.visibilities
sidebar: sidebar
popouts: popouts
anchors.bottom: parent.bottom
anchors.right: parent.right
}
Sidebar.Wrapper {
id: sidebar
visibilities: root.visibilities
panels: root
anchors.top: notifications.bottom
anchors.bottom: utilities.top
anchors.right: parent.right
}
} }
+187
View File
@@ -0,0 +1,187 @@
pragma Singleton
import qs.Config
import Quickshell
import Quickshell.Services.Notifications
import QtQuick
Singleton {
id: root
readonly property var weatherIcons: ({
"0": "clear_day",
"1": "clear_day",
"2": "partly_cloudy_day",
"3": "cloud",
"45": "foggy",
"48": "foggy",
"51": "rainy",
"53": "rainy",
"55": "rainy",
"56": "rainy",
"57": "rainy",
"61": "rainy",
"63": "rainy",
"65": "rainy",
"66": "rainy",
"67": "rainy",
"71": "cloudy_snowing",
"73": "cloudy_snowing",
"75": "snowing_heavy",
"77": "cloudy_snowing",
"80": "rainy",
"81": "rainy",
"82": "rainy",
"85": "cloudy_snowing",
"86": "snowing_heavy",
"95": "thunderstorm",
"96": "thunderstorm",
"99": "thunderstorm"
})
readonly property var categoryIcons: ({
WebBrowser: "web",
Printing: "print",
Security: "security",
Network: "chat",
Archiving: "archive",
Compression: "archive",
Development: "code",
IDE: "code",
TextEditor: "edit_note",
Audio: "music_note",
Music: "music_note",
Player: "music_note",
Recorder: "mic",
Game: "sports_esports",
FileTools: "files",
FileManager: "files",
Filesystem: "files",
FileTransfer: "files",
Settings: "settings",
DesktopSettings: "settings",
HardwareSettings: "settings",
TerminalEmulator: "terminal",
ConsoleOnly: "terminal",
Utility: "build",
Monitor: "monitor_heart",
Midi: "graphic_eq",
Mixer: "graphic_eq",
AudioVideoEditing: "video_settings",
AudioVideo: "music_video",
Video: "videocam",
Building: "construction",
Graphics: "photo_library",
"2DGraphics": "photo_library",
RasterGraphics: "photo_library",
TV: "tv",
System: "host",
Office: "content_paste"
})
function getAppIcon(name: string, fallback: string): string {
const icon = DesktopEntries.heuristicLookup(name)?.icon;
if (fallback !== "undefined")
return Quickshell.iconPath(icon, fallback);
return Quickshell.iconPath(icon);
}
function getAppCategoryIcon(name: string, fallback: string): string {
const categories = DesktopEntries.heuristicLookup(name)?.categories;
if (categories)
for (const [key, value] of Object.entries(categoryIcons))
if (categories.includes(key))
return value;
return fallback;
}
function getNetworkIcon(strength: int, isSecure = false): string {
if (isSecure) {
if (strength >= 80)
return "network_wifi_locked";
if (strength >= 60)
return "network_wifi_3_bar_locked";
if (strength >= 40)
return "network_wifi_2_bar_locked";
if (strength >= 20)
return "network_wifi_1_bar_locked";
return "signal_wifi_0_bar";
} else {
if (strength >= 80)
return "network_wifi";
if (strength >= 60)
return "network_wifi_3_bar";
if (strength >= 40)
return "network_wifi_2_bar";
if (strength >= 20)
return "network_wifi_1_bar";
return "signal_wifi_0_bar";
}
}
function getBluetoothIcon(icon: string): string {
if (icon.includes("headset") || icon.includes("headphones"))
return "headphones";
if (icon.includes("audio"))
return "speaker";
if (icon.includes("phone"))
return "smartphone";
if (icon.includes("mouse"))
return "mouse";
if (icon.includes("keyboard"))
return "keyboard";
return "bluetooth";
}
function getWeatherIcon(code: string): string {
if (weatherIcons.hasOwnProperty(code))
return weatherIcons[code];
return "air";
}
function getNotifIcon(summary: string, urgency: int): string {
summary = summary.toLowerCase();
if (summary.includes("reboot"))
return "restart_alt";
if (summary.includes("recording"))
return "screen_record";
if (summary.includes("battery"))
return "power";
if (summary.includes("screenshot"))
return "screenshot_monitor";
if (summary.includes("welcome"))
return "waving_hand";
if (summary.includes("time") || summary.includes("a break"))
return "schedule";
if (summary.includes("installed"))
return "download";
if (summary.includes("update"))
return "update";
if (summary.includes("unable to"))
return "deployed_code_alert";
if (summary.includes("profile"))
return "person";
if (summary.includes("file"))
return "folder_copy";
if (urgency === NotificationUrgency.Critical)
return "release_alert";
return "chat";
}
function getVolumeIcon(volume: real, isMuted: bool): string {
if (isMuted)
return "no_sound";
if (volume >= 0.5)
return "volume_up";
if (volume > 0)
return "volume_down";
return "volume_mute";
}
function getMicVolumeIcon(volume: real, isMuted: bool): string {
if (!isMuted && volume > 0)
return "mic";
return "mic_off";
}
}
+56
View File
@@ -0,0 +1,56 @@
pragma Singleton
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
Singleton {
id: root
property alias enabled: props.enabled
readonly property alias enabledSince: props.enabledSince
onEnabledChanged: {
if (enabled)
props.enabledSince = new Date();
}
PersistentProperties {
id: props
property bool enabled
property date enabledSince
reloadableId: "idleInhibitor"
}
IdleInhibitor {
enabled: props.enabled
window: PanelWindow {
implicitWidth: 0
implicitHeight: 0
color: "transparent"
mask: Region {}
}
}
IpcHandler {
target: "idleInhibitor"
function isEnabled(): bool {
return props.enabled;
}
function toggle(): void {
props.enabled = !props.enabled;
}
function enable(): void {
props.enabled = true;
}
function disable(): void {
props.enabled = false;
}
}
}
+6
View File
@@ -9,6 +9,12 @@ Singleton {
readonly property int minutes: clock.minutes readonly property int minutes: clock.minutes
readonly property int seconds: clock.seconds readonly property int seconds: clock.seconds
readonly property string timeStr: format("hh:mm")
readonly property list<string> timeComponents: timeStr.split(":")
readonly property string hourStr: timeComponents[0] ?? ""
readonly property string minuteStr: timeComponents[1] ?? ""
readonly property string amPmStr: timeComponents[2] ?? ""
function format(fmt: string): string { function format(fmt: string): string {
return Qt.formatDateTime(clock.date, fmt); return Qt.formatDateTime(clock.date, fmt);
} }
+16
View File
@@ -0,0 +1,16 @@
pragma Singleton
import Quickshell
Singleton {
property var screens: new Map()
property var bars: new Map()
function load(screen: ShellScreen, visibilities: var): void {
screens.set(Hypr.monitorFor(screen), visibilities);
}
function getForActive(): PersistentProperties {
return screens.get(Hypr.focusedMonitor);
}
}
+204
View File
@@ -0,0 +1,204 @@
pragma Singleton
import qs.Config
import Quickshell
import QtQuick
Singleton {
id: root
property string city
property string loc
property var cc
property list<var> forecast
property list<var> hourlyForecast
readonly property string icon: cc ? Icons.getWeatherIcon(cc.weatherCode) : "cloud_alert"
readonly property string description: cc?.weatherDesc ?? qsTr("No weather")
readonly property string temp: `${cc?.tempC ?? 0}°C`
readonly property string feelsLike: `${cc?.feelsLikeC ?? 0}°C`
readonly property int humidity: cc?.humidity ?? 0
readonly property real windSpeed: cc?.windSpeed ?? 0
readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), "h:mm") : "--:--"
readonly property string sunset: cc ? Qt.formatDateTime(new Date(cc.sunset), "h:mm") : "--:--"
readonly property var cachedCities: new Map()
function reload(): void {
const configLocation = Config.services.weatherLocation;
if (configLocation) {
if (configLocation.indexOf(",") !== -1 && !isNaN(parseFloat(configLocation.split(",")[0]))) {
loc = configLocation;
fetchCityFromCoords(configLocation);
} else {
fetchCoordsFromCity(configLocation);
}
} else if (!loc || timer.elapsed() > 900) {
Requests.get("https://ipinfo.io/json", text => {
const response = JSON.parse(text);
if (response.loc) {
loc = response.loc;
city = response.city ?? "";
timer.restart();
}
});
}
}
function fetchCityFromCoords(coords: string): void {
if (cachedCities.has(coords)) {
city = cachedCities.get(coords);
return;
}
const [lat, lon] = coords.split(",");
const url = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=geocodejson`;
Requests.get(url, text => {
const geo = JSON.parse(text).features?.[0]?.properties.geocoding;
if (geo) {
const geoCity = geo.type === "city" ? geo.name : geo.city;
city = geoCity;
cachedCities.set(coords, geoCity);
} else {
city = "Unknown City";
}
});
}
function fetchCoordsFromCity(cityName: string): void {
const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(cityName)}&count=1&language=en&format=json`;
Requests.get(url, text => {
const json = JSON.parse(text);
if (json.results && json.results.length > 0) {
const result = json.results[0];
loc = result.latitude + "," + result.longitude;
city = result.name;
} else {
loc = "";
reload();
}
});
}
function fetchWeatherData(): void {
const url = getWeatherUrl();
if (url === "")
return;
Requests.get(url, text => {
const json = JSON.parse(text);
if (!json.current || !json.daily)
return;
cc = {
weatherCode: json.current.weather_code,
weatherDesc: getWeatherCondition(json.current.weather_code),
tempC: Math.round(json.current.temperature_2m),
tempF: Math.round(toFahrenheit(json.current.temperature_2m)),
feelsLikeC: Math.round(json.current.apparent_temperature),
feelsLikeF: Math.round(toFahrenheit(json.current.apparent_temperature)),
humidity: json.current.relative_humidity_2m,
windSpeed: json.current.wind_speed_10m,
isDay: json.current.is_day,
sunrise: json.daily.sunrise[0],
sunset: json.daily.sunset[0]
};
const forecastList = [];
for (let i = 0; i < json.daily.time.length; i++)
forecastList.push({
date: json.daily.time[i],
maxTempC: Math.round(json.daily.temperature_2m_max[i]),
maxTempF: Math.round(toFahrenheit(json.daily.temperature_2m_max[i])),
minTempC: Math.round(json.daily.temperature_2m_min[i]),
minTempF: Math.round(toFahrenheit(json.daily.temperature_2m_min[i])),
weatherCode: json.daily.weather_code[i],
icon: Icons.getWeatherIcon(json.daily.weather_code[i])
});
forecast = forecastList;
const hourlyList = [];
const now = new Date();
for (let i = 0; i < json.hourly.time.length; i++) {
const time = new Date(json.hourly.time[i]);
if (time < now)
continue;
hourlyList.push({
timestamp: json.hourly.time[i],
hour: time.getHours(),
tempC: Math.round(json.hourly.temperature_2m[i]),
tempF: Math.round(toFahrenheit(json.hourly.temperature_2m[i])),
weatherCode: json.hourly.weather_code[i],
icon: Icons.getWeatherIcon(json.hourly.weather_code[i])
});
}
hourlyForecast = hourlyList;
});
}
function toFahrenheit(celcius: real): real {
return celcius * 9 / 5 + 32;
}
function getWeatherUrl(): string {
if (!loc || loc.indexOf(",") === -1)
return "";
const [lat, lon] = loc.split(",");
const baseUrl = "https://api.open-meteo.com/v1/forecast";
const params = ["latitude=" + lat, "longitude=" + lon, "hourly=weather_code,temperature_2m", "daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset", "current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,weather_code,wind_speed_10m", "timezone=auto", "forecast_days=7"];
return baseUrl + "?" + params.join("&");
}
function getWeatherCondition(code: string): string {
const conditions = {
"0": "Clear",
"1": "Clear",
"2": "Partly cloudy",
"3": "Overcast",
"45": "Fog",
"48": "Fog",
"51": "Drizzle",
"53": "Drizzle",
"55": "Drizzle",
"56": "Freezing drizzle",
"57": "Freezing drizzle",
"61": "Light rain",
"63": "Rain",
"65": "Heavy rain",
"66": "Light rain",
"67": "Heavy rain",
"71": "Light snow",
"73": "Snow",
"75": "Heavy snow",
"77": "Snow",
"80": "Light rain",
"81": "Rain",
"82": "Heavy rain",
"85": "Light snow showers",
"86": "Heavy snow showers",
"95": "Thunderstorm",
"96": "Thunderstorm with hail",
"99": "Thunderstorm with hail"
};
return conditions[code] || "Unknown";
}
onLocChanged: fetchWeatherData()
// Refresh current location hourly
Timer {
interval: 3600000 // 1 hour
running: true
repeat: true
onTriggered: fetchWeatherData()
}
ElapsedTimer {
id: timer
}
}
+11 -1
View File
@@ -16,6 +16,7 @@ RowLayout {
readonly property int vPadding: 6 readonly property int vPadding: 6
required property Wrapper popouts required property Wrapper popouts
required property PersistentProperties visibilities
required property PanelWindow bar required property PanelWindow bar
function checkPopout(x: real): void { function checkPopout(x: real): void {
@@ -26,6 +27,9 @@ RowLayout {
return; return;
} }
if ( visibilities.sidebar )
return;
const id = ch.id; const id = ch.id;
const top = ch.x; const top = ch.x;
const item = ch.item; const item = ch.item;
@@ -56,6 +60,10 @@ RowLayout {
popouts.currentName = "calendar"; popouts.currentName = "calendar";
popouts.currentCenter = Qt.binding( () => item.mapToItem( root, itemWidth / 2, 0 ).x ); popouts.currentCenter = Qt.binding( () => item.mapToItem( root, itemWidth / 2, 0 ).x );
popouts.hasCurrent = true; 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 { DelegateChoice {
roleValue: "notifBell" roleValue: "notifBell"
delegate: WrappedLoader { delegate: WrappedLoader {
sourceComponent: NotifBell {} sourceComponent: NotifBell {
visibilities: root.visibilities
}
} }
} }
DelegateChoice { DelegateChoice {
+9
View File
@@ -7,6 +7,7 @@ import qs.Config
import qs.Modules.Calendar import qs.Modules.Calendar
import qs.Modules.WSOverview import qs.Modules.WSOverview
import qs.Modules.Polkit import qs.Modules.Polkit
import qs.Modules.Dashboard
Item { Item {
id: root id: root
@@ -88,6 +89,14 @@ Item {
screen: root.wrapper.screen screen: root.wrapper.screen
} }
} }
Popout {
name: "dash"
sourceComponent: Dashboard {
wrapper: root.wrapper
}
}
} }
component Popout: Loader { 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 Quickshell.Hyprland
import QtQuick import QtQuick
import qs.Config import qs.Config
@@ -7,6 +8,8 @@ import qs.Components
Item { Item {
id: root id: root
required property PersistentProperties visibilities
implicitWidth: 20 implicitWidth: 20
anchors.top: parent.top anchors.top: parent.top
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
@@ -33,7 +36,8 @@ Item {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
Hyprland.dispatch("global zshell-nc:toggle-nc"); // Hyprland.dispatch("global zshell-nc:toggle-nc");
root.visibilities.sidebar = !root.visibilities.sidebar;
} }
} }
} }
+8 -13
View File
@@ -11,7 +11,11 @@ import qs.Helpers
import qs.Daemons import qs.Daemons
import qs.Effects import qs.Effects
PanelWindow { Scope {
Variants {
model: Quickshell.screens
PanelWindow {
id: root id: root
color: "transparent" color: "transparent"
anchors { anchors {
@@ -30,21 +34,10 @@ PanelWindow {
mask: Region { item: backgroundRect } mask: Region { item: backgroundRect }
Connections {
target: Hypr
function onFocusedMonitorChanged(): void {
if ( !root.centerShown ) {
root.screen = Hypr.getActiveScreen();
}
}
}
GlobalShortcut { GlobalShortcut {
appid: "zshell-nc" appid: "zshell-nc"
name: "toggle-nc" name: "toggle-nc"
onPressed: { onPressed: {
root.screen = Hypr.getActiveScreen();
root.centerShown = !root.centerShown; root.centerShown = !root.centerShown;
} }
} }
@@ -59,7 +52,7 @@ PanelWindow {
if ( !root.centerShown ) { if ( !root.centerShown ) {
closeAnimation.start(); closeAnimation.start();
closeTimer.start(); closeTimer.start();
} else { } else if ( Hypr.getActiveScreen() === root.screen ) {
root.visible = true; root.visible = true;
} }
} }
@@ -182,4 +175,6 @@ PanelWindow {
} }
} }
} }
}
}
} }
+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
}
}
+21 -4
View File
@@ -89,15 +89,31 @@ Scope {
id: contentRow id: contentRow
spacing: 24 spacing: 24
Item {
Layout.preferredWidth: icon.implicitSize
Layout.preferredHeight: icon.implicitSize
Layout.leftMargin: 16
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
IconImage { IconImage {
id: icon
anchors.fill: parent
visible: `${source}`.includes("://")
source: Quickshell.iconPath(polkitAgent.flow?.iconName, true) ?? "" source: Quickshell.iconPath(polkitAgent.flow?.iconName, true) ?? ""
implicitSize: 64 implicitSize: 64
mipmap: true mipmap: true
}
Layout.preferredWidth: implicitSize MaterialIcon {
Layout.preferredHeight: implicitSize visible: !icon.visible
Layout.leftMargin: 16
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter text: "security"
anchors.fill: parent
font.pointSize: 64
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
} }
ColumnLayout { ColumnLayout {
@@ -277,6 +293,7 @@ Scope {
Layout.alignment: Qt.AlignRight Layout.alignment: Qt.AlignRight
onClicked: { onClicked: {
root.shouldShow = false root.shouldShow = false
console.log(icon.source, icon.visible)
polkitAgent.flow.cancelAuthenticationRequest() polkitAgent.flow.cancelAuthenticationRequest()
passInput.text = "" passInput.text = ""
} }
+15 -13
View File
@@ -4,6 +4,7 @@ import Quickshell.Wayland
import Quickshell.Hyprland import Quickshell.Hyprland
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick import QtQuick
import qs.Components
import qs.Config import qs.Config
import qs.Daemons import qs.Daemons
import qs.Helpers import qs.Helpers
@@ -122,7 +123,7 @@ PanelWindow {
radius: backgroundRect.radius radius: backgroundRect.radius
} }
Rectangle { CustomClippingRect {
id: backgroundRect id: backgroundRect
implicitWidth: 400 implicitWidth: 400
implicitHeight: contentLayout.childrenRect.height + 16 implicitHeight: contentLayout.childrenRect.height + 16
@@ -131,6 +132,19 @@ PanelWindow {
border.color: "#555555" border.color: "#555555"
radius: 8 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: { Component.onCompleted: {
root.notifRegions.push( notifRegion.createObject(root, { item: backgroundRect })); root.notifRegions.push( notifRegion.createObject(root, { item: backgroundRect }));
} }
@@ -228,18 +242,6 @@ PanelWindow {
ElapsedTimer { ElapsedTimer {
id: timer id: timer
} }
}
MouseArea {
z: 1
anchors.fill: parent
hoverEnabled: true
onEntered: {
rootItem.modelData.timer.stop();
}
onExited: {
rootItem.modelData.timer.start();
}
} }
} }
} }
+4
View File
@@ -28,12 +28,16 @@ Item {
if ( mouse.button === Qt.LeftButton ) { if ( mouse.button === Qt.LeftButton ) {
root.item.activate(); root.item.activate();
} else if ( mouse.button === Qt.RightButton ) { } else if ( mouse.button === Qt.RightButton ) {
if ( visibilities.sidebar ) {
return;
} else {
root.popouts.currentName = `traymenu${ root.ind }`; root.popouts.currentName = `traymenu${ root.ind }`;
root.popouts.currentCenter = Qt.binding( () => root.mapToItem( root.loader, root.implicitWidth / 2, 0 ).x ); root.popouts.currentCenter = Qt.binding( () => root.mapToItem( root.loader, root.implicitWidth / 2, 0 ).x );
root.popouts.hasCurrent = true; root.popouts.hasCurrent = true;
} }
} }
} }
}
Image { Image {
id: icon id: icon
+19
View File
@@ -0,0 +1,19 @@
#version 440
layout(location = 0) in vec2 qt_TexCoord0;
layout(location = 0) out vec4 fragColor;
layout(std140, binding = 0) uniform buf {
// qt_Matrix and qt_Opacity must always be both present
// if the built-in vertex shader is used.
mat4 qt_Matrix;
float qt_Opacity;
};
layout(binding = 1) uniform sampler2D source;
layout(binding = 2) uniform sampler2D maskSource;
void main() {
fragColor = texture(source, qt_TexCoord0.st) *
(texture(maskSource, qt_TexCoord0.st).a) * qt_Opacity;
}
+6 -6
View File
@@ -3,7 +3,7 @@
//@ pragma Env QS_NO_RELOAD_POPUP=1 //@ pragma Env QS_NO_RELOAD_POPUP=1
import Quickshell import Quickshell
import qs.Modules import qs.Modules
import qs.Modules.Lock import qs.Modules.Lock as Lock
import qs.Helpers import qs.Helpers
import qs.Modules.Polkit import qs.Modules.Polkit
@@ -12,17 +12,17 @@ Scope {
Wallpaper {} Wallpaper {}
Launcher {} Launcher {}
AreaPicker {} AreaPicker {}
Lock { Lock.Lock {
id: lock id: lock
} }
IdleInhibitor { Lock.IdleInhibitor {
lock: lock lock: lock
} }
NotificationCenter { // NotificationCenter {
//
} // }
Polkit {} Polkit {}
} }