Add Ongoing to the Panama Daybook
This commit is contained in:
@@ -20,7 +20,8 @@ Last live audit: 2026-08-17, Fedora 44, Hyprland 0.56.2, Quickshell 0.3.0.
|
|||||||
| Panel | Workspaces, CPU/RAM/GPU, clock, weather, quiet event capsule, media, clipboard, tray, status | Live |
|
| Panel | Workspaces, CPU/RAM/GPU, clock, weather, quiet event capsule, media, clipboard, tray, status | Live |
|
||||||
| Clipboard history | Vicinae owns capture/storage; Quickshell provides the panel | Live |
|
| Clipboard history | Vicinae owns capture/storage; Quickshell provides the panel | Live |
|
||||||
| Notifications | Quickshell server, toasts, grouped history, DND, clear actions | Live |
|
| Notifications | Quickshell server, toasts, grouped history, DND, clear actions | Live |
|
||||||
| Date menu | Two-column Daybook with EDS agenda, notifications, month calendar, weather, and media | Live |
|
| Date menu | Two-column Daybook with Agenda, Ongoing activity, Notifications, month calendar, weather, and media | Live |
|
||||||
|
| Ongoing activity | Focus, Caffeine, recording, screen sharing, camera, and microphone state with direct controls | Live |
|
||||||
| Calendar agenda | Existing GNOME, Google, iCloud, and Nextcloud sources through Evolution Data Server | Live |
|
| Calendar agenda | Existing GNOME, Google, iCloud, and Nextcloud sources through Evolution Data Server | Live |
|
||||||
| Quick settings | Wi-Fi, Bluetooth, audio input/output, caffeine, night light, focus | Live |
|
| Quick settings | Wi-Fi, Bluetooth, audio input/output, caffeine, night light, focus | Live |
|
||||||
| Focus sessions | Persistent workspace-bound timer, DND/caffeine coordination, Signal Glass controls | Live |
|
| Focus sessions | Persistent workspace-bound timer, DND/caffeine coordination, Signal Glass controls | Live |
|
||||||
|
|||||||
@@ -191,8 +191,16 @@ Settings remains searchable in Vicinae for hardware and account panels.
|
|||||||
|
|
||||||
Click the clock to open the two-column Daybook on **Agenda**. The month grid,
|
Click the clock to open the two-column Daybook on **Agenda**. The month grid,
|
||||||
today's schedule, Up Next, weather, and conditional media controls stay visible
|
today's schedule, Up Next, weather, and conditional media controls stay visible
|
||||||
together. `SUPER + B` opens **Notifications** in that same surface; returning to
|
together. Its right pane has three first-class pages:
|
||||||
Agenda preserves the selected date and visible month.
|
|
||||||
|
- **Agenda** — the selected day's calendar.
|
||||||
|
- **Ongoing** — Focus, Caffeine, recording, screen sharing, camera, and
|
||||||
|
microphone activity, visible until each state ends.
|
||||||
|
- **Notifications** — grouped history, Do Not Disturb, and clear actions.
|
||||||
|
|
||||||
|
`SUPER + B` opens Notifications directly. Switching pages preserves the
|
||||||
|
selected date and visible month. Brief completed events such as screenshots and
|
||||||
|
device changes remain in Signal Glass instead of accumulating in Ongoing.
|
||||||
|
|
||||||
Calendar data comes from Evolution Data Server, so the Google, iCloud,
|
Calendar data comes from Evolution Data Server, so the Google, iCloud,
|
||||||
Nextcloud, and local calendars already configured for GNOME remain the source
|
Nextcloud, and local calendars already configured for GNOME remain the source
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import qs.modules.quicksettings
|
|||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
signal requestNotifications
|
|
||||||
|
|
||||||
readonly property bool isToday: CalendarAgenda.selectedDateKey === CalendarAgenda.dateKey(clock.date)
|
readonly property bool isToday: CalendarAgenda.selectedDateKey === CalendarAgenda.dateKey(clock.date)
|
||||||
readonly property var upNext: CalendarAgenda.nextEventForSelectedDate
|
readonly property var upNext: CalendarAgenda.nextEventForSelectedDate
|
||||||
readonly property var listedEvents: CalendarAgenda.selectedEvents.filter(event => !root.upNext || event.id !== root.upNext.id)
|
readonly property var listedEvents: CalendarAgenda.selectedEvents.filter(event => !root.upNext || event.id !== root.upNext.id)
|
||||||
@@ -238,10 +236,6 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationSummary {
|
|
||||||
width: parent.width
|
|
||||||
onActivated: root.requestNotifications()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function relativeStart(event: var): string {
|
function relativeStart(event: var): string {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// GNOME's date/time menu: the calendar and the message tray in one panel,
|
// GNOME's date/time menu: calendar, ongoing activity and messages in one panel,
|
||||||
// dropping from the top centre because that is where GNOME put it and the
|
// dropping from the top centre because that is where GNOME put it and the
|
||||||
// muscle memory is the whole point.
|
// muscle memory is the whole point.
|
||||||
//
|
//
|
||||||
@@ -20,8 +20,10 @@ import qs.widgets
|
|||||||
PanelWindow {
|
PanelWindow {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
// "notifications" is the existing overlay key, so Super+B, the notification
|
readonly property bool ongoingContentFits: panel.ongoingContentFits
|
||||||
// IPC target and the clock all land here.
|
|
||||||
|
// "notifications" remains the overlay key so existing keybinds keep their
|
||||||
|
// contract; the surface itself now has Agenda, Ongoing and Notifications.
|
||||||
visible: ShellState.notificationsOpen
|
visible: ShellState.notificationsOpen
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// The approved A · Daybook: month context on the left, the selected day's
|
// The approved A · Daybook: month context on the left, the selected day's
|
||||||
// schedule or grouped notification history on the right.
|
// schedule, persistent activity or grouped notification history on the right.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
@@ -46,7 +46,7 @@ Item {
|
|||||||
Text {
|
Text {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.leftMargin: 4
|
anchors.leftMargin: 4
|
||||||
anchors.right: headerActions.left
|
anchors.right: dateMenuTabs.left
|
||||||
anchors.rightMargin: Theme.itemSpacing
|
anchors.rightMargin: Theme.itemSpacing
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: Qt.formatDateTime(clock.date, "dddd, MMMM d")
|
text: Qt.formatDateTime(clock.date, "dddd, MMMM d")
|
||||||
@@ -59,35 +59,36 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: headerActions
|
id: dateMenuTabs
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
spacing: 6
|
spacing: 6
|
||||||
|
|
||||||
HeaderPill {
|
HeaderPill {
|
||||||
visible: ShellState.dateMenuPage === "notifications"
|
text: "Agenda"
|
||||||
text: "← Back to agenda"
|
active: ShellState.dateMenuPage === "agenda"
|
||||||
onClicked: ShellState.dateMenuPage = "agenda"
|
onClicked: ShellState.dateMenuPage = "agenda"
|
||||||
}
|
}
|
||||||
|
|
||||||
HeaderPill {
|
HeaderPill {
|
||||||
text: "Do Not Disturb"
|
text: "Ongoing"
|
||||||
active: Notifs.doNotDisturb
|
active: ShellState.dateMenuPage === "ongoing"
|
||||||
icon: "notifications-disabled-symbolic"
|
badge: Ongoing.count
|
||||||
onClicked: Notifs.doNotDisturb = !Notifs.doNotDisturb
|
onClicked: ShellState.dateMenuPage = "ongoing"
|
||||||
|
}
|
||||||
|
|
||||||
|
HeaderPill {
|
||||||
|
text: "Notifications"
|
||||||
|
active: ShellState.dateMenuPage === "notifications"
|
||||||
|
badge: Notifs.history.length
|
||||||
|
onClicked: ShellState.dateMenuPage = "notifications"
|
||||||
}
|
}
|
||||||
|
|
||||||
HeaderPill {
|
HeaderPill {
|
||||||
visible: ShellState.dateMenuPage === "agenda"
|
visible: ShellState.dateMenuPage === "agenda"
|
||||||
text: "Open Calendar ↗"
|
text: "Calendar ↗"
|
||||||
onClicked: CalendarAgenda.openCalendar(CalendarAgenda.selectedDate)
|
onClicked: CalendarAgenda.openCalendar(CalendarAgenda.selectedDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
HeaderPill {
|
|
||||||
visible: ShellState.dateMenuPage === "notifications" && Notifs.hasNotifications
|
|
||||||
text: "Clear all"
|
|
||||||
onClicked: Notifs.dismissAll()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,10 +166,16 @@ Item {
|
|||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.margins: 14
|
anchors.margins: 14
|
||||||
visible: ShellState.dateMenuPage === "agenda"
|
visible: ShellState.dateMenuPage === "agenda"
|
||||||
onRequestNotifications: {
|
}
|
||||||
ShellState.dateMenuPage = "notifications";
|
|
||||||
Notifs.markAllRead();
|
OngoingPane {
|
||||||
}
|
id: ongoingPane
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.margins: 14
|
||||||
|
maxHeight: body.height - 28
|
||||||
|
visible: ShellState.dateMenuPage === "ongoing"
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
@@ -186,6 +193,8 @@ Item {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
anchors.right: notificationActions.left
|
||||||
|
anchors.rightMargin: 8
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: "Notifications"
|
text: "Notifications"
|
||||||
color: Theme.fg
|
color: Theme.fg
|
||||||
@@ -194,14 +203,24 @@ Item {
|
|||||||
font.weight: Font.DemiBold
|
font.weight: Font.DemiBold
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Row {
|
||||||
|
id: notificationActions
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: Notifs.history.length + (Notifs.history.length === 1 ? " item" : " items")
|
spacing: 6
|
||||||
color: Theme.fgDim
|
|
||||||
font.family: Theme.fontFamily
|
HeaderPill {
|
||||||
font.features: Theme.tabularFigures
|
text: "Do Not Disturb"
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
active: Notifs.doNotDisturb
|
||||||
|
icon: "notifications-disabled-symbolic"
|
||||||
|
onClicked: Notifs.doNotDisturb = !Notifs.doNotDisturb
|
||||||
|
}
|
||||||
|
|
||||||
|
HeaderPill {
|
||||||
|
visible: Notifs.hasNotifications
|
||||||
|
text: "Clear all"
|
||||||
|
onClicked: Notifs.dismissAll()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,9 +234,14 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property real rightContentHeight: ShellState.dateMenuPage === "notifications"
|
readonly property real rightContentHeight: {
|
||||||
? notificationPane.implicitHeight
|
if (ShellState.dateMenuPage === "notifications")
|
||||||
: agenda.implicitHeight
|
return notificationPane.implicitHeight;
|
||||||
|
if (ShellState.dateMenuPage === "ongoing")
|
||||||
|
return ongoingPane.implicitHeight;
|
||||||
|
return agenda.implicitHeight;
|
||||||
|
}
|
||||||
|
readonly property bool ongoingContentFits: ongoingPane.implicitHeight <= body.height - 28
|
||||||
|
|
||||||
component HeaderPill: Rectangle {
|
component HeaderPill: Rectangle {
|
||||||
id: pill
|
id: pill
|
||||||
@@ -225,6 +249,7 @@ Item {
|
|||||||
required property string text
|
required property string text
|
||||||
property string icon: ""
|
property string icon: ""
|
||||||
property bool active: false
|
property bool active: false
|
||||||
|
property int badge: -1
|
||||||
signal clicked
|
signal clicked
|
||||||
|
|
||||||
width: pillRow.implicitWidth + 20
|
width: pillRow.implicitWidth + 20
|
||||||
@@ -260,6 +285,27 @@ Item {
|
|||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: pill.badge > 0
|
||||||
|
width: visible ? badgeText.implicitWidth + 9 : 0
|
||||||
|
height: 17
|
||||||
|
radius: Theme.pillRadius
|
||||||
|
border.width: 0
|
||||||
|
color: Theme.alpha(pill.active ? Theme.accent : Theme.fg, 0.12)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: badgeText
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: String(pill.badge)
|
||||||
|
color: pill.active ? Theme.accent : Theme.fgDim
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.features: Theme.tabularFigures
|
||||||
|
font.pixelSize: 9
|
||||||
|
font.weight: Font.DemiBold
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
|
|||||||
@@ -0,0 +1,263 @@
|
|||||||
|
// Persistent work and privacy activity inside the Daybook.
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import qs.config
|
||||||
|
import qs.services
|
||||||
|
import qs.modules.quicksettings
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property real maxHeight: 500
|
||||||
|
implicitHeight: content.implicitHeight
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: content
|
||||||
|
width: parent.width
|
||||||
|
spacing: 12
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 38
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "PERSISTENT ACTIVITY"
|
||||||
|
color: Theme.fgMuted
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.DemiBold
|
||||||
|
font.letterSpacing: 0.9
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: "Ongoing"
|
||||||
|
color: Theme.fg
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
font.weight: Font.DemiBold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
text: Ongoing.count === 0 ? "Nothing active" : Ongoing.count + (Ongoing.count === 1 ? " activity" : " activities")
|
||||||
|
color: Theme.fgDim
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.features: Theme.tabularFigures
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: visible ? 190 : 0
|
||||||
|
visible: Ongoing.count === 0
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: "\u{F04A5}"
|
||||||
|
color: Theme.fgMuted
|
||||||
|
font.family: Theme.fontMono
|
||||||
|
font.pixelSize: 24
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: "Nothing ongoing"
|
||||||
|
color: Theme.fg
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSize
|
||||||
|
font.weight: Font.DemiBold
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: "Focus, Caffeine, recording and privacy activity appear here."
|
||||||
|
color: Theme.fgDim
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollColumn {
|
||||||
|
width: parent.width
|
||||||
|
// Header + footer + the two 12px Column gaps consume 116px.
|
||||||
|
// Keeping that outside the scroll budget prevents the pane from
|
||||||
|
// growing beyond the Daybook when every producer is active.
|
||||||
|
maxHeight: Math.max(0, root.maxHeight - 116)
|
||||||
|
spacing: 8
|
||||||
|
visible: Ongoing.count > 0
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: Ongoing.activities
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: activityRow
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: 62
|
||||||
|
radius: Theme.cardRadius
|
||||||
|
border.width: 0
|
||||||
|
color: rowMouse.containsMouse
|
||||||
|
? Theme.alpha(Theme.fg, 0.10)
|
||||||
|
: Theme.alpha(Theme.fg, 0.055)
|
||||||
|
|
||||||
|
Behavior on color { ColorAnimation { duration: Theme.durFast } }
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: activityIcon
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 13
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: activityRow.modelData.glyph
|
||||||
|
color: root.toneColor(activityRow.modelData.tone)
|
||||||
|
font.family: Theme.fontMono
|
||||||
|
font.pixelSize: 17
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.left: activityIcon.right
|
||||||
|
anchors.leftMargin: 11
|
||||||
|
anchors.right: activityAction.visible ? activityAction.left : activityState.left
|
||||||
|
anchors.rightMargin: 10
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 3
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: activityRow.modelData.label
|
||||||
|
color: Theme.fg
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSize
|
||||||
|
font.weight: Font.DemiBold
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: activityRow.modelData.detail
|
||||||
|
color: Theme.fgDim
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: activityState
|
||||||
|
anchors.right: activityAction.visible ? activityAction.left : parent.right
|
||||||
|
anchors.rightMargin: activityAction.visible ? 7 : 12
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: stateLabel.implicitWidth + 16
|
||||||
|
height: 25
|
||||||
|
radius: Theme.pillRadius
|
||||||
|
border.width: 0
|
||||||
|
color: Theme.alpha(root.toneColor(activityRow.modelData.tone), 0.12)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: stateLabel
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: activityRow.modelData.state
|
||||||
|
color: root.toneColor(activityRow.modelData.tone)
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.features: Theme.tabularFigures
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.DemiBold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: activityAction
|
||||||
|
z: 2
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 9
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: activityRow.modelData.action !== ""
|
||||||
|
width: visible ? actionLabel.implicitWidth + 18 : 0
|
||||||
|
height: 28
|
||||||
|
radius: Theme.pillRadius
|
||||||
|
border.width: 0
|
||||||
|
color: actionMouse.containsMouse
|
||||||
|
? Theme.alpha(activityRow.modelData.tone === "danger" ? Theme.danger : Theme.fg, 0.20)
|
||||||
|
: Theme.alpha(activityRow.modelData.tone === "danger" ? Theme.danger : Theme.fg, 0.08)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: actionLabel
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: activityRow.modelData.action
|
||||||
|
color: activityRow.modelData.tone === "danger" ? Theme.danger : Theme.fg
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: actionMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: Ongoing.invoke(activityRow.modelData.kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: rowMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
z: 1
|
||||||
|
enabled: activityRow.modelData.kind !== "caffeine"
|
||||||
|
hoverEnabled: enabled
|
||||||
|
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
onClicked: Ongoing.activate(activityRow.modelData.kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 54
|
||||||
|
radius: Theme.cardRadius
|
||||||
|
border.width: 0
|
||||||
|
visible: Ongoing.count > 0
|
||||||
|
color: Theme.alpha(Theme.fg, 0.035)
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 13
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 3
|
||||||
|
Text {
|
||||||
|
text: "Visible until it ends"
|
||||||
|
color: Theme.fg
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.DemiBold
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: "Completed screenshots, devices and other brief events stay in Signal Glass."
|
||||||
|
color: Theme.fgDim
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toneColor(tone: string): color {
|
||||||
|
if (tone === "danger")
|
||||||
|
return Theme.danger;
|
||||||
|
if (tone === "warn")
|
||||||
|
return Theme.warn;
|
||||||
|
if (tone === "ok")
|
||||||
|
return Theme.ok;
|
||||||
|
return Theme.accent;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,4 +9,5 @@ MonthArrow 1.0 MonthArrow.qml
|
|||||||
NotificationGroup 1.0 NotificationGroup.qml
|
NotificationGroup 1.0 NotificationGroup.qml
|
||||||
NotificationList 1.0 NotificationList.qml
|
NotificationList 1.0 NotificationList.qml
|
||||||
NotificationSummary 1.0 NotificationSummary.qml
|
NotificationSummary 1.0 NotificationSummary.qml
|
||||||
|
OngoingPane 1.0 OngoingPane.qml
|
||||||
WeatherCard 1.0 WeatherCard.qml
|
WeatherCard 1.0 WeatherCard.qml
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
// One presentation model for state that remains active until it is explicitly
|
||||||
|
// stopped or naturally completes. Transient confirmations stay in Signal Glass.
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property var activities: {
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
if (FocusSession.active) {
|
||||||
|
result.push({
|
||||||
|
kind: "focus",
|
||||||
|
glyph: "\u{F051F}",
|
||||||
|
label: "Focus session",
|
||||||
|
detail: (FocusSession.workspaceLabel || "Focused workspace") + " · " + FocusSession.statusText,
|
||||||
|
state: FocusSession.remainingText,
|
||||||
|
tone: FocusSession.paused ? "warn" : "accent",
|
||||||
|
action: FocusSession.paused ? "Resume" : "Pause"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Caffeine.enabled) {
|
||||||
|
result.push({
|
||||||
|
kind: "caffeine",
|
||||||
|
glyph: "\u{F0F2E}",
|
||||||
|
label: "Caffeine",
|
||||||
|
detail: "Automatic sleep is blocked",
|
||||||
|
state: "On",
|
||||||
|
tone: "ok",
|
||||||
|
action: "Turn off"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PrivacyState.recordingActive) {
|
||||||
|
result.push({
|
||||||
|
kind: "recording",
|
||||||
|
glyph: "\u{F044A}",
|
||||||
|
label: "Screen recording",
|
||||||
|
detail: "Panama · " + root.recordingElapsed,
|
||||||
|
state: "Live",
|
||||||
|
tone: "danger",
|
||||||
|
action: "Stop"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (PrivacyState.screenSharingActive)
|
||||||
|
result.push(root.privacyActivity("screen", "\u{F0379}", "Screen sharing", PrivacyState.screenSharingApp));
|
||||||
|
if (PrivacyState.cameraActive)
|
||||||
|
result.push(root.privacyActivity("camera", "\u{F0100}", "Camera", PrivacyState.cameraApp));
|
||||||
|
if (PrivacyState.microphoneActive)
|
||||||
|
result.push(root.privacyActivity("microphone", "\u{F036C}", "Microphone", PrivacyState.microphoneApp));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property int count: activities.length
|
||||||
|
readonly property string recordingElapsed: {
|
||||||
|
const total = Capture.recordingSeconds;
|
||||||
|
const seconds = String(total % 60).padStart(2, "0");
|
||||||
|
const minutes = Math.floor(total / 60) % 60;
|
||||||
|
const hours = Math.floor(total / 3600);
|
||||||
|
return hours > 0
|
||||||
|
? `${hours}:${String(minutes).padStart(2, "0")}:${seconds}`
|
||||||
|
: `${minutes}:${seconds}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function privacyActivity(kind: string, glyph: string, label: string, app: string): var {
|
||||||
|
return {
|
||||||
|
kind,
|
||||||
|
glyph,
|
||||||
|
label,
|
||||||
|
detail: app || "Managed by the application",
|
||||||
|
state: "In use",
|
||||||
|
tone: "warn",
|
||||||
|
action: ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function activate(kind: string): void {
|
||||||
|
if (kind === "focus")
|
||||||
|
FocusSession.activateWorkspace();
|
||||||
|
else if (["recording", "screen", "camera", "microphone"].indexOf(kind) >= 0)
|
||||||
|
ShellState.open("activity");
|
||||||
|
}
|
||||||
|
|
||||||
|
function invoke(kind: string): void {
|
||||||
|
if (kind === "focus")
|
||||||
|
FocusSession.pauseOrResume();
|
||||||
|
else if (kind === "caffeine")
|
||||||
|
Caffeine.toggle();
|
||||||
|
else if (kind === "recording")
|
||||||
|
Capture.stopRecording();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -138,6 +138,8 @@ Singleton {
|
|||||||
root.applyStates(false, true, false, "", "Contract fixture", "", true);
|
root.applyStates(false, true, false, "", "Contract fixture", "", true);
|
||||||
else if (name === "screen-on")
|
else if (name === "screen-on")
|
||||||
root.applyStates(false, false, true, "", "", "Contract fixture", true);
|
root.applyStates(false, false, true, "", "", "Contract fixture", true);
|
||||||
|
else if (name === "all-on")
|
||||||
|
root.applyStates(true, true, true, "Contract fixture", "Contract fixture", "Contract fixture", true);
|
||||||
else if (name === "all-off")
|
else if (name === "all-off")
|
||||||
root.applyStates(false, false, false, "", "", "", true);
|
root.applyStates(false, false, false, "", "", "", true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ Singleton {
|
|||||||
|
|
||||||
readonly property bool anyOverlayOpen: activeOverlay !== ""
|
readonly property bool anyOverlayOpen: activeOverlay !== ""
|
||||||
|
|
||||||
// The date menu is one surface with two deliberate entry points. The clock
|
// The date menu keeps calendar context fixed while its right pane switches
|
||||||
// opens the Daybook; Super+B and notification actions open message history.
|
// between the schedule, persistent activity and message history.
|
||||||
property string dateMenuPage: "agenda"
|
property string dateMenuPage: "agenda"
|
||||||
|
|
||||||
// 0 means "use the currently focused workspace". A positive value lets a
|
// 0 means "use the currently focused workspace". A positive value lets a
|
||||||
@@ -53,12 +53,12 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openDateMenu(page: string): void {
|
function openDateMenu(page: string): void {
|
||||||
root.dateMenuPage = page === "notifications" ? "notifications" : "agenda";
|
root.dateMenuPage = root.normalizedDateMenuPage(page);
|
||||||
root.activeOverlay = "notifications";
|
root.activeOverlay = "notifications";
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleDateMenu(page: string): void {
|
function toggleDateMenu(page: string): void {
|
||||||
const target = page === "notifications" ? "notifications" : "agenda";
|
const target = root.normalizedDateMenuPage(page);
|
||||||
if (root.activeOverlay === "notifications" && root.dateMenuPage === target) {
|
if (root.activeOverlay === "notifications" && root.dateMenuPage === target) {
|
||||||
root.activeOverlay = "";
|
root.activeOverlay = "";
|
||||||
return;
|
return;
|
||||||
@@ -67,6 +67,10 @@ Singleton {
|
|||||||
root.activeOverlay = "notifications";
|
root.activeOverlay = "notifications";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizedDateMenuPage(page: string): string {
|
||||||
|
return ["agenda", "ongoing", "notifications"].indexOf(page) >= 0 ? page : "agenda";
|
||||||
|
}
|
||||||
|
|
||||||
function openOverview(workspaceId: int): void {
|
function openOverview(workspaceId: int): void {
|
||||||
root.overviewWorkspaceId = Math.max(0, workspaceId);
|
root.overviewWorkspaceId = Math.max(0, workspaceId);
|
||||||
root.overviewQuery = "";
|
root.overviewQuery = "";
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ ShellRoot {
|
|||||||
Overview {}
|
Overview {}
|
||||||
// QuickSettings is the window; QuickSettingsPanel is its content.
|
// QuickSettings is the window; QuickSettingsPanel is its content.
|
||||||
QuickSettings {}
|
QuickSettings {}
|
||||||
DateMenu {}
|
DateMenu { id: dateMenu }
|
||||||
ClipboardPanel {}
|
ClipboardPanel {}
|
||||||
CaptureOverlay {}
|
CaptureOverlay {}
|
||||||
IntelligenceResult {}
|
IntelligenceResult {}
|
||||||
@@ -268,6 +268,7 @@ ShellRoot {
|
|||||||
function reset(): void { CalendarAgenda.clearFixture(); }
|
function reset(): void { CalendarAgenda.clearFixture(); }
|
||||||
function refresh(): void { CalendarAgenda.refresh(); }
|
function refresh(): void { CalendarAgenda.refresh(); }
|
||||||
function open(): void { ShellState.openDateMenu("agenda"); }
|
function open(): void { ShellState.openDateMenu("agenda"); }
|
||||||
|
function page(name: string): void { ShellState.openDateMenu(name); }
|
||||||
function close(): void { ShellState.close(); }
|
function close(): void { ShellState.close(); }
|
||||||
function select(date: string): void { CalendarAgenda.selectIsoDate(date); }
|
function select(date: string): void { CalendarAgenda.selectIsoDate(date); }
|
||||||
function status(): string {
|
function status(): string {
|
||||||
@@ -281,7 +282,9 @@ ShellRoot {
|
|||||||
selectedDate: CalendarAgenda.selectedDateKey,
|
selectedDate: CalendarAgenda.selectedDateKey,
|
||||||
selectedEventCount: CalendarAgenda.selectedEvents.length,
|
selectedEventCount: CalendarAgenda.selectedEvents.length,
|
||||||
capsuleVisible: CalendarAgenda.capsuleVisible,
|
capsuleVisible: CalendarAgenda.capsuleVisible,
|
||||||
capsuleText: CalendarAgenda.capsuleText
|
capsuleText: CalendarAgenda.capsuleText,
|
||||||
|
ongoingCount: Ongoing.count,
|
||||||
|
ongoingFits: dateMenu.ongoingContentFits
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ fail() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
|
qs ipc call focus end >/dev/null 2>&1 || true
|
||||||
|
qs ipc call activity clear >/dev/null 2>&1 || true
|
||||||
qs ipc call calendar-agenda reset >/dev/null 2>&1 || true
|
qs ipc call calendar-agenda reset >/dev/null 2>&1 || true
|
||||||
qs ipc call calendar-agenda close >/dev/null 2>&1 || true
|
qs ipc call calendar-agenda close >/dev/null 2>&1 || true
|
||||||
}
|
}
|
||||||
@@ -40,6 +42,18 @@ jq -e '.open == true and .page == "notifications"' \
|
|||||||
<<<"$(qs ipc call calendar-agenda status)" >/dev/null \
|
<<<"$(qs ipc call calendar-agenda status)" >/dev/null \
|
||||||
|| fail 'notification open did not route to Notifications'
|
|| fail 'notification open did not route to Notifications'
|
||||||
|
|
||||||
|
qs ipc call activity fixture microphone-on >/dev/null
|
||||||
|
qs ipc call calendar-agenda page ongoing >/dev/null
|
||||||
|
jq -e '.open == true and .page == "ongoing" and .ongoingCount >= 1' \
|
||||||
|
<<<"$(qs ipc call calendar-agenda status)" >/dev/null \
|
||||||
|
|| fail 'persistent activity did not route to Ongoing'
|
||||||
|
|
||||||
|
qs ipc call activity fixture all-on >/dev/null
|
||||||
|
qs ipc call focus start >/dev/null
|
||||||
|
jq -e '.ongoingCount == 5 and .ongoingFits == true' \
|
||||||
|
<<<"$(qs ipc call calendar-agenda status)" >/dev/null \
|
||||||
|
|| fail 'crowded Ongoing activity does not fit the Daybook'
|
||||||
|
|
||||||
hyprctl layers | rg -q 'namespace: qs-popover-datemenu' \
|
hyprctl layers | rg -q 'namespace: qs-popover-datemenu' \
|
||||||
|| fail 'Daybook layer did not map'
|
|| fail 'Daybook layer did not map'
|
||||||
rg -q 'implicitWidth: 760' config/dot/quickshell/modules/datemenu/DateMenu.qml \
|
rg -q 'implicitWidth: 760' config/dot/quickshell/modules/datemenu/DateMenu.qml \
|
||||||
|
|||||||
Reference in New Issue
Block a user