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

@@ -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,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
MediaPlayerWidget {
visible: true // Always visible - shows placeholder when no media
width: parent.width
height: 160
theme: centerCommandCenter.theme
// 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
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()
}
}
}
}

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
MediaPlayerWidget 1.0 MediaPlayerWidget.qml
WeatherWidget 1.0 WeatherWidget.qml
CalendarWidget 1.0 CalendarWidget.qml
CalendarWidget 1.0 CalendarWidget.qml
EventsWidget 1.0 EventsWidget.qml