Build the Prism calendar Daybook
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
import QtQuick
|
||||
import qs.config
|
||||
import qs.services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property var eventData
|
||||
property bool past: false
|
||||
|
||||
signal activated
|
||||
signal joinRequested
|
||||
|
||||
readonly property var sourceData: CalendarAgenda.sourceForId(eventData.sourceId)
|
||||
readonly property color eventColor: sourceData?.color ?? Theme.accent
|
||||
readonly property string timeText: eventData.allDay
|
||||
? "All day"
|
||||
: Qt.formatTime(new Date(Number(eventData.start) * 1000), Settings.use24Hour ? "HH:mm" : "h:mm AP")
|
||||
readonly property string detailText: {
|
||||
const source = root.sourceData?.name ?? "Calendar";
|
||||
const location = String(root.eventData.location ?? "").trim();
|
||||
return location === "" ? source : source + " · " + location;
|
||||
}
|
||||
|
||||
implicitHeight: 58
|
||||
opacity: past ? 0.52 : 1
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: 9
|
||||
border.width: 0
|
||||
color: rowMouse.containsMouse ? Theme.alpha(Theme.fg, 0.07) : "transparent"
|
||||
|
||||
Behavior on color { ColorAnimation { duration: Theme.durFast } }
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: rowMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.activated()
|
||||
}
|
||||
|
||||
Text {
|
||||
id: timeLabel
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 56
|
||||
text: root.timeText
|
||||
color: Theme.fgDim
|
||||
horizontalAlignment: Text.AlignRight
|
||||
font.family: Theme.fontFamily
|
||||
font.features: Theme.tabularFigures
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: sourceRail
|
||||
anchors.left: timeLabel.right
|
||||
anchors.leftMargin: 10
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 3
|
||||
height: 34
|
||||
radius: 2
|
||||
border.width: 0
|
||||
color: root.eventColor
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.left: sourceRail.right
|
||||
anchors.leftMargin: 10
|
||||
anchors.right: joinPill.visible ? joinPill.left : parent.right
|
||||
anchors.rightMargin: joinPill.visible ? 10 : 4
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 3
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: root.eventData.summary || "Untitled event"
|
||||
color: Theme.fg
|
||||
elide: Text.ElideRight
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: root.detailText
|
||||
color: Theme.fgDim
|
||||
elide: Text.ElideRight
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: joinPill
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 4
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: String(root.eventData.joinUrl ?? "").startsWith("https://")
|
||||
width: joinLabel.implicitWidth + 18
|
||||
height: 26
|
||||
radius: Theme.pillRadius
|
||||
border.width: 0
|
||||
color: joinMouse.containsMouse ? Theme.alpha(Theme.accent, 0.28) : Theme.alpha(Theme.accent, 0.14)
|
||||
|
||||
Text {
|
||||
id: joinLabel
|
||||
anchors.centerIn: parent
|
||||
text: "Join"
|
||||
color: Theme.accent
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: joinMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.joinRequested()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.config
|
||||
import qs.services
|
||||
import qs.modules.quicksettings
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
signal requestNotifications
|
||||
|
||||
readonly property bool isToday: CalendarAgenda.selectedDateKey === CalendarAgenda.dateKey(clock.date)
|
||||
readonly property var upNext: CalendarAgenda.nextEventForSelectedDate
|
||||
readonly property var listedEvents: CalendarAgenda.selectedEvents.filter(event => !root.upNext || event.id !== root.upNext.id)
|
||||
readonly property string selectedLabel: root.isToday
|
||||
? "Today"
|
||||
: Qt.formatDate(CalendarAgenda.selectedDate, "dddd, MMMM d")
|
||||
|
||||
implicitHeight: content.implicitHeight
|
||||
|
||||
SystemClock {
|
||||
id: clock
|
||||
precision: SystemClock.Minutes
|
||||
}
|
||||
|
||||
Column {
|
||||
id: content
|
||||
width: parent.width
|
||||
spacing: 12
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 3
|
||||
visible: CalendarAgenda.phase === "loading"
|
||||
|
||||
Text {
|
||||
text: "CALENDAR"
|
||||
color: Theme.fgMuted
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.DemiBold
|
||||
font.letterSpacing: 1.1
|
||||
}
|
||||
Text {
|
||||
text: "Loading calendars…"
|
||||
color: Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 10
|
||||
visible: CalendarAgenda.phase === "unavailable"
|
||||
|
||||
Text {
|
||||
text: "Calendar data is unavailable"
|
||||
color: Theme.fg
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
Text {
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
text: "Notifications, weather, and the month grid are still available. GNOME Calendar may have more detail."
|
||||
color: Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
}
|
||||
ActionPill {
|
||||
text: "Open Calendar"
|
||||
onClicked: CalendarAgenda.openCalendar(CalendarAgenda.selectedDate)
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: nextCard
|
||||
width: parent.width
|
||||
height: visible ? 88 : 0
|
||||
visible: CalendarAgenda.available && root.isToday && root.upNext !== null
|
||||
radius: Theme.cardRadius
|
||||
border.width: 1
|
||||
border.color: Theme.alpha(Theme.accent, 0.18)
|
||||
color: Theme.alpha(Theme.accent, 0.105)
|
||||
clip: true
|
||||
|
||||
Rectangle {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: -24
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: -34
|
||||
width: 110
|
||||
height: 110
|
||||
radius: 55
|
||||
border.width: 0
|
||||
color: Theme.alpha(Theme.accentSecondary, 0.08)
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 14
|
||||
anchors.right: nextJoin.visible ? nextJoin.left : parent.right
|
||||
anchors.rightMargin: nextJoin.visible ? 10 : 14
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 5
|
||||
|
||||
Text {
|
||||
text: "UP NEXT · " + root.relativeStart(root.upNext)
|
||||
color: Theme.accent
|
||||
font.family: Theme.fontFamily
|
||||
font.features: Theme.tabularFigures
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
font.letterSpacing: 0.8
|
||||
}
|
||||
Text {
|
||||
width: parent.width
|
||||
text: root.upNext?.summary ?? ""
|
||||
color: Theme.fg
|
||||
elide: Text.ElideRight
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
Text {
|
||||
width: parent.width
|
||||
text: root.eventDetail(root.upNext)
|
||||
color: Theme.fgDim
|
||||
elide: Text.ElideRight
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
}
|
||||
}
|
||||
|
||||
ActionPill {
|
||||
id: nextJoin
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 12
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 12
|
||||
visible: String(root.upNext?.joinUrl ?? "").startsWith("https://")
|
||||
text: "Join"
|
||||
accent: true
|
||||
onClicked: CalendarAgenda.join(root.upNext)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: CalendarAgenda.openEvent(root.upNext)
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 34
|
||||
visible: CalendarAgenda.available
|
||||
|
||||
Column {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
|
||||
Text {
|
||||
text: root.selectedLabel.toUpperCase()
|
||||
color: Theme.fgMuted
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.DemiBold
|
||||
font.letterSpacing: 0.9
|
||||
}
|
||||
Text {
|
||||
text: "Your schedule"
|
||||
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: CalendarAgenda.selectedEvents.length + (CalendarAgenda.selectedEvents.length === 1 ? " event" : " events")
|
||||
color: Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.features: Theme.tabularFigures
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: visible ? 92 : 0
|
||||
visible: CalendarAgenda.available && CalendarAgenda.selectedEvents.length === 0
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: "Nothing scheduled"
|
||||
color: Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
}
|
||||
ActionPill {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: "Open Calendar"
|
||||
onClicked: CalendarAgenda.openCalendar(CalendarAgenda.selectedDate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScrollColumn {
|
||||
width: parent.width
|
||||
maxHeight: 286
|
||||
spacing: 0
|
||||
visible: CalendarAgenda.available && root.listedEvents.length > 0
|
||||
|
||||
Repeater {
|
||||
model: root.listedEvents
|
||||
|
||||
AgendaEvent {
|
||||
required property var modelData
|
||||
|
||||
width: parent.width
|
||||
eventData: modelData
|
||||
past: Number(modelData.end) <= CalendarAgenda.nowEpoch
|
||||
onActivated: CalendarAgenda.openEvent(modelData)
|
||||
onJoinRequested: CalendarAgenda.join(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NotificationSummary {
|
||||
width: parent.width
|
||||
onActivated: root.requestNotifications()
|
||||
}
|
||||
}
|
||||
|
||||
function relativeStart(event: var): string {
|
||||
if (!event)
|
||||
return "";
|
||||
const seconds = Number(event.start) - CalendarAgenda.nowEpoch;
|
||||
if (seconds <= 0)
|
||||
return "NOW";
|
||||
if (seconds < 3600)
|
||||
return "IN " + Math.ceil(seconds / 60) + " MINUTES";
|
||||
return Qt.formatTime(new Date(Number(event.start) * 1000), Settings.use24Hour ? "HH:mm" : "h:mm AP");
|
||||
}
|
||||
|
||||
function eventDetail(event: var): string {
|
||||
if (!event)
|
||||
return "";
|
||||
const source = CalendarAgenda.sourceForId(event.sourceId)?.name ?? "Calendar";
|
||||
const location = String(event.location ?? "").trim();
|
||||
return location === "" ? source : source + " · " + location;
|
||||
}
|
||||
|
||||
component ActionPill: Rectangle {
|
||||
id: action
|
||||
|
||||
required property string text
|
||||
property bool accent: false
|
||||
signal clicked
|
||||
|
||||
width: actionLabel.implicitWidth + 20
|
||||
height: 28
|
||||
radius: Theme.pillRadius
|
||||
border.width: 0
|
||||
color: actionMouse.containsMouse
|
||||
? Theme.alpha(action.accent ? Theme.accent : Theme.fg, 0.24)
|
||||
: Theme.alpha(action.accent ? Theme.accent : Theme.fg, action.accent ? 0.14 : 0.07)
|
||||
|
||||
Text {
|
||||
id: actionLabel
|
||||
anchors.centerIn: parent
|
||||
text: action.text
|
||||
color: action.accent ? Theme.accent : Theme.fg
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
MouseArea {
|
||||
id: actionMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: action.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ PanelWindow {
|
||||
margins.top: Theme.barHeight + Theme.barGap * 2
|
||||
exclusiveZone: 0
|
||||
|
||||
implicitWidth: panel.implicitWidth
|
||||
implicitWidth: 760
|
||||
implicitHeight: panel.implicitHeight
|
||||
|
||||
// The `qs-popover` prefix is matched by a layerrule in hypr/rules.lua.
|
||||
@@ -41,15 +41,24 @@ PanelWindow {
|
||||
|
||||
onVisibleChanged: {
|
||||
if (root.visible) {
|
||||
// Opening the tray is what marks everything read, same as GNOME.
|
||||
Notifs.markAllRead();
|
||||
panel.prepare();
|
||||
CalendarAgenda.refresh();
|
||||
if (ShellState.dateMenuPage === "notifications")
|
||||
Notifs.markAllRead();
|
||||
panel.prepare(ShellState.dateMenuPage);
|
||||
openAnim.restart();
|
||||
} else {
|
||||
panel.reset();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ShellState
|
||||
function onDateMenuPageChanged(): void {
|
||||
if (root.visible && ShellState.dateMenuPage === "notifications")
|
||||
Notifs.markAllRead();
|
||||
}
|
||||
}
|
||||
|
||||
// Closes the panel when a click lands anywhere else on the desktop.
|
||||
HyprlandFocusGrab {
|
||||
windows: [root]
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
// The contents of the date menu, in GNOME's order: date header, notifications,
|
||||
// calendar, weather.
|
||||
//
|
||||
// Kept separate from DateMenu.qml (the PanelWindow) so it can be dropped into a
|
||||
// FloatingWindow for testing — layer-shell surfaces cannot map outside a
|
||||
// wlroots compositor.
|
||||
// The approved A · Daybook: month context on the left, the selected day's
|
||||
// schedule or grouped notification history on the right.
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
@@ -14,38 +10,38 @@ import qs.widgets
|
||||
Item {
|
||||
id: root
|
||||
|
||||
implicitWidth: 480
|
||||
implicitHeight: content.implicitHeight + Theme.popoverPadding * 2
|
||||
implicitWidth: 760
|
||||
implicitHeight: frame.implicitHeight + Theme.popoverPadding * 2
|
||||
|
||||
// Called by the window when it opens and closes, so the panel is always in
|
||||
// a known state rather than wherever the last visit left it.
|
||||
function prepare(): void {
|
||||
function prepare(page: string): void {
|
||||
calendar.showToday();
|
||||
CalendarAgenda.selectDate(clock.date);
|
||||
CalendarAgenda.setVisibleMonth(clock.date.getFullYear(), clock.date.getMonth());
|
||||
if (page === "notifications")
|
||||
Notifs.markAllRead();
|
||||
}
|
||||
|
||||
function reset(): void {
|
||||
notifications.reset();
|
||||
}
|
||||
|
||||
// Minutes precision: the header only shows a date, so this exists purely to
|
||||
// roll it over at midnight without a per-second wake.
|
||||
SystemClock {
|
||||
id: clock
|
||||
precision: SystemClock.Minutes
|
||||
}
|
||||
|
||||
Column {
|
||||
id: content
|
||||
id: frame
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.popoverPadding
|
||||
spacing: Theme.sectionSpacing
|
||||
spacing: 0
|
||||
|
||||
// ── Header ──────────────────────────────────────────────────────────
|
||||
Item {
|
||||
id: header
|
||||
width: parent.width
|
||||
height: 34
|
||||
height: 42
|
||||
|
||||
Text {
|
||||
anchors.left: parent.left
|
||||
@@ -57,7 +53,6 @@ Item {
|
||||
color: Theme.fg
|
||||
elide: Text.ElideRight
|
||||
font.family: Theme.fontFamily
|
||||
// The day number changes in place at midnight.
|
||||
font.features: Theme.tabularFigures
|
||||
font.pixelSize: Theme.fontSizeTitle
|
||||
font.weight: Font.DemiBold
|
||||
@@ -69,153 +64,210 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 6
|
||||
|
||||
// Labelled rather than icon-only: an unexplained crossed-out
|
||||
// bell is exactly the kind of thing a normal person has to
|
||||
// click to find out about.
|
||||
Rectangle {
|
||||
id: dndPill
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: dndRow.implicitWidth + 20
|
||||
height: 28
|
||||
radius: Theme.pillRadius
|
||||
border.width: 0
|
||||
|
||||
color: {
|
||||
if (Notifs.doNotDisturb)
|
||||
return dndMouse.containsMouse ? Theme.alpha(Theme.accent, 0.42) : Theme.alpha(Theme.accent, 0.28);
|
||||
return dndMouse.containsMouse ? Theme.alpha(Theme.fg, 0.16) : Theme.alpha(Theme.fg, 0.08);
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: dndMouse.containsMouse ? Theme.durFast : Theme.durNormal
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: dndRow
|
||||
anchors.centerIn: parent
|
||||
spacing: 6
|
||||
|
||||
ThemedIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
size: 14
|
||||
icon: "notifications-disabled-symbolic"
|
||||
iconFallback: "user-invisible-symbolic"
|
||||
tint: Notifs.doNotDisturb ? Theme.accent : Theme.fgDim
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Do Not Disturb"
|
||||
color: Notifs.doNotDisturb ? Theme.accent : Theme.fg
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: dndMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: Notifs.doNotDisturb = !Notifs.doNotDisturb
|
||||
}
|
||||
HeaderPill {
|
||||
visible: ShellState.dateMenuPage === "notifications"
|
||||
text: "← Back to agenda"
|
||||
onClicked: ShellState.dateMenuPage = "agenda"
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: clearPill
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: clearLabel.implicitWidth + 20
|
||||
height: 28
|
||||
radius: Theme.pillRadius
|
||||
border.width: 0
|
||||
// Hidden rather than disabled: there is nothing ambiguous
|
||||
// about an empty tray, so the control has no job.
|
||||
visible: Notifs.hasNotifications
|
||||
color: clearMouse.containsMouse ? Theme.alpha(Theme.fg, 0.16) : Theme.alpha(Theme.fg, 0.08)
|
||||
HeaderPill {
|
||||
text: "Do Not Disturb"
|
||||
active: Notifs.doNotDisturb
|
||||
icon: "notifications-disabled-symbolic"
|
||||
onClicked: Notifs.doNotDisturb = !Notifs.doNotDisturb
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: clearMouse.containsMouse ? Theme.durFast : Theme.durNormal
|
||||
}
|
||||
}
|
||||
HeaderPill {
|
||||
visible: ShellState.dateMenuPage === "agenda"
|
||||
text: "Open Calendar ↗"
|
||||
onClicked: CalendarAgenda.openCalendar(CalendarAgenda.selectedDate)
|
||||
}
|
||||
|
||||
Text {
|
||||
id: clearLabel
|
||||
anchors.centerIn: parent
|
||||
text: "Clear all"
|
||||
color: Theme.fg
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: clearMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: Notifs.dismissAll()
|
||||
}
|
||||
HeaderPill {
|
||||
visible: ShellState.dateMenuPage === "notifications" && Notifs.hasNotifications
|
||||
text: "Clear all"
|
||||
onClicked: Notifs.dismissAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Notifications ───────────────────────────────────────────────────
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
border.width: 0
|
||||
color: Theme.alpha(Theme.fg, 0.1)
|
||||
color: Theme.alpha(Theme.fg, 0.10)
|
||||
}
|
||||
|
||||
MediaCard {
|
||||
id: media
|
||||
Row {
|
||||
id: body
|
||||
width: parent.width
|
||||
height: Math.min(620, Math.max(438, leftContent.implicitHeight + 28, rightContentHeight + 28))
|
||||
|
||||
Item {
|
||||
id: leftColumn
|
||||
width: 310
|
||||
height: body.height
|
||||
|
||||
Column {
|
||||
id: leftContent
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: 14
|
||||
spacing: 12
|
||||
|
||||
CalendarGrid {
|
||||
id: calendar
|
||||
width: parent.width
|
||||
selectedDate: CalendarAgenda.selectedDate
|
||||
onDateActivated: date => CalendarAgenda.selectDate(date)
|
||||
onVisibleMonthChanged: (year, month) => CalendarAgenda.setVisibleMonth(year, month)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
border.width: 0
|
||||
visible: media.visible || Weather.available
|
||||
color: Theme.alpha(Theme.fg, 0.08)
|
||||
}
|
||||
|
||||
MediaCard {
|
||||
id: media
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
WeatherCard {
|
||||
width: parent.width
|
||||
visible: Weather.available
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 1
|
||||
height: body.height
|
||||
border.width: 0
|
||||
color: Theme.alpha(Theme.fg, 0.10)
|
||||
}
|
||||
|
||||
Item {
|
||||
id: rightColumn
|
||||
width: body.width - leftColumn.width - 1
|
||||
height: body.height
|
||||
|
||||
readonly property real availableWidth: width - 28
|
||||
|
||||
AgendaPane {
|
||||
id: agenda
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: 14
|
||||
visible: ShellState.dateMenuPage === "agenda"
|
||||
onRequestNotifications: {
|
||||
ShellState.dateMenuPage = "notifications";
|
||||
Notifs.markAllRead();
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: notificationPane
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: 14
|
||||
spacing: 10
|
||||
visible: ShellState.dateMenuPage === "notifications"
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 28
|
||||
|
||||
Text {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Notifications"
|
||||
color: Theme.fg
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: Notifs.history.length + (Notifs.history.length === 1 ? " item" : " items")
|
||||
color: Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.features: Theme.tabularFigures
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
}
|
||||
}
|
||||
|
||||
NotificationList {
|
||||
id: notifications
|
||||
width: parent.width
|
||||
maxHeight: body.height - 76
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly property real rightContentHeight: ShellState.dateMenuPage === "notifications"
|
||||
? notificationPane.implicitHeight
|
||||
: agenda.implicitHeight
|
||||
|
||||
component HeaderPill: Rectangle {
|
||||
id: pill
|
||||
|
||||
required property string text
|
||||
property string icon: ""
|
||||
property bool active: false
|
||||
signal clicked
|
||||
|
||||
width: pillRow.implicitWidth + 20
|
||||
height: 28
|
||||
radius: Theme.pillRadius
|
||||
border.width: 0
|
||||
color: {
|
||||
if (pill.active)
|
||||
return pillMouse.containsMouse ? Theme.alpha(Theme.accent, 0.38) : Theme.alpha(Theme.accent, 0.24);
|
||||
return pillMouse.containsMouse ? Theme.alpha(Theme.fg, 0.15) : Theme.alpha(Theme.fg, 0.07);
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
border.width: 0
|
||||
visible: media.visible
|
||||
color: Theme.alpha(Theme.fg, 0.1)
|
||||
Behavior on color { ColorAnimation { duration: Theme.durFast } }
|
||||
|
||||
Row {
|
||||
id: pillRow
|
||||
anchors.centerIn: parent
|
||||
spacing: 6
|
||||
|
||||
ThemedIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: pill.icon !== ""
|
||||
size: visible ? 14 : 0
|
||||
icon: pill.icon
|
||||
tint: pill.active ? Theme.accent : Theme.fgDim
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: pill.text
|
||||
color: pill.active ? Theme.accent : Theme.fg
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
}
|
||||
|
||||
NotificationList {
|
||||
id: notifications
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
// ── Calendar ────────────────────────────────────────────────────────
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
border.width: 0
|
||||
color: Theme.alpha(Theme.fg, 0.1)
|
||||
}
|
||||
|
||||
CalendarGrid {
|
||||
id: calendar
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
// ── Weather ─────────────────────────────────────────────────────────
|
||||
// Separator and card share one condition so the panel does not end on a
|
||||
// rule with nothing under it.
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
border.width: 0
|
||||
visible: Weather.available
|
||||
color: Theme.alpha(Theme.fg, 0.1)
|
||||
}
|
||||
|
||||
WeatherCard {
|
||||
width: parent.width
|
||||
visible: Weather.available
|
||||
MouseArea {
|
||||
id: pillMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: pill.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,13 +9,15 @@ import qs.widgets
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
readonly property bool compact: width < 360
|
||||
|
||||
readonly property MprisPlayer player: {
|
||||
const players = Mpris.players ? Mpris.players.values : [];
|
||||
return players.find(p => p.isPlaying) ?? players.find(p => p.trackTitle) ?? null;
|
||||
}
|
||||
|
||||
visible: root.player !== null
|
||||
implicitHeight: visible ? 76 : 0
|
||||
implicitHeight: visible ? (compact ? 64 : 76) : 0
|
||||
radius: Theme.cardRadius
|
||||
border.width: 0
|
||||
color: Theme.alpha(Theme.fg, 0.06)
|
||||
@@ -24,11 +26,11 @@ Rectangle {
|
||||
Rectangle {
|
||||
id: artwork
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 10
|
||||
anchors.leftMargin: root.compact ? 8 : 10
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 56
|
||||
height: 56
|
||||
radius: 10
|
||||
width: root.compact ? 46 : 56
|
||||
height: width
|
||||
radius: root.compact ? 9 : 10
|
||||
border.width: 0
|
||||
color: Theme.alpha(Theme.accentAlt, 0.16)
|
||||
clip: true
|
||||
@@ -54,9 +56,9 @@ Rectangle {
|
||||
|
||||
Column {
|
||||
anchors.left: artwork.right
|
||||
anchors.leftMargin: 12
|
||||
anchors.leftMargin: root.compact ? 9 : 12
|
||||
anchors.right: controls.left
|
||||
anchors.rightMargin: 10
|
||||
anchors.rightMargin: root.compact ? 6 : 10
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 3
|
||||
|
||||
@@ -83,7 +85,7 @@ Rectangle {
|
||||
Row {
|
||||
id: controls
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 10
|
||||
anchors.rightMargin: root.compact ? 7 : 10
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
|
||||
@@ -114,7 +116,7 @@ Rectangle {
|
||||
property bool emphasized: false
|
||||
signal clicked
|
||||
|
||||
width: emphasized ? 34 : 30
|
||||
width: root.compact ? (emphasized ? 30 : 27) : (emphasized ? 34 : 30)
|
||||
height: width
|
||||
radius: width / 2
|
||||
border.width: 0
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import QtQuick
|
||||
import qs.config
|
||||
import qs.services
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
signal activated
|
||||
|
||||
visible: Notifs.hasNotifications
|
||||
implicitHeight: visible ? 62 : 0
|
||||
radius: Theme.cardRadius
|
||||
border.width: 0
|
||||
color: summaryMouse.containsMouse ? Theme.alpha(Theme.fg, 0.10) : Theme.alpha(Theme.fg, 0.055)
|
||||
|
||||
Behavior on color { ColorAnimation { duration: Theme.durFast } }
|
||||
|
||||
Column {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 14
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 3
|
||||
|
||||
Text {
|
||||
text: Notifs.history.length + (Notifs.history.length === 1 ? " notification" : " notifications")
|
||||
color: Theme.fg
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Grouped quietly until you open them"
|
||||
color: Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 14
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "View →"
|
||||
color: Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: summaryMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.activated()
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
module qs.modules.datemenu
|
||||
AgendaEvent 1.0 AgendaEvent.qml
|
||||
AgendaPane 1.0 AgendaPane.qml
|
||||
CalendarGrid 1.0 CalendarGrid.qml
|
||||
DateMenu 1.0 DateMenu.qml
|
||||
DateMenuPanel 1.0 DateMenuPanel.qml
|
||||
@@ -6,4 +8,5 @@ MediaCard 1.0 MediaCard.qml
|
||||
MonthArrow 1.0 MonthArrow.qml
|
||||
NotificationGroup 1.0 NotificationGroup.qml
|
||||
NotificationList 1.0 NotificationList.qml
|
||||
NotificationSummary 1.0 NotificationSummary.qml
|
||||
WeatherCard 1.0 WeatherCard.qml
|
||||
|
||||
@@ -526,7 +526,7 @@ git commit -m "Route the interactive Panama date menu"
|
||||
- Consumes: `ShellState.dateMenuPage`, `Notifs.hasNotifications`, and existing `NotificationList`.
|
||||
- Produces: a 760-pixel two-column date menu with left width 310 and independently bounded right content.
|
||||
|
||||
- [ ] **Step 1: Add failing structural and live-surface assertions**
|
||||
- [x] **Step 1: Add failing structural and live-surface assertions**
|
||||
|
||||
Extend the contract to assert the loaded layer and approved components:
|
||||
|
||||
@@ -542,7 +542,7 @@ done
|
||||
|
||||
Run the contract and expect failure on width or missing components.
|
||||
|
||||
- [ ] **Step 2: Implement focused event and summary components**
|
||||
- [x] **Step 2: Implement focused event and summary components**
|
||||
|
||||
`AgendaEvent.qml` accepts `property var event`, `property bool past`, and signals `activated` and `joinRequested`. It renders a 48-pixel tabular time column, 3-pixel source-color rail, elided summary, source/location metadata, and optional Join pill. All-day rows replace time with `All day`.
|
||||
|
||||
@@ -557,7 +557,7 @@ Run the contract and expect failure on width or missing components.
|
||||
|
||||
The Up Next card is omitted for selected dates other than today and when no future timed event exists. Use `ScrollColumn` with a maximum content height derived from the panel's available height. Event clicks call `CalendarAgenda.openEvent(event)`; Join calls `CalendarAgenda.join(event)`.
|
||||
|
||||
- [ ] **Step 3: Recompose DateMenuPanel as Daybook**
|
||||
- [x] **Step 3: Recompose DateMenuPanel as Daybook**
|
||||
|
||||
Keep the existing header styling and Prism container, but replace the single `Column` with this concrete structure:
|
||||
|
||||
@@ -601,7 +601,7 @@ When Agenda's notification summary activates, set `ShellState.dateMenuPage = "no
|
||||
|
||||
Resize MediaCard typography/controls responsively for the 310-pixel column; do not duplicate its playback logic.
|
||||
|
||||
- [ ] **Step 4: Update DateMenu lifecycle and bounds**
|
||||
- [x] **Step 4: Update DateMenu lifecycle and bounds**
|
||||
|
||||
Set `DateMenu.implicitWidth: 760`. Bound height to the active screen's available geometry, with the panel content choosing a maximum right-column scroll height instead of overflowing the monitor.
|
||||
|
||||
@@ -616,7 +616,7 @@ panel.prepare(ShellState.dateMenuPage);
|
||||
|
||||
Add a connection that marks notifications read when an already-open panel switches into Notifications. Opening from the clock must not mark them read.
|
||||
|
||||
- [ ] **Step 5: Verify fixture visuals on the real layer surface**
|
||||
- [x] **Step 5: Verify fixture visuals on the real layer surface**
|
||||
|
||||
Run:
|
||||
|
||||
@@ -637,7 +637,7 @@ Capture a temporary full-screen screenshot with `grim`, inspect the Daybook at o
|
||||
|
||||
Close the panel and reset fixture mode after inspection. Do not add screenshots to git.
|
||||
|
||||
- [ ] **Step 6: Commit the Daybook**
|
||||
- [x] **Step 6: Commit the Daybook**
|
||||
|
||||
```bash
|
||||
git add config/dot/quickshell/modules/datemenu \
|
||||
|
||||
@@ -40,6 +40,15 @@ jq -e '.open == true and .page == "notifications"' \
|
||||
<<<"$(qs ipc call calendar-agenda status)" >/dev/null \
|
||||
|| fail 'notification open did not route to Notifications'
|
||||
|
||||
hyprctl layers | rg -q 'namespace: qs-popover-datemenu' \
|
||||
|| fail 'Daybook layer did not map'
|
||||
rg -q 'implicitWidth: 760' config/dot/quickshell/modules/datemenu/DateMenu.qml \
|
||||
|| fail 'Daybook is not the approved width'
|
||||
for component in AgendaPane AgendaEvent NotificationSummary; do
|
||||
rg -q "${component} 1.0 ${component}.qml" config/dot/quickshell/modules/datemenu/qmldir \
|
||||
|| fail "$component is not registered"
|
||||
done
|
||||
|
||||
qs ipc call calendar-agenda reset >/dev/null
|
||||
jq -e '.fixture == false' <<<"$(qs ipc call calendar-agenda status)" >/dev/null \
|
||||
|| fail 'fixture reset did not restore live mode'
|
||||
|
||||
Reference in New Issue
Block a user