1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-07 14:05:38 -05:00

Implement calendar events with khal

This commit is contained in:
bbedward
2025-07-12 13:26:09 -04:00
parent 095606f6e9
commit 8a2b81aafb
8 changed files with 876 additions and 34 deletions

View File

@@ -0,0 +1,314 @@
import QtQuick
import Quickshell
import Quickshell.Io
pragma Singleton
pragma ComponentBehavior: Bound
Singleton {
id: root
property bool khalAvailable: false
property var eventsByDate: ({})
property bool isLoading: false
property string lastError: ""
// Periodic refresh timer (5 minutes)
Timer {
id: refreshTimer
interval: 300000 // 5 minutes
running: root.khalAvailable
repeat: true
onTriggered: {
if (lastStartDate && lastEndDate) {
loadEvents(lastStartDate, lastEndDate)
}
}
}
property date lastStartDate
property date lastEndDate
// Process for checking khal configuration
Process {
id: khalCheckProcess
command: ["khal", "list", "today"]
running: false
onExited: (exitCode) => {
root.khalAvailable = (exitCode === 0)
if (exitCode !== 0) {
console.warn("CalendarService: khal not configured (exit code:", exitCode, ")")
} else {
console.log("CalendarService: khal configured and available")
// Load current month events when khal becomes available
loadCurrentMonth()
}
}
onStarted: {
console.log("CalendarService: Checking khal configuration...")
}
}
// Process for loading events
Process {
id: eventsProcess
running: false
property date requestStartDate
property date requestEndDate
property string rawOutput: ""
stdout: SplitParser {
splitMarker: "\n"
onRead: (data) => {
eventsProcess.rawOutput += data + "\n"
}
}
onExited: (exitCode) => {
root.isLoading = false
if (exitCode !== 0) {
root.lastError = "Failed to load events (exit code: " + exitCode + ")"
console.warn("CalendarService:", root.lastError)
return
}
try {
let newEventsByDate = {}
let lines = eventsProcess.rawOutput.split('\n')
for (let line of lines) {
line = line.trim()
if (!line || line === "[]") continue
// Parse JSON line
let dayEvents = JSON.parse(line)
// Process each event in this day's array
for (let event of dayEvents) {
if (!event.title) continue
// Parse start and end dates
let startDate, endDate
if (event['start-date']) {
let startParts = event['start-date'].split('/')
startDate = new Date(parseInt(startParts[2]), parseInt(startParts[0]) - 1, parseInt(startParts[1]))
} else {
startDate = new Date()
}
if (event['end-date']) {
let endParts = event['end-date'].split('/')
endDate = new Date(parseInt(endParts[2]), parseInt(endParts[0]) - 1, parseInt(endParts[1]))
} else {
endDate = new Date(startDate)
}
// Create start/end times
let startTime = new Date(startDate)
let endTime = new Date(endDate)
if (event['start-time'] && event['all-day'] !== "True") {
// Parse time if available and not all-day
let timeStr = event['start-time']
if (timeStr) {
let timeParts = timeStr.match(/(\d+):(\d+)/)
if (timeParts) {
startTime.setHours(parseInt(timeParts[1]), parseInt(timeParts[2]))
if (event['end-time']) {
let endTimeParts = event['end-time'].match(/(\d+):(\d+)/)
if (endTimeParts) {
endTime.setHours(parseInt(endTimeParts[1]), parseInt(endTimeParts[2]))
}
} else {
// Default to 1 hour duration on same day
endTime = new Date(startTime)
endTime.setHours(startTime.getHours() + 1)
}
}
}
}
// Create unique ID for this event (to track multi-day events)
let eventId = event.title + "_" + event['start-date'] + "_" + (event['start-time'] || 'allday')
// Create event object template
let eventTemplate = {
id: eventId,
title: event.title || "Untitled Event",
start: startTime,
end: endTime,
location: event.location || "",
description: event.description || "",
url: event.url || "",
calendar: "",
color: "",
allDay: event['all-day'] === "True",
isMultiDay: startDate.toDateString() !== endDate.toDateString()
}
// Add event to each day it spans
let currentDate = new Date(startDate)
while (currentDate <= endDate) {
let dateKey = Qt.formatDate(currentDate, "yyyy-MM-dd")
if (!newEventsByDate[dateKey]) {
newEventsByDate[dateKey] = []
}
// Check if this exact event is already added to this date (prevent duplicates)
let existingEvent = newEventsByDate[dateKey].find(e => e.id === eventId)
if (existingEvent) {
// Move to next day without adding duplicate
currentDate.setDate(currentDate.getDate() + 1)
continue
}
// Create a copy of the event for this date
let dayEvent = Object.assign({}, eventTemplate)
// For multi-day events, adjust the display time for this specific day
if (currentDate.getTime() === startDate.getTime()) {
// First day - use original start time
dayEvent.start = new Date(startTime)
} else {
// Subsequent days - start at beginning of day for all-day events
dayEvent.start = new Date(currentDate)
if (!dayEvent.allDay) {
dayEvent.start.setHours(0, 0, 0, 0)
}
}
if (currentDate.getTime() === endDate.getTime()) {
// Last day - use original end time
dayEvent.end = new Date(endTime)
} else {
// Earlier days - end at end of day for all-day events
dayEvent.end = new Date(currentDate)
if (!dayEvent.allDay) {
dayEvent.end.setHours(23, 59, 59, 999)
}
}
newEventsByDate[dateKey].push(dayEvent)
// Move to next day
currentDate.setDate(currentDate.getDate() + 1)
}
}
}
// Sort events by start time within each date
for (let dateKey in newEventsByDate) {
newEventsByDate[dateKey].sort((a, b) => a.start.getTime() - b.start.getTime())
}
root.eventsByDate = newEventsByDate
root.lastError = ""
console.log("CalendarService: Loaded events for", Object.keys(newEventsByDate).length, "days")
// Debug: log parsed events
for (let dateKey in newEventsByDate) {
console.log(" " + dateKey + ":", newEventsByDate[dateKey].map(e => e.title).join(", "))
}
} catch (error) {
root.lastError = "Failed to parse events JSON: " + error.toString()
console.error("CalendarService:", root.lastError)
console.error("CalendarService: Raw output was:", eventsProcess.rawOutput)
root.eventsByDate = {}
}
// Reset for next run
eventsProcess.rawOutput = ""
}
onStarted: {
console.log("CalendarService: Loading events from", Qt.formatDate(requestStartDate, "yyyy-MM-dd"),
"to", Qt.formatDate(requestEndDate, "yyyy-MM-dd"))
}
}
function checkKhalAvailability() {
if (!khalCheckProcess.running) {
khalCheckProcess.running = true
}
}
function loadCurrentMonth() {
if (!root.khalAvailable) return
let today = new Date()
let firstDay = new Date(today.getFullYear(), today.getMonth(), 1)
let lastDay = new Date(today.getFullYear(), today.getMonth() + 1, 0)
// Add padding
let startDate = new Date(firstDay)
startDate.setDate(startDate.getDate() - firstDay.getDay() - 7)
let endDate = new Date(lastDay)
endDate.setDate(endDate.getDate() + (6 - lastDay.getDay()) + 7)
loadEvents(startDate, endDate)
}
function loadEvents(startDate, endDate) {
if (!root.khalAvailable) {
console.warn("CalendarService: khal not available, skipping event loading")
return
}
if (eventsProcess.running) {
console.log("CalendarService: Event loading already in progress, skipping...")
return
}
// Store last requested date range for refresh timer
root.lastStartDate = startDate
root.lastEndDate = endDate
root.isLoading = true
// Format dates for khal (MM/dd/yyyy based on printformats)
let startDateStr = Qt.formatDate(startDate, "MM/dd/yyyy")
let endDateStr = Qt.formatDate(endDate, "MM/dd/yyyy")
eventsProcess.requestStartDate = startDate
eventsProcess.requestEndDate = endDate
eventsProcess.command = [
"khal", "list",
"--json", "title",
"--json", "description",
"--json", "start-date",
"--json", "start-time",
"--json", "end-date",
"--json", "end-time",
"--json", "all-day",
"--json", "location",
"--json", "url",
startDateStr, endDateStr
]
eventsProcess.running = true
}
function getEventsForDate(date) {
let dateKey = Qt.formatDate(date, "yyyy-MM-dd")
return root.eventsByDate[dateKey] || []
}
function hasEventsForDate(date) {
let events = getEventsForDate(date)
return events.length > 0
}
// Initialize on component completion
Component.onCompleted: {
console.log("CalendarService: Component completed, initializing...")
checkKhalAvailability()
}
}

View File

@@ -12,3 +12,4 @@ singleton AppSearchService 1.0 AppSearchService.qml
singleton PreferencesService 1.0 PreferencesService.qml
singleton LauncherService 1.0 LauncherService.qml
singleton NiriWorkspaceService 1.0 NiriWorkspaceService.qml
singleton CalendarService 1.0 CalendarService.qml

View File

@@ -1,6 +1,8 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import "../../Common"
import "../../Services"
Column {
id: calendarWidget
@@ -11,6 +13,46 @@ Column {
spacing: theme.spacingM
// Load events when display date changes
onDisplayDateChanged: {
loadEventsForMonth()
}
// Load events when calendar service becomes available
Connections {
target: CalendarService
enabled: CalendarService !== null
function onKhalAvailableChanged() {
if (CalendarService && CalendarService.khalAvailable) {
loadEventsForMonth()
}
}
}
Component.onCompleted: {
console.log("CalendarWidget: Component completed, CalendarService available:", !!CalendarService)
if (CalendarService) {
console.log("CalendarWidget: khal available:", CalendarService.khalAvailable)
}
loadEventsForMonth()
}
function loadEventsForMonth() {
if (!CalendarService || !CalendarService.khalAvailable) return
// Calculate date range with padding
let firstDay = new Date(displayDate.getFullYear(), displayDate.getMonth(), 1)
let dayOfWeek = firstDay.getDay()
let startDate = new Date(firstDay)
startDate.setDate(startDate.getDate() - dayOfWeek - 7) // Extra week padding
let lastDay = new Date(displayDate.getFullYear(), displayDate.getMonth() + 1, 0)
let endDate = new Date(lastDay)
endDate.setDate(endDate.getDate() + (6 - lastDay.getDay()) + 7) // Extra week padding
CalendarService.loadEvents(startDate, endDate)
}
// Month navigation header
Row {
width: parent.width
@@ -158,6 +200,64 @@ Column {
font.weight: isToday || isSelected ? Font.Medium : Font.Normal
}
// Event indicator - full-width elegant bar
Rectangle {
id: eventIndicator
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 2
height: 3
radius: 1.5
visible: CalendarService && CalendarService.khalAvailable && CalendarService.hasEventsForDate(dayDate)
// Dynamic color based on state with opacity
color: {
if (isSelected) {
// Use a lighter tint of primary for selected state
return Qt.lighter(theme.primary, 1.3)
} else if (isToday) {
return theme.primary
} else {
return theme.primary
}
}
opacity: {
if (isSelected) {
return 0.9
} else if (isToday) {
return 0.8
} else {
return 0.6
}
}
// Subtle animation on hover
scale: dayArea.containsMouse ? 1.05 : 1.0
Behavior on scale {
NumberAnimation {
duration: theme.shortDuration
easing.type: theme.standardEasing
}
}
Behavior on color {
ColorAnimation {
duration: theme.shortDuration
easing.type: theme.standardEasing
}
}
Behavior on opacity {
NumberAnimation {
duration: theme.shortDuration
easing.type: theme.standardEasing
}
}
}
MouseArea {
id: dayArea
anchors.fill: parent

View File

@@ -69,19 +69,23 @@ PanelWindow {
function calculateHeight() {
let contentHeight = theme.spacingM * 2 // margins
// Calculate widget heights - media widget is always present
// Main row with widgets and calendar
let widgetHeight = 160 // Media widget always present
if (weather?.available) {
widgetHeight += (weather ? 140 : 80) + theme.spacingM
}
// Calendar height is always 300
let calendarHeight = 300
let mainRowHeight = Math.max(widgetHeight, calendarHeight)
// Take the max of widgets and calendar
contentHeight += Math.max(widgetHeight, calendarHeight)
contentHeight += mainRowHeight + theme.spacingM // Add spacing between main row and events
return Math.min(contentHeight, parent.height * 0.85)
// Add events widget height - dynamically calculated
if (CalendarService && CalendarService.khalAvailable) {
let eventsHeight = eventsWidget.height || 120 // Use actual widget height or fallback
contentHeight += eventsHeight
}
return Math.min(contentHeight, parent.height * 0.9)
}
color: theme.surfaceContainer
@@ -123,6 +127,18 @@ PanelWindow {
opacity: root.calendarVisible ? 1.0 : 0.0
scale: root.calendarVisible ? 1.0 : 0.92
// Update height when calendar service events change
Connections {
target: CalendarService
enabled: CalendarService !== null
function onEventsByDateChanged() {
mainContainer.height = mainContainer.calculateHeight()
}
function onKhalAvailableChanged() {
mainContainer.height = mainContainer.calculateHeight()
}
}
Behavior on opacity {
NumberAnimation {
duration: theme.longDuration
@@ -144,11 +160,24 @@ PanelWindow {
}
}
Row {
Column {
anchors.fill: parent
anchors.margins: theme.spacingM
spacing: theme.spacingM
// Main row with widgets and calendar
Row {
width: parent.width
height: {
let widgetHeight = 160 // Media widget always present
if (weather?.available) {
widgetHeight += (weather ? 140 : 80) + theme.spacingM
}
let calendarHeight = 300
return Math.max(widgetHeight, calendarHeight)
}
spacing: theme.spacingM
// Left section for widgets
Column {
id: leftWidgets
@@ -179,11 +208,26 @@ PanelWindow {
// Right section for calendar
CalendarWidget {
id: calendarWidget
width: leftWidgets.hasAnyWidgets ? parent.width * 0.55 - theme.spacingL : parent.width
height: parent.height
theme: centerCommandCenter.theme
}
}
// Full-width events widget below
EventsWidget {
id: eventsWidget
width: parent.width
theme: centerCommandCenter.theme
selectedDate: calendarWidget.selectedDate
// Update container height when events widget height changes
onHeightChanged: {
mainContainer.height = mainContainer.calculateHeight()
}
}
}
}
MouseArea {

View File

@@ -0,0 +1,316 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import "../../Common"
import "../../Services"
// Events widget for selected date - Material Design 3 style
Rectangle {
id: eventsWidget
property var theme: Theme
property date selectedDate: new Date()
property var selectedDateEvents: []
property bool hasEvents: selectedDateEvents && selectedDateEvents.length > 0
onSelectedDateEventsChanged: {
console.log("EventsWidget: selectedDateEvents changed, count:", selectedDateEvents.length)
eventsList.model = selectedDateEvents
}
property bool shouldShow: CalendarService && CalendarService.khalAvailable
width: parent.width
height: shouldShow ? (hasEvents ? Math.min(300, 80 + selectedDateEvents.length * 60) : 120) : 0
radius: theme.cornerRadiusLarge
color: Qt.rgba(theme.surfaceVariant.r, theme.surfaceVariant.g, theme.surfaceVariant.b, 0.12)
border.color: Qt.rgba(theme.outline.r, theme.outline.g, theme.outline.b, 0.08)
border.width: 1
visible: shouldShow
// Material elevation shadow
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 2
shadowBlur: 0.25
shadowColor: Qt.rgba(0, 0, 0, 0.1)
shadowOpacity: 0.1
}
Behavior on height {
NumberAnimation {
duration: theme.mediumDuration
easing.type: theme.emphasizedEasing
}
}
// Update events when selected date or events change
Connections {
target: CalendarService
enabled: CalendarService !== null
function onEventsByDateChanged() {
updateSelectedDateEvents()
}
function onKhalAvailableChanged() {
updateSelectedDateEvents()
}
}
Component.onCompleted: {
updateSelectedDateEvents()
}
onSelectedDateChanged: {
updateSelectedDateEvents()
}
function updateSelectedDateEvents() {
if (CalendarService && CalendarService.khalAvailable) {
let events = CalendarService.getEventsForDate(selectedDate)
console.log("EventsWidget: Updating events for", Qt.formatDate(selectedDate, "yyyy-MM-dd"), "found", events.length, "events")
selectedDateEvents = events
} else {
selectedDateEvents = []
}
}
Item {
anchors.fill: parent
anchors.margins: theme.spacingL
Column {
anchors.fill: parent
spacing: theme.spacingM
// Header - always visible when widget is shown
Item {
width: parent.width
height: headerRow.height
Row {
id: headerRow
width: parent.width
spacing: theme.spacingS
Text {
text: "event"
font.family: theme.iconFont
font.pixelSize: theme.iconSize - 2
color: theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: hasEvents ?
(Qt.formatDate(selectedDate, "MMM d") + " • " +
(selectedDateEvents.length === 1 ? "1 event" : selectedDateEvents.length + " events")) :
Qt.formatDate(selectedDate, "MMM d")
font.pixelSize: theme.fontSizeMedium
color: theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
}
// Content area
Rectangle {
anchors.fill: parent
anchors.margins: theme.spacingL
color: "transparent"
// No events placeholder - absolutely centered in this gray container
Column {
anchors.centerIn: parent
spacing: theme.spacingXS
visible: !hasEvents
Text {
text: "event_busy"
font.family: theme.iconFont
font.pixelSize: theme.iconSize + 8
color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.3)
anchors.horizontalCenter: parent.horizontalCenter
}
Text {
text: "No events"
font.pixelSize: theme.fontSizeMedium
color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.5)
font.weight: Font.Normal
anchors.horizontalCenter: parent.horizontalCenter
}
}
// Events list
ListView {
id: eventsList
anchors.fill: parent
anchors.margins: theme.spacingM
visible: hasEvents
clip: true
spacing: theme.spacingS
boundsMovement: Flickable.StopAtBounds
boundsBehavior: Flickable.StopAtBounds
ScrollBar.vertical: ScrollBar {
policy: eventsList.contentHeight > eventsList.height ? ScrollBar.AsNeeded : ScrollBar.AlwaysOff
}
delegate: Rectangle {
width: eventsList.width
height: eventContent.implicitHeight + theme.spacingM
radius: theme.cornerRadius
color: {
if (modelData.url && eventMouseArea.containsMouse) {
return Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.12)
} else if (eventMouseArea.containsMouse) {
return Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.06)
}
return Qt.rgba(theme.surfaceVariant.r, theme.surfaceVariant.g, theme.surfaceVariant.b, 0.06)
}
border.color: {
if (modelData.url && eventMouseArea.containsMouse) {
return Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.3)
} else if (eventMouseArea.containsMouse) {
return Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.15)
}
return "transparent"
}
border.width: 1
// Event indicator strip
Rectangle {
width: 4
height: parent.height - 8
anchors.left: parent.left
anchors.leftMargin: 4
anchors.verticalCenter: parent.verticalCenter
radius: 2
color: theme.primary
opacity: 0.8
}
Column {
id: eventContent
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: theme.spacingL + 4
anchors.rightMargin: theme.spacingM
spacing: 6
Text {
width: parent.width
text: modelData.title
font.pixelSize: theme.fontSizeMedium
color: theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
wrapMode: Text.Wrap
maximumLineCount: 2
}
Item {
width: parent.width
height: Math.max(timeRow.height, locationRow.height)
Row {
id: timeRow
spacing: 4
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
Text {
text: "schedule"
font.family: theme.iconFont
font.pixelSize: theme.fontSizeSmall
color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.7)
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: {
if (modelData.allDay) {
return "All day"
} else {
let startTime = Qt.formatTime(modelData.start, "h:mm AP")
if (modelData.start.toDateString() !== modelData.end.toDateString() ||
modelData.start.getTime() !== modelData.end.getTime()) {
return startTime + " " + Qt.formatTime(modelData.end, "h:mm AP")
}
return startTime
}
}
font.pixelSize: theme.fontSizeSmall
color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.7)
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
id: locationRow
spacing: 4
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
visible: modelData.location !== ""
Text {
text: "location_on"
font.family: theme.iconFont
font.pixelSize: theme.fontSizeSmall
color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.7)
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: modelData.location
font.pixelSize: theme.fontSizeSmall
color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.7)
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
maximumLineCount: 1
width: Math.min(implicitWidth, 200)
}
}
}
}
MouseArea {
id: eventMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: modelData.url ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: modelData.url !== ""
onClicked: {
if (modelData.url && modelData.url !== "") {
if (Qt.openUrlExternally(modelData.url) === false) {
console.warn("Couldn't open", modelData.url)
}
}
}
}
Behavior on color {
ColorAnimation {
duration: theme.shortDuration
easing.type: theme.standardEasing
}
}
Behavior on border.color {
ColorAnimation {
duration: theme.shortDuration
easing.type: theme.standardEasing
}
}
}
}
}
}
}
}

View File

@@ -2,3 +2,4 @@ CenterCommandCenter 1.0 CenterCommandCenter.qml
MediaPlayerWidget 1.0 MediaPlayerWidget.qml
WeatherWidget 1.0 WeatherWidget.qml
CalendarWidget 1.0 CalendarWidget.qml
EventsWidget 1.0 EventsWidget.qml

View File

@@ -80,7 +80,69 @@ PanelWindow {
anchors.verticalCenter: parent.verticalCenter
}
Item { width: parent.width - 200; height: 1 }
Item { width: parent.width - 300; height: 1 }
// Calendar status indicator
Rectangle {
width: 100
height: 24
radius: Theme.cornerRadiusSmall
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.16)
anchors.verticalCenter: parent.verticalCenter
visible: CalendarService && CalendarService.khalAvailable
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
Text {
text: "event"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 6
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Text {
id: todayEventsText
property var todayEvents: []
text: todayEvents.length === 0 ? "No events today" :
todayEvents.length === 1 ? "1 event today" :
todayEvents.length + " events today"
function updateTodayEvents() {
if (CalendarService && CalendarService.khalAvailable) {
todayEvents = CalendarService.getEventsForDate(new Date())
} else {
todayEvents = []
}
}
Component.onCompleted: {
console.log("ControlCenter: Calendar status text initialized, CalendarService available:", !!CalendarService)
if (CalendarService) {
console.log("ControlCenter: khal available:", CalendarService.khalAvailable)
}
updateTodayEvents()
}
font.pixelSize: Theme.fontSizeXS
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
// Update when events change or khal becomes available
Connections {
target: CalendarService
enabled: CalendarService !== null
function onEventsByDateChanged() {
todayEventsText.updateTodayEvents()
}
function onKhalAvailableChanged() {
todayEventsText.updateTodayEvents()
}
}
}
}
}
}
// Tab buttons

View File

@@ -78,6 +78,10 @@ ShellRoot {
// Brightness properties from BrightnessService
property int brightnessLevel: BrightnessService.brightnessLevel
// Calendar properties from CalendarService
property bool calendarAvailable: CalendarService.khalAvailable
property var calendarEvents: CalendarService.eventsByDate
// WiFi password dialog
property bool wifiPasswordDialogVisible: false
property string wifiPasswordSSID: ""