Personal noctalia plugins collection
1import QtQuick
2import QtQuick.Controls
3import QtQuick.Layouts
4import Quickshell
5import qs.Commons
6import qs.Services.Location
7import qs.Services.UI
8import qs.Widgets
9
10Item {
11 id: root
12 property var pluginApi: null
13 readonly property var mainInstance: pluginApi?.mainInstance
14 readonly property var geometryPlaceholder: panelContainer
15 property real contentPreferredWidth: 950 * Style.uiScaleRatio
16 property real contentPreferredHeight: 900 * Style.uiScaleRatio
17 property real topHeaderHeight: 60 * Style.uiScaleRatio
18 readonly property bool allowAttach: mainInstance ? mainInstance.panelModeSetting === "attached" : false
19 readonly property bool panelAnchorHorizontalCenter: mainInstance ? mainInstance.panelModeSetting === "centered" : false
20 readonly property bool panelAnchorVerticalCenter: mainInstance ? mainInstance.panelModeSetting === "centered" : false
21 anchors.fill: parent
22
23 property bool showCreateDialog: false
24 property bool showCreateTaskDialog: false
25
26 property real defaultHourHeight: 50 * Style.uiScaleRatio
27 property real minHourHeight: 32 * Style.uiScaleRatio
28 property real hourHeight: defaultHourHeight
29 property real timeColumnWidth: 65 * Style.uiScaleRatio
30 property real daySpacing: 1 * Style.uiScaleRatio
31
32 // Panel doesn't need its own CalendarService connection - Main.qml handles it.
33 // When panel opens, trigger a fresh load if needed.
34 Component.onCompleted: {
35 mainInstance?.initializePlugin()
36 Qt.callLater(root.adjustHourHeightForViewport)
37 }
38 onVisibleChanged: if (visible && mainInstance) {
39 mainInstance.refreshView()
40 mainInstance.goToToday()
41 Qt.callLater(root.scrollToCurrentTime)
42 mainInstance.loadTodos()
43 Qt.callLater(root.adjustHourHeightForViewport)
44 }
45
46 function adjustHourHeightForViewport() {
47 if (!calendarFlickable || calendarFlickable.height <= 0) return
48 // Target showing 08:30–24:00 (~15.5 hours) without scroll; fall back to min height if space is tight.
49 var target = calendarFlickable.height / 15.5
50 var newHeight = Math.max(minHourHeight, Math.min(defaultHourHeight, target))
51 if (Math.abs(newHeight - hourHeight) > 0.5) hourHeight = newHeight
52 }
53
54 // Scroll to time indicator position
55 function scrollToCurrentTime() {
56 if (!mainInstance || !calendarFlickable) return
57 var now = new Date(), today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
58 var weekStart = new Date(mainInstance.weekStart)
59 var weekEnd = new Date(weekStart.getFullYear(), weekStart.getMonth(), weekStart.getDate() + 7)
60
61 if (today >= weekStart && today < weekEnd) {
62 var currentHour = now.getHours() + now.getMinutes() / 60
63 var scrollPos = (currentHour * hourHeight) - (calendarFlickable.height / 2)
64 var maxScroll = Math.max(0, (24 * hourHeight) - calendarFlickable.height)
65 scrollAnim.targetY = Math.max(0, Math.min(scrollPos, maxScroll))
66 scrollAnim.start()
67 }
68 }
69
70 // Event creation dialog
71 Rectangle {
72 id: createEventOverlay
73 anchors.fill: parent
74 color: Qt.rgba(0, 0, 0, 0.5)
75 visible: showCreateDialog
76 z: 2000
77
78 MouseArea { anchors.fill: parent; onClicked: showCreateDialog = false }
79
80 Rectangle {
81 anchors.centerIn: parent
82 width: 400 * Style.uiScaleRatio
83 height: createDialogColumn.implicitHeight + 2 * Style.marginM
84 color: Color.mSurface
85 radius: Style.radiusM
86
87 MouseArea { anchors.fill: parent } // block clicks through
88
89 ColumnLayout {
90 id: createDialogColumn
91 anchors.fill: parent
92 anchors.margins: Style.marginM
93 spacing: Style.marginS
94
95 NText {
96 text: pluginApi.tr("panel.add_event")
97 font.pointSize: Style.fontSizeL; font.weight: Font.Bold
98 color: Color.mOnSurface
99 }
100
101 NText { text: pluginApi.tr("panel.summary"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
102 TextField {
103 id: createEventSummary
104 Layout.fillWidth: true
105 placeholderText: pluginApi.tr("panel.summary")
106 color: Color.mOnSurface
107 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS }
108 }
109
110 NText { text: pluginApi.tr("panel.date"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
111 TextField {
112 id: createEventDate
113 Layout.fillWidth: true
114 placeholderText: "YYYY-MM-DD"
115 color: Color.mOnSurface
116 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS }
117 }
118
119 RowLayout {
120 spacing: Style.marginS
121 ColumnLayout {
122 Layout.fillWidth: true
123 NText { text: pluginApi.tr("panel.start_time"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
124 TextField {
125 id: createEventStartTime
126 Layout.fillWidth: true
127 placeholderText: "HH:MM"
128 color: Color.mOnSurface
129 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS }
130 }
131 }
132 ColumnLayout {
133 Layout.fillWidth: true
134 NText { text: pluginApi.tr("panel.end_time"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
135 TextField {
136 id: createEventEndTime
137 Layout.fillWidth: true
138 placeholderText: "HH:MM"
139 color: Color.mOnSurface
140 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS }
141 }
142 }
143 }
144
145 NText { text: pluginApi.tr("panel.location"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
146 TextField {
147 id: createEventLocation
148 Layout.fillWidth: true
149 placeholderText: pluginApi.tr("panel.location")
150 color: Color.mOnSurface
151 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS }
152 }
153
154 NText { text: pluginApi.tr("panel.description"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
155 TextField {
156 id: createEventDescription
157 Layout.fillWidth: true
158 placeholderText: pluginApi.tr("panel.description")
159 color: Color.mOnSurface
160 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS }
161 }
162
163 NText { text: pluginApi.tr("panel.calendar_select"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
164 ComboBox {
165 id: calendarSelector
166 Layout.fillWidth: true
167 model: CalendarService.calendars || []
168 textRole: "name"
169 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS }
170 }
171
172 RowLayout {
173 Layout.fillWidth: true
174 spacing: Style.marginS
175
176 Item { Layout.fillWidth: true }
177
178 Rectangle {
179 Layout.preferredWidth: cancelBtn.implicitWidth + 2 * Style.marginM
180 Layout.preferredHeight: cancelBtn.implicitHeight + Style.marginS
181 color: Color.mSurfaceVariant; radius: Style.radiusS
182 NText {
183 id: cancelBtn; anchors.centerIn: parent
184 text: pluginApi.tr("panel.cancel"); color: Color.mOnSurfaceVariant
185 }
186 MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor; onClicked: showCreateDialog = false }
187 }
188
189 Rectangle {
190 Layout.preferredWidth: createBtn.implicitWidth + 2 * Style.marginM
191 Layout.preferredHeight: createBtn.implicitHeight + Style.marginS
192 color: Color.mPrimary; radius: Style.radiusS
193 opacity: createEventSummary.text.trim() !== "" ? 1.0 : 0.5
194 NText {
195 id: createBtn; anchors.centerIn: parent
196 text: pluginApi.tr("panel.create"); color: Color.mOnPrimary; font.weight: Font.Bold
197 }
198 MouseArea {
199 anchors.fill: parent; cursorShape: Qt.PointingHandCursor
200 onClicked: {
201 if (createEventSummary.text.trim() === "") return
202 var cal = CalendarService.calendars?.[calendarSelector.currentIndex]
203 var calUid = cal?.uid || ""
204 var dateParts = createEventDate.text.split("-")
205 var startParts = createEventStartTime.text.split(":")
206 var endParts = createEventEndTime.text.split(":")
207 var startDate = new Date(parseInt(dateParts[0]), parseInt(dateParts[1])-1, parseInt(dateParts[2]),
208 parseInt(startParts[0]), parseInt(startParts[1]), 0)
209 var endDate = new Date(parseInt(dateParts[0]), parseInt(dateParts[1])-1, parseInt(dateParts[2]),
210 parseInt(endParts[0]), parseInt(endParts[1]), 0)
211 mainInstance?.createEvent(calUid, createEventSummary.text.trim(),
212 Math.floor(startDate.getTime()/1000), Math.floor(endDate.getTime()/1000),
213 createEventLocation.text.trim(), createEventDescription.text.trim())
214 showCreateDialog = false
215 }
216 }
217 }
218 }
219 }
220 }
221 }
222
223 // Task creation dialog
224 Rectangle {
225 id: createTaskOverlay
226 anchors.fill: parent
227 color: Qt.rgba(0, 0, 0, 0.5)
228 visible: showCreateTaskDialog
229 z: 2000
230
231 MouseArea { anchors.fill: parent; onClicked: showCreateTaskDialog = false }
232
233 Rectangle {
234 anchors.centerIn: parent
235 width: 400 * Style.uiScaleRatio
236 height: createTaskDialogColumn.implicitHeight + 2 * Style.marginM
237 color: Color.mSurface
238 radius: Style.radiusM
239
240 MouseArea { anchors.fill: parent }
241
242 ColumnLayout {
243 id: createTaskDialogColumn
244 anchors.fill: parent
245 anchors.margins: Style.marginM
246 spacing: Style.marginS
247
248 NText {
249 text: pluginApi.tr("panel.add_task")
250 font.pointSize: Style.fontSizeL; font.weight: Font.Bold
251 color: Color.mOnSurface
252 }
253
254 NText { text: pluginApi.tr("panel.task_summary"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
255 TextField {
256 id: createTaskSummary
257 Layout.fillWidth: true
258 placeholderText: pluginApi.tr("panel.task_summary")
259 color: Color.mOnSurface
260 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS }
261 }
262
263 NText { text: pluginApi.tr("panel.due_date"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
264 TextField {
265 id: createTaskDueDate
266 Layout.fillWidth: true
267 placeholderText: "YYYY-MM-DD HH:MM"
268 color: Color.mOnSurface
269 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS }
270 }
271
272 NText { text: pluginApi.tr("panel.description"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
273 TextField {
274 id: createTaskDescription
275 Layout.fillWidth: true
276 placeholderText: pluginApi.tr("panel.description")
277 color: Color.mOnSurface
278 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS }
279 }
280
281 NText { text: pluginApi.tr("panel.priority"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
282 property int selectedPriority: 0
283 RowLayout {
284 spacing: Style.marginS
285 Repeater {
286 model: [
287 { label: pluginApi.tr("panel.priority_high"), value: 1 },
288 { label: pluginApi.tr("panel.priority_medium"), value: 5 },
289 { label: pluginApi.tr("panel.priority_low"), value: 9 }
290 ]
291 Rectangle {
292 Layout.preferredWidth: priLabel.implicitWidth + 2 * Style.marginM
293 Layout.preferredHeight: priLabel.implicitHeight + Style.marginS
294 color: createTaskDialogColumn.selectedPriority === modelData.value ? Color.mPrimary : Color.mSurfaceVariant
295 radius: Style.radiusS
296 NText {
297 id: priLabel; anchors.centerIn: parent
298 text: modelData.label
299 color: createTaskDialogColumn.selectedPriority === modelData.value ? Color.mOnPrimary : Color.mOnSurfaceVariant
300 font.weight: Font.Medium
301 }
302 MouseArea {
303 anchors.fill: parent; cursorShape: Qt.PointingHandCursor
304 onClicked: createTaskDialogColumn.selectedPriority =
305 createTaskDialogColumn.selectedPriority === modelData.value ? 0 : modelData.value
306 }
307 }
308 }
309 }
310
311 NText { text: pluginApi.tr("panel.task_list_select"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
312 ComboBox {
313 id: taskListSelector
314 Layout.fillWidth: true
315 model: mainInstance?.taskLists || []
316 textRole: "name"
317 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS }
318 }
319
320 RowLayout {
321 Layout.fillWidth: true
322 spacing: Style.marginS
323
324 Item { Layout.fillWidth: true }
325
326 Rectangle {
327 Layout.preferredWidth: taskCancelBtn.implicitWidth + 2 * Style.marginM
328 Layout.preferredHeight: taskCancelBtn.implicitHeight + Style.marginS
329 color: Color.mSurfaceVariant; radius: Style.radiusS
330 NText {
331 id: taskCancelBtn; anchors.centerIn: parent
332 text: pluginApi.tr("panel.cancel"); color: Color.mOnSurfaceVariant
333 }
334 MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor; onClicked: showCreateTaskDialog = false }
335 }
336
337 Rectangle {
338 Layout.preferredWidth: taskCreateBtn.implicitWidth + 2 * Style.marginM
339 Layout.preferredHeight: taskCreateBtn.implicitHeight + Style.marginS
340 color: Color.mPrimary; radius: Style.radiusS
341 opacity: createTaskSummary.text.trim() !== "" ? 1.0 : 0.5
342 NText {
343 id: taskCreateBtn; anchors.centerIn: parent
344 text: pluginApi.tr("panel.create"); color: Color.mOnPrimary; font.weight: Font.Bold
345 }
346 MouseArea {
347 anchors.fill: parent; cursorShape: Qt.PointingHandCursor
348 onClicked: {
349 if (createTaskSummary.text.trim() === "") return
350 var tl = mainInstance?.taskLists?.[taskListSelector.currentIndex]
351 var tlUid = tl?.uid || ""
352 var dueTs = 0
353 if (createTaskDueDate.text.trim() !== "") {
354 var d = new Date(createTaskDueDate.text.trim())
355 if (!isNaN(d.getTime())) dueTs = Math.floor(d.getTime() / 1000)
356 }
357 mainInstance?.createTodo(tlUid, createTaskSummary.text.trim(),
358 dueTs, createTaskDialogColumn.selectedPriority,
359 createTaskDescription.text.trim())
360 showCreateTaskDialog = false
361 }
362 }
363 }
364 }
365 }
366 }
367 }
368
369 // UI
370 Rectangle {
371 id: panelContainer
372 anchors.fill: parent
373 color: "transparent"
374
375 ColumnLayout {
376 anchors.fill: parent
377 anchors.margins: Style.marginM
378 spacing: Style.marginM
379
380 //Header Section
381 Rectangle {
382 id: header
383 Layout.fillWidth: true
384 Layout.preferredHeight: topHeaderHeight
385 color: Color.mSurfaceVariant
386 radius: Style.radiusM
387
388 RowLayout {
389 anchors.margins: Style.marginM
390 anchors.fill: parent
391
392 NIcon { icon: "calendar-week"; pointSize: Style.fontSizeXXL; color: Color.mPrimary }
393
394 ColumnLayout {
395 Layout.fillHeight: true
396 spacing: 0
397 NText {
398 text: pluginApi.tr("panel.header")
399 font.pointSize: Style.fontSizeL; font.weight: Font.Bold; color: Color.mOnSurface
400 }
401 RowLayout {
402 spacing: Style.marginS
403 NText {
404 text: mainInstance?.monthRangeText || ""
405 font.pointSize: Style.fontSizeS; font.weight: Font.Medium; color: Color.mOnSurfaceVariant
406 }
407 Rectangle {
408 Layout.preferredWidth: 8; Layout.preferredHeight: 8; radius: 4
409 color: mainInstance?.isLoading ? Color.mError :
410 mainInstance?.syncStatus?.includes("No") ? Color.mError : Color.mOnSurfaceVariant
411 }
412 NText {
413 text: mainInstance?.syncStatus || ""
414 font.pointSize: Style.fontSizeS; color: Color.mOnSurfaceVariant
415 }
416 }
417 }
418
419 Item { Layout.fillWidth: true }
420
421 RowLayout {
422 spacing: Style.marginS
423 NIconButton {
424 icon: "plus"; tooltipText: pluginApi.tr("panel.add_event")
425 onClicked: {
426 createEventSummary.text = ""
427 createEventLocation.text = ""
428 createEventDescription.text = ""
429 var now = new Date()
430 var startH = now.getHours() + 1
431 createEventDate.text = now.getFullYear() + "-" + String(now.getMonth()+1).padStart(2,'0') + "-" + String(now.getDate()).padStart(2,'0')
432 createEventStartTime.text = String(startH).padStart(2,'0') + ":00"
433 createEventEndTime.text = String(startH+1).padStart(2,'0') + ":00"
434 showCreateDialog = true
435 }
436 }
437 NIconButton {
438 icon: "clipboard-check"; tooltipText: pluginApi.tr("panel.add_task")
439 onClicked: {
440 createTaskSummary.text = ""
441 createTaskDueDate.text = ""
442 createTaskDescription.text = ""
443 createTaskDialogColumn.selectedPriority = 0
444 showCreateTaskDialog = true
445 }
446 }
447 NIconButton {
448 icon: mainInstance?.showCompletedTodos ? "eye-off" : "eye"
449 tooltipText: pluginApi.tr("panel.show_completed")
450 onClicked: {
451 if (mainInstance) {
452 mainInstance.showCompletedTodos = !mainInstance.showCompletedTodos
453 mainInstance.loadTodos()
454 }
455 }
456 }
457 NIconButton {
458 icon: "chevron-left"
459 onClicked: mainInstance?.navigateWeek(-7)
460 }
461 NIconButton {
462 icon: "calendar"; tooltipText: pluginApi.tr("panel.today")
463 onClicked: { mainInstance?.goToToday(); Qt.callLater(root.scrollToCurrentTime) }
464 }
465 NIconButton {
466 icon: "chevron-right"
467 onClicked: mainInstance?.navigateWeek(7)
468 }
469 NIconButton {
470 icon: "refresh"; tooltipText: I18n.tr("common.refresh")
471 onClicked: { mainInstance?.loadEvents(); mainInstance?.loadTodos() }
472 enabled: mainInstance ? !mainInstance.isLoading : false
473 }
474 NIconButton {
475 icon: "close"; tooltipText: I18n.tr("common.close")
476 onClicked: pluginApi.closePanel(pluginApi.panelOpenScreen)
477 }
478 }
479 }
480 }
481
482 // Calendar View
483 Rectangle {
484 Layout.fillWidth: true
485 Layout.fillHeight: true
486 color: Color.mSurfaceVariant
487 radius: Style.radiusM
488 clip: true
489
490 Column {
491 anchors.fill: parent
492 spacing: 0
493
494 //Day Headers
495 Rectangle {
496 id: dayHeaders
497 width: parent.width
498 height: 56
499 color: Color.mSurfaceVariant
500 radius: Style.radiusM
501
502 Row {
503 anchors.fill: parent
504 anchors.leftMargin: root.timeColumnWidth
505 spacing: root.daySpacing
506
507 Repeater {
508 model: 7
509 Rectangle {
510 width: mainInstance?.dayColumnWidth
511 height: parent.height
512 color: "transparent"
513 property date dayDate: mainInstance?.weekDates?.[index] || new Date()
514 property bool isToday: {
515 var today = new Date()
516 return dayDate.getDate() === today.getDate() &&
517 dayDate.getMonth() === today.getMonth() &&
518 dayDate.getFullYear() === today.getFullYear()
519 }
520 Rectangle {
521 anchors.fill: parent
522 anchors.margins: 4
523 color: Color.mSurfaceVariant
524 border.color: isToday ? Color.mPrimary : "transparent"
525 border.width: 2
526 radius: Style.radiusM
527 Column {
528 anchors.centerIn: parent
529 spacing: 2
530 NText {
531 anchors.horizontalCenter: parent.horizontalCenter
532 text: dayDate ? I18n.locale.dayName(dayDate.getDay(), Locale.ShortFormat).toUpperCase() : ""
533 color: isToday ? Color.mPrimary : Color.mOnSurface
534 font.pointSize: Style.fontSizeS; font.weight: Font.Medium
535 }
536 NText {
537 anchors.horizontalCenter: parent.horizontalCenter
538 text: dayDate ? ((dayDate.getDate() < 10 ? "0" : "") + dayDate.getDate()) : ""
539 color: isToday ? Color.mPrimary : Color.mOnSurface
540 font.pointSize: Style.fontSizeM; font.weight: Font.Bold
541 }
542 }
543 }
544 }
545 }
546 }
547 }
548 // All-day row
549 Rectangle {
550 id: allDayEventsSection
551 width: parent.width
552 height: mainInstance ? Math.round(mainInstance.allDaySectionHeight * Style.uiScaleRatio) : 0
553 color: Color.mSurfaceVariant
554 visible: height > 0
555
556 Item {
557 id: allDayEventsContainer
558 anchors.fill: parent
559 anchors.leftMargin: root.timeColumnWidth
560
561 Repeater {
562 model: 6
563 delegate: Rectangle {
564 width: 1; height: parent.height
565 x: (index + 1) * ((mainInstance?.dayColumnWidth) + (root.daySpacing)) - ((root.daySpacing) / 2)
566 color: Qt.alpha(mainInstance?.lineColor || Color.mOutline, mainInstance?.dayLineOpacitySetting || 0.9)
567 }
568 }
569
570 Repeater {
571 model: mainInstance?.allDayEventsWithLayout || []
572 delegate: Item {
573 property var eventData: modelData
574 property bool isTodoItem: eventData.isTodo || false
575 x: eventData.startDay * ((mainInstance?.dayColumnWidth) + (root.daySpacing))
576 y: eventData.lane * 25
577 width: (eventData.spanDays * ((mainInstance?.dayColumnWidth) + (root.daySpacing))) - (root.daySpacing)
578 height: 24
579
580 Rectangle {
581 anchors.fill: parent
582 color: isTodoItem ? Color.mSecondary : Color.mTertiary
583 radius: Style.radiusS
584 opacity: isTodoItem && eventData.todoStatus === "COMPLETED" ? 0.5 : 1.0
585 NText {
586 anchors.fill: parent; anchors.margins: 4
587 text: (isTodoItem ? (eventData.todoStatus === "COMPLETED" ? "\u2611 " : "\u2610 ") : "") + eventData.title
588 color: isTodoItem ? Color.mOnSecondary : Color.mOnTertiary
589 font.pointSize: Style.fontSizeXXS; font.weight: Font.Medium
590 font.strikeout: isTodoItem && eventData.todoStatus === "COMPLETED"
591 elide: Text.ElideRight; verticalAlignment: Text.AlignVCenter
592 }
593 }
594 MouseArea {
595 anchors.fill: parent
596 hoverEnabled: true
597 cursorShape: Qt.PointingHandCursor
598 onEntered: {
599 var tip = mainInstance?.getEventTooltip(eventData) || ""
600 TooltipService.show(parent, tip, "auto", Style.tooltipDelay, Settings.data.ui.fontFixed)
601 }
602 onClicked: {
603 if (isTodoItem) {
604 if (eventData.todoStatus === "COMPLETED")
605 mainInstance?.uncompleteTodo(eventData.calendarUid, eventData.todoUid)
606 else
607 mainInstance?.completeTodo(eventData.calendarUid, eventData.todoUid)
608 } else {
609 mainInstance?.handleEventClick(eventData)
610 }
611 }
612 onExited: TooltipService.hide()
613 }
614 }
615 }
616 }
617 }
618 // Calendar flickable
619 Rectangle {
620 width: parent.width
621 height: parent.height - dayHeaders.height - (allDayEventsSection.visible ? allDayEventsSection.height : 0)
622 color: Color.mSurfaceVariant
623 radius: Style.radiusM
624 clip: true
625
626 Flickable {
627 id: calendarFlickable
628 anchors.fill: parent
629 clip: true
630 contentHeight: 24 * (root.hourHeight)
631 boundsBehavior: Flickable.DragOverBounds
632 onHeightChanged: Qt.callLater(root.adjustHourHeightForViewport)
633
634 Component.onCompleted: {
635 calendarFlickable.forceActiveFocus()
636 }
637
638 // Keyboard interaction
639 Keys.onPressed: function(event) {
640 if (event.key === Qt.Key_Up || event.key === Qt.Key_Down) {
641 var step = root.hourHeight
642 var targetY = event.key === Qt.Key_Up ? Math.max(0, contentY - step) :
643 Math.min(Math.max(0, contentHeight - height), contentY + step)
644 scrollAnim.targetY = targetY
645 scrollAnim.start()
646 event.accepted = true
647 } else if (event.key === Qt.Key_Left || event.key === Qt.Key_Right) {
648 if (mainInstance) {
649 mainInstance.navigateWeek(event.key === Qt.Key_Left ? -7 : 7)
650 }
651 event.accepted = true
652 }
653 }
654
655 NumberAnimation {
656 id: scrollAnim
657 target: calendarFlickable; property: "contentY"; duration: 100
658 easing.type: Easing.OutCubic; property real targetY: 0; to: targetY
659 }
660
661 ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
662
663 Row {
664 width: parent.width
665 height: parent.height
666
667 // Time Column
668 Column {
669 width: root.timeColumnWidth
670 height: parent.height
671 Repeater {
672 model: 23
673 Rectangle {
674 width: root.timeColumnWidth
675 height: root.hourHeight
676 color: "transparent"
677 NText {
678 text: {
679 var hour = index + 1
680 if (mainInstance?.use12hourFormat) {
681 var d = new Date(); d.setHours(hour, 0, 0, 0)
682 return mainInstance.formatTime(d)
683 }
684 return (hour < 10 ? "0" : "") + hour + ':00'
685 }
686 anchors.right: parent.right
687 anchors.rightMargin: Style.marginS
688 anchors.verticalCenter: parent.top
689 anchors.verticalCenterOffset: root.hourHeight
690 font.pointSize: Style.fontSizeXS; color: Color.mOnSurfaceVariant
691 }
692 }
693 }
694 }
695
696 // Hour Rectangles
697 Item {
698 width: 7 * ((mainInstance?.dayColumnWidth) + (root.daySpacing))
699 height: parent.height
700
701 Row {
702 anchors.fill: parent
703 spacing: root.daySpacing
704 Repeater {
705 model: 7
706 Column {
707 width: mainInstance?.dayColumnWidth
708 height: parent.height
709 Repeater {
710 model: 24
711 Rectangle { width: parent.width; height: 1; color: Color.mSurfaceVariant }
712 }
713 }
714 }
715 }
716 // Hour Lines
717 Repeater {
718 model: 24
719 Rectangle {
720 width: parent.width; height: 1
721 y: index * (root.hourHeight)
722 color: Qt.alpha(mainInstance?.lineColor || Color.mOutline, mainInstance?.hourLineOpacitySetting || 0.5)
723 }
724 }
725 // Day Lines
726 Repeater {
727 model: 6
728 Rectangle {
729 width: 1; height: parent.height
730 x: (index + 1) * ((mainInstance?.dayColumnWidth) + (root.daySpacing)) - ((root.daySpacing) / 2)
731 color: Qt.alpha(mainInstance?.lineColor || Color.mOutline, mainInstance?.dayLineOpacitySetting || 0.9)
732 }
733 }
734
735 // Event positioning
736 Repeater {
737 model: mainInstance?.eventsModel
738 delegate: Item {
739 property var eventData: model
740 property int dayIndex: mainInstance?.getDisplayDayIndexForDate(model.startTime) ?? -1
741 property real startHour: model.startTime.getHours() + model.startTime.getMinutes() / 60
742 property real endHour: model.endTime.getHours() + model.endTime.getMinutes() / 60
743 property real duration: Math.max(0, (model.endTime - model.startTime) / 3600000)
744
745 property real exactHeight: Math.max(1, duration * (root.hourHeight) - 1)
746 property bool isCompact: exactHeight < 40
747 property var overlapInfo: mainInstance?.overlappingEventsData?.[index] ?? {
748 xOffset: 0, width: (mainInstance?.dayColumnWidth) - 8, lane: 0, totalLanes: 1
749 }
750 property real eventWidth: overlapInfo.width - 1
751 property real eventXOffset: overlapInfo.xOffset
752
753 visible: dayIndex >= 0 && dayIndex < 7 && duration > 0
754 width: eventWidth
755 height: exactHeight
756 x: dayIndex * ((mainInstance?.dayColumnWidth) + (root.daySpacing)) + eventXOffset
757 y: startHour * (root.hourHeight)
758 z: 100 + overlapInfo.lane
759
760 property bool isTodoItem: model.isTodo || false
761 property color eventColor: isTodoItem ? Color.mSecondary : Color.mPrimary
762 property color eventTextColor: isTodoItem ? Color.mOnSecondary : Color.mOnPrimary
763
764 Rectangle {
765 anchors.fill: parent
766 color: eventColor
767 radius: Style.radiusS
768 opacity: isTodoItem && model.todoStatus === "COMPLETED" ? 0.5 : 0.9
769 clip: true
770 Rectangle {
771 visible: exactHeight < 5 && overlapInfo.lane > 0
772 anchors.fill: parent
773 color: "transparent"
774 radius: parent.radius
775 border.width: 1
776 border.color: eventColor
777 }
778 Loader {
779 anchors.fill: parent
780 anchors.margins: exactHeight < 10 ? 1 : Style.marginS
781 anchors.leftMargin: exactHeight < 10 ? 1 : Style.marginS + 3
782 sourceComponent: isCompact ? compactLayout : normalLayout
783 }
784 }
785
786 Component {
787 id: normalLayout
788 Column {
789 spacing: 2
790 width: parent.width - 3
791 NText {
792 visible: exactHeight >= 20
793 text: (isTodoItem ? (model.todoStatus === "COMPLETED" ? "\u2611 " : "\u2610 ") : "") + model.title
794 color: eventTextColor
795 font.pointSize: Style.fontSizeXS; font.weight: Font.Medium
796 font.strikeout: isTodoItem && model.todoStatus === "COMPLETED"
797 elide: Text.ElideRight; width: parent.width
798 }
799 NText {
800 visible: exactHeight >= 30 && !isTodoItem
801 text: mainInstance?.formatTimeRangeForDisplay(model) || ""
802 color: eventTextColor
803 font.pointSize: Style.fontSizeXXS; opacity: 0.9
804 elide: Text.ElideRight; width: parent.width
805 }
806 NText {
807 visible: exactHeight >= 45 && model.location && model.location !== ""
808 text: "\u26B2 " + (model.location || "")
809 color: eventTextColor
810 font.pointSize: Style.fontSizeXXS; opacity: 0.8
811 elide: Text.ElideRight; width: parent.width
812 }
813 }
814 }
815
816 Component {
817 id: compactLayout
818 NText {
819 text: {
820 var prefix = isTodoItem ? (model.todoStatus === "COMPLETED" ? "\u2611 " : "\u2610 ") : ""
821 if (exactHeight < 15) return prefix + model.title
822 if (isTodoItem) return prefix + model.title
823 return model.title + " \u2022 " + (mainInstance?.formatTimeRangeForDisplay(model) || "")
824 }
825 color: eventTextColor
826 font.pointSize: exactHeight < 15 ? Style.fontSizeXXS : Style.fontSizeXS
827 font.weight: Font.Medium
828 font.strikeout: isTodoItem && model.todoStatus === "COMPLETED"
829 elide: Text.ElideRight; verticalAlignment: Text.AlignVCenter
830 width: parent.width - 3
831 }
832 }
833
834 MouseArea {
835 anchors.fill: parent
836 hoverEnabled: true
837 cursorShape: Qt.PointingHandCursor
838 onEntered: {
839 var tip = mainInstance?.getEventTooltip(model) || ""
840 TooltipService.show(parent, tip, "auto", Style.tooltipDelay, Settings.data.ui.fontFixed)
841 }
842 onClicked: {
843 if (isTodoItem) {
844 if (model.todoStatus === "COMPLETED")
845 mainInstance?.uncompleteTodo(model.calendarUid, model.todoUid)
846 else
847 mainInstance?.completeTodo(model.calendarUid, model.todoUid)
848 } else {
849 mainInstance?.handleEventClick(eventData)
850 }
851 }
852 onExited: TooltipService.hide()
853 }
854 }
855 }
856
857 // Time Indicator
858 Rectangle {
859 property var now: new Date()
860 property date today: new Date(now.getFullYear(), now.getMonth(), now.getDate())
861 property date weekStartDate: mainInstance?.weekStart ?? new Date()
862 property date weekEndDate: mainInstance ?
863 new Date(mainInstance.weekStart.getFullYear(), mainInstance.weekStart.getMonth(), mainInstance.weekStart.getDate() + 7) : new Date()
864 property bool inCurrentWeek: today >= weekStartDate && today < weekEndDate
865 property int currentDay: mainInstance?.getDayIndexForDate(now) ?? -1
866 property real currentHour: now.getHours() + now.getMinutes() / 60
867
868 visible: inCurrentWeek && currentDay >= 0
869 width: mainInstance?.dayColumnWidth
870 height: 2
871 x: currentDay * ((mainInstance?.dayColumnWidth) + (root.daySpacing))
872 y: currentHour * (root.hourHeight)
873 color: Color.mError
874 radius: 1
875 z: 1000
876 Rectangle {
877 width: 8; height: 8; radius: 4; color: Color.mError
878 anchors.verticalCenter: parent.verticalCenter; x: -4
879 }
880 Timer {
881 interval: 60000; running: true; repeat: true
882 onTriggered: parent.now = new Date()
883 }
884 }
885 }
886 }
887 }
888 }
889 }
890 }
891
892 }
893 }
894}