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:
314
Services/CalendarService.qml
Normal file
314
Services/CalendarService.qml
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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,44 +160,72 @@ PanelWindow {
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: theme.spacingM
|
||||
spacing: theme.spacingM
|
||||
|
||||
// Left section for widgets
|
||||
Column {
|
||||
id: leftWidgets
|
||||
width: hasAnyWidgets ? parent.width * 0.45 : 0
|
||||
height: childrenRect.height
|
||||
// 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
|
||||
visible: hasAnyWidgets
|
||||
anchors.top: parent.top
|
||||
|
||||
property bool hasAnyWidgets: true || weather?.available // Always show media widget
|
||||
// Left section for widgets
|
||||
Column {
|
||||
id: leftWidgets
|
||||
width: hasAnyWidgets ? parent.width * 0.45 : 0
|
||||
height: childrenRect.height
|
||||
spacing: theme.spacingM
|
||||
visible: hasAnyWidgets
|
||||
anchors.top: parent.top
|
||||
|
||||
MediaPlayerWidget {
|
||||
visible: true // Always visible - shows placeholder when no media
|
||||
width: parent.width
|
||||
height: 160
|
||||
theme: centerCommandCenter.theme
|
||||
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 {
|
||||
visible: weather?.available
|
||||
width: parent.width
|
||||
height: weather ? 140 : 80
|
||||
// Right section for calendar
|
||||
CalendarWidget {
|
||||
id: calendarWidget
|
||||
width: leftWidgets.hasAnyWidgets ? parent.width * 0.55 - theme.spacingL : parent.width
|
||||
height: parent.height
|
||||
theme: centerCommandCenter.theme
|
||||
weather: centerCommandCenter.weather
|
||||
useFahrenheit: centerCommandCenter.useFahrenheit
|
||||
}
|
||||
}
|
||||
|
||||
// Right section for calendar
|
||||
CalendarWidget {
|
||||
width: leftWidgets.hasAnyWidgets ? parent.width * 0.55 - theme.spacingL : parent.width
|
||||
height: parent.height
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
316
Widgets/CenterCommandCenter/EventsWidget.qml
Normal file
316
Widgets/CenterCommandCenter/EventsWidget.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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: ""
|
||||
|
||||
Reference in New Issue
Block a user