- Vertical bar on DP-2 with rounded-square pills throughout - Per-monitor workspace groups sorted by screen x position, with Nerd Font icons for named workspaces and apex-neon red active indicator - Bar layout: datetime+weather top, workspaces centered, gamemode+media+notif+system bottom - Popouts anchor to triggering icon (top-right for datetime/weather, bottom-right for media/notif/system) - Lock command switched from hyprlock to swaylock - Hyprland blur/ignore_alpha layerrules for quickshell namespace Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
353 lines
14 KiB
QML
353 lines
14 KiB
QML
import Quickshell
|
|
import Quickshell.Services.Notifications
|
|
import Quickshell.Widgets
|
|
import QtQuick
|
|
import QtQuick.Layouts
|
|
import "../../shared" as Shared
|
|
|
|
Item {
|
|
id: root
|
|
|
|
required property var trackedNotifications
|
|
required property var daemon
|
|
|
|
function relativeTime(id) {
|
|
let ts = root.daemon?.timestamps?.[id];
|
|
if (!ts) return "";
|
|
let diff = Math.floor((Date.now() - ts) / 1000);
|
|
if (diff < 60) return "now";
|
|
if (diff < 3600) return Math.floor(diff / 60) + "m";
|
|
if (diff < 86400) return Math.floor(diff / 3600) + "h";
|
|
return Math.floor(diff / 86400) + "d";
|
|
}
|
|
|
|
implicitWidth: Shared.Theme.popoutWidth
|
|
implicitHeight: Math.min(600, col.implicitHeight + Shared.Theme.popoutPadding * 2)
|
|
|
|
PopoutBackground { anchors.fill: parent }
|
|
MouseArea { anchors.fill: parent }
|
|
|
|
// Drives relative timestamp re-evaluation
|
|
Timer { id: tsRefresh; property int tick: 0; interval: 30000; running: true; repeat: true; onTriggered: tick++ }
|
|
|
|
// DnD auto-off timer
|
|
Timer {
|
|
id: dndTimer
|
|
property int remaining: 0 // seconds, 0 = indefinite
|
|
interval: 1000
|
|
running: false
|
|
repeat: true
|
|
onTriggered: {
|
|
remaining--;
|
|
if (remaining <= 0) {
|
|
running = false;
|
|
if (root.daemon) root.daemon.dnd = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
id: col
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
anchors.margins: Shared.Theme.popoutPadding
|
|
spacing: 8
|
|
|
|
// Header
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 8
|
|
|
|
Text {
|
|
text: "Notifications"
|
|
color: Shared.Theme.text
|
|
font.pixelSize: Shared.Theme.fontLarge
|
|
font.family: Shared.Theme.fontFamily
|
|
font.bold: true
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
// DnD toggle
|
|
Rectangle {
|
|
implicitWidth: dndRow.implicitWidth + 12
|
|
implicitHeight: 24
|
|
radius: 12
|
|
color: root.daemon?.dnd ? Qt.alpha(Shared.Theme.danger, Shared.Theme.opacityLight) : (dndMouse.containsMouse ? Shared.Theme.surface1 : Shared.Theme.surface0)
|
|
border.width: root.daemon?.dnd ? 1 : 0
|
|
border.color: Qt.alpha(Shared.Theme.danger, Shared.Theme.opacityMedium)
|
|
Behavior on color { ColorAnimation { duration: 100 } }
|
|
|
|
RowLayout {
|
|
id: dndRow
|
|
anchors.centerIn: parent
|
|
spacing: 4
|
|
Text { text: root.daemon?.dnd ? "\u{f009b}" : "\u{f009a}"; color: root.daemon?.dnd ? Shared.Theme.danger : Shared.Theme.overlay0; font.pixelSize: 12; font.family: Shared.Theme.iconFont }
|
|
Text { text: "DnD"; color: root.daemon?.dnd ? Shared.Theme.danger : Shared.Theme.overlay0; font.pixelSize: Shared.Theme.fontSmall; font.family: Shared.Theme.fontFamily }
|
|
}
|
|
|
|
MouseArea {
|
|
id: dndMouse
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
onClicked: { if (root.daemon) root.daemon.dnd = !root.daemon.dnd; }
|
|
}
|
|
}
|
|
|
|
// Clear all
|
|
Rectangle {
|
|
visible: root.trackedNotifications.values.length > 0
|
|
implicitWidth: clearRow.implicitWidth + 12
|
|
implicitHeight: 24
|
|
radius: 12
|
|
color: clearMouse.containsMouse ? Shared.Theme.surface1 : Shared.Theme.surface0
|
|
Behavior on color { ColorAnimation { duration: 100 } }
|
|
|
|
RowLayout {
|
|
id: clearRow
|
|
anchors.centerIn: parent
|
|
spacing: 4
|
|
Text { text: "Clear all"; color: Shared.Theme.overlay0; font.pixelSize: Shared.Theme.fontSmall; font.family: Shared.Theme.fontFamily }
|
|
}
|
|
|
|
MouseArea {
|
|
id: clearMouse
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
onClicked: {
|
|
let tracked = root.trackedNotifications;
|
|
for (let i = tracked.values.length - 1; i >= 0; i--)
|
|
tracked.values[i].dismiss();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// DnD schedule (visible when DnD is active)
|
|
Loader {
|
|
Layout.fillWidth: true
|
|
active: root.daemon?.dnd === true
|
|
visible: active
|
|
sourceComponent: RowLayout {
|
|
spacing: 4
|
|
Repeater {
|
|
model: [
|
|
{ label: "30m", mins: 30 },
|
|
{ label: "1h", mins: 60 },
|
|
{ label: "2h", mins: 120 },
|
|
{ label: "\u{f0026}", mins: 0 } // infinity = until manual off
|
|
]
|
|
|
|
Rectangle {
|
|
required property var modelData
|
|
required property int index
|
|
readonly property bool isActive: {
|
|
if (modelData.mins === 0) return dndTimer.remaining <= 0;
|
|
return dndTimer.remaining > 0 && dndTimer.remaining <= modelData.mins * 60;
|
|
}
|
|
Layout.fillWidth: true
|
|
implicitHeight: 22
|
|
radius: 11
|
|
color: isActive ? Qt.alpha(Shared.Theme.danger, Shared.Theme.opacityLight) : (schedMouse.containsMouse ? Shared.Theme.surface1 : Shared.Theme.surface0)
|
|
Behavior on color { ColorAnimation { duration: 100 } }
|
|
|
|
Text {
|
|
anchors.centerIn: parent
|
|
text: parent.modelData.label
|
|
color: parent.isActive ? Shared.Theme.danger : Shared.Theme.overlay0
|
|
font.pixelSize: Shared.Theme.fontSmall
|
|
font.family: Shared.Theme.fontFamily
|
|
}
|
|
|
|
MouseArea {
|
|
id: schedMouse
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
onClicked: {
|
|
if (parent.modelData.mins === 0) {
|
|
dndTimer.remaining = 0; // indefinite
|
|
} else {
|
|
dndTimer.remaining = parent.modelData.mins * 60;
|
|
dndTimer.running = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle { Layout.fillWidth: true; height: 1; color: Shared.Theme.surface0 }
|
|
|
|
// Empty state
|
|
Text {
|
|
visible: root.trackedNotifications.values.length === 0
|
|
text: "No notifications"
|
|
color: Shared.Theme.overlay0
|
|
font.pixelSize: Shared.Theme.fontSize
|
|
font.family: Shared.Theme.fontFamily
|
|
Layout.alignment: Qt.AlignHCenter
|
|
Layout.topMargin: 20
|
|
Layout.bottomMargin: 20
|
|
}
|
|
|
|
// Notification list
|
|
Flickable {
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: Math.min(480, notifList.implicitHeight)
|
|
contentHeight: notifList.implicitHeight
|
|
clip: true
|
|
visible: root.trackedNotifications.values.length > 0
|
|
|
|
ColumnLayout {
|
|
id: notifList
|
|
width: parent.width
|
|
spacing: 6
|
|
|
|
Repeater {
|
|
model: root.trackedNotifications
|
|
|
|
Rectangle {
|
|
id: notifCard
|
|
required property var modelData
|
|
|
|
Layout.fillWidth: true
|
|
implicitHeight: notifContent.implicitHeight + 16
|
|
radius: Shared.Theme.radiusSmall
|
|
color: notifMouse.containsMouse ? Shared.Theme.surface1 : Shared.Theme.surface0
|
|
border.width: modelData.urgency === NotificationUrgency.Critical ? 1 : 0
|
|
border.color: Shared.Theme.danger
|
|
|
|
Behavior on color { ColorAnimation { duration: 100 } }
|
|
|
|
MouseArea {
|
|
id: notifMouse
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
onClicked: {
|
|
let actions = notifCard.modelData.actions;
|
|
if (actions.length > 0)
|
|
actions[0].invoke();
|
|
else
|
|
notifCard.modelData.dismiss();
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
id: notifContent
|
|
anchors.fill: parent
|
|
anchors.margins: 8
|
|
spacing: 4
|
|
|
|
// App name + close
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 6
|
|
|
|
IconImage {
|
|
source: notifCard.modelData.appIcon
|
|
implicitSize: 14
|
|
visible: notifCard.modelData.appIcon !== ""
|
|
}
|
|
|
|
Text {
|
|
text: notifCard.modelData.appName || "App"
|
|
color: Shared.Theme.overlay0
|
|
font.pixelSize: Shared.Theme.fontSmall
|
|
font.family: Shared.Theme.fontFamily
|
|
Layout.fillWidth: true
|
|
elide: Text.ElideRight
|
|
}
|
|
|
|
Text {
|
|
property int tick: tsRefresh.tick
|
|
text: root.relativeTime(notifCard.modelData.id)
|
|
color: Shared.Theme.surface2
|
|
font.pixelSize: Shared.Theme.fontSmall
|
|
font.family: Shared.Theme.fontFamily
|
|
visible: text !== ""
|
|
}
|
|
|
|
Text {
|
|
text: "\u{f0156}"
|
|
color: dismissMouse.containsMouse ? Shared.Theme.danger : Shared.Theme.overlay0
|
|
font.pixelSize: 12
|
|
font.family: Shared.Theme.iconFont
|
|
MouseArea {
|
|
id: dismissMouse
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
onClicked: notifCard.modelData.dismiss()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Summary
|
|
Text {
|
|
visible: text !== ""
|
|
text: notifCard.modelData.summary
|
|
color: Shared.Theme.text
|
|
font.pixelSize: Shared.Theme.fontSize
|
|
font.family: Shared.Theme.fontFamily
|
|
font.bold: true
|
|
Layout.fillWidth: true
|
|
wrapMode: Text.WordWrap
|
|
maximumLineCount: 2
|
|
elide: Text.ElideRight
|
|
}
|
|
|
|
// Body
|
|
Text {
|
|
visible: text !== ""
|
|
text: notifCard.modelData.body
|
|
textFormat: Text.PlainText
|
|
color: Shared.Theme.subtext0
|
|
font.pixelSize: Shared.Theme.fontSmall
|
|
font.family: Shared.Theme.fontFamily
|
|
Layout.fillWidth: true
|
|
wrapMode: Text.WordWrap
|
|
maximumLineCount: 3
|
|
elide: Text.ElideRight
|
|
}
|
|
|
|
// Actions
|
|
RowLayout {
|
|
visible: notifCard.modelData.actions.length > 0
|
|
Layout.fillWidth: true
|
|
spacing: 4
|
|
|
|
Repeater {
|
|
model: notifCard.modelData.actions
|
|
|
|
Rectangle {
|
|
required property var modelData
|
|
Layout.fillWidth: true
|
|
implicitHeight: 24
|
|
radius: 4
|
|
color: actMouse.containsMouse ? Shared.Theme.surface2 : Shared.Theme.surface1
|
|
|
|
Text {
|
|
anchors.centerIn: parent
|
|
text: modelData.text
|
|
color: Shared.Theme.text
|
|
font.pixelSize: Shared.Theme.fontSmall
|
|
font.family: Shared.Theme.fontFamily
|
|
}
|
|
|
|
MouseArea {
|
|
id: actMouse
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
onClicked: modelData.invoke()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|