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

@@ -11,4 +11,5 @@ singleton SystemMonitorService 1.0 SystemMonitorService.qml
singleton AppSearchService 1.0 AppSearchService.qml singleton AppSearchService 1.0 AppSearchService.qml
singleton PreferencesService 1.0 PreferencesService.qml singleton PreferencesService 1.0 PreferencesService.qml
singleton LauncherService 1.0 LauncherService.qml singleton LauncherService 1.0 LauncherService.qml
singleton NiriWorkspaceService 1.0 NiriWorkspaceService.qml singleton NiriWorkspaceService 1.0 NiriWorkspaceService.qml
singleton CalendarService 1.0 CalendarService.qml

View File

@@ -1,6 +1,8 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Effects
import "../../Common" import "../../Common"
import "../../Services"
Column { Column {
id: calendarWidget id: calendarWidget
@@ -11,6 +13,46 @@ Column {
spacing: theme.spacingM 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 // Month navigation header
Row { Row {
width: parent.width width: parent.width
@@ -158,6 +200,64 @@ Column {
font.weight: isToday || isSelected ? Font.Medium : Font.Normal 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 { MouseArea {
id: dayArea id: dayArea
anchors.fill: parent anchors.fill: parent

View File

@@ -69,19 +69,23 @@ PanelWindow {
function calculateHeight() { function calculateHeight() {
let contentHeight = theme.spacingM * 2 // margins 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 let widgetHeight = 160 // Media widget always present
if (weather?.available) { if (weather?.available) {
widgetHeight += (weather ? 140 : 80) + theme.spacingM widgetHeight += (weather ? 140 : 80) + theme.spacingM
} }
// Calendar height is always 300
let calendarHeight = 300 let calendarHeight = 300
let mainRowHeight = Math.max(widgetHeight, calendarHeight)
// Take the max of widgets and calendar contentHeight += mainRowHeight + theme.spacingM // Add spacing between main row and events
contentHeight += Math.max(widgetHeight, calendarHeight)
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 color: theme.surfaceContainer
@@ -123,6 +127,18 @@ PanelWindow {
opacity: root.calendarVisible ? 1.0 : 0.0 opacity: root.calendarVisible ? 1.0 : 0.0
scale: root.calendarVisible ? 1.0 : 0.92 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 { Behavior on opacity {
NumberAnimation { NumberAnimation {
duration: theme.longDuration duration: theme.longDuration
@@ -144,44 +160,72 @@ PanelWindow {
} }
} }
Row { Column {
anchors.fill: parent anchors.fill: parent
anchors.margins: theme.spacingM anchors.margins: theme.spacingM
spacing: theme.spacingM spacing: theme.spacingM
// Left section for widgets // Main row with widgets and calendar
Column { Row {
id: leftWidgets width: parent.width
width: hasAnyWidgets ? parent.width * 0.45 : 0 height: {
height: childrenRect.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 spacing: theme.spacingM
visible: hasAnyWidgets
anchors.top: parent.top
property bool hasAnyWidgets: true || weather?.available // Always show media widget // Left section for widgets
Column {
MediaPlayerWidget { id: leftWidgets
visible: true // Always visible - shows placeholder when no media width: hasAnyWidgets ? parent.width * 0.45 : 0
width: parent.width height: childrenRect.height
height: 160 spacing: theme.spacingM
theme: centerCommandCenter.theme visible: hasAnyWidgets
anchors.top: parent.top
property bool hasAnyWidgets: true || weather?.available // Always show media widget
MediaPlayerWidget {
visible: true // Always visible - shows placeholder when no media
width: parent.width
height: 160
theme: centerCommandCenter.theme
}
WeatherWidget {
visible: weather?.available
width: parent.width
height: weather ? 140 : 80
theme: centerCommandCenter.theme
weather: centerCommandCenter.weather
useFahrenheit: centerCommandCenter.useFahrenheit
}
} }
WeatherWidget { // Right section for calendar
visible: weather?.available CalendarWidget {
width: parent.width id: calendarWidget
height: weather ? 140 : 80 width: leftWidgets.hasAnyWidgets ? parent.width * 0.55 - theme.spacingL : parent.width
height: parent.height
theme: centerCommandCenter.theme theme: centerCommandCenter.theme
weather: centerCommandCenter.weather
useFahrenheit: centerCommandCenter.useFahrenheit
} }
} }
// Right section for calendar // Full-width events widget below
CalendarWidget { EventsWidget {
width: leftWidgets.hasAnyWidgets ? parent.width * 0.55 - theme.spacingL : parent.width id: eventsWidget
height: parent.height width: parent.width
theme: centerCommandCenter.theme theme: centerCommandCenter.theme
selectedDate: calendarWidget.selectedDate
// Update container height when events widget height changes
onHeightChanged: {
mainContainer.height = mainContainer.calculateHeight()
}
} }
} }
} }

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

@@ -1,4 +1,5 @@
CenterCommandCenter 1.0 CenterCommandCenter.qml CenterCommandCenter 1.0 CenterCommandCenter.qml
MediaPlayerWidget 1.0 MediaPlayerWidget.qml MediaPlayerWidget 1.0 MediaPlayerWidget.qml
WeatherWidget 1.0 WeatherWidget.qml WeatherWidget 1.0 WeatherWidget.qml
CalendarWidget 1.0 CalendarWidget.qml CalendarWidget 1.0 CalendarWidget.qml
EventsWidget 1.0 EventsWidget.qml

View File

@@ -80,7 +80,69 @@ PanelWindow {
anchors.verticalCenter: parent.verticalCenter 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 // Tab buttons

View File

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