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 property bool showEventDetailDialog: false
26 property bool eventDetailEditMode: false
27 property bool showDeleteConfirmation: false
28
29 property real defaultHourHeight: 50 * Style.uiScaleRatio
30 property real minHourHeight: 32 * Style.uiScaleRatio
31 property real hourHeight: defaultHourHeight
32 property real timeColumnWidth: 65 * Style.uiScaleRatio
33 property real daySpacing: 1 * Style.uiScaleRatio
34
35 // Panel doesn't need its own CalendarService connection - Main.qml handles it.
36 // When panel opens, trigger a fresh load if needed.
37 Component.onCompleted: {
38 mainInstance?.initializePlugin()
39 Qt.callLater(root.adjustHourHeightForViewport)
40 }
41 onVisibleChanged: if (visible && mainInstance) {
42 mainInstance.refreshView()
43 mainInstance.goToToday()
44 Qt.callLater(root.scrollToCurrentTime)
45 mainInstance.loadTodos()
46 Qt.callLater(root.adjustHourHeightForViewport)
47 }
48
49 function adjustHourHeightForViewport() {
50 if (!calendarFlickable || calendarFlickable.height <= 0) return
51 // Target showing 08:30–24:00 (~15.5 hours) without scroll; fall back to min height if space is tight.
52 var target = calendarFlickable.height / 15.5
53 var newHeight = Math.max(minHourHeight, Math.min(defaultHourHeight, target))
54 if (Math.abs(newHeight - hourHeight) > 0.5) hourHeight = newHeight
55 }
56
57 // Scroll to time indicator position
58 function scrollToCurrentTime() {
59 if (!mainInstance || !calendarFlickable) return
60 var now = new Date(), today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
61 var weekStart = new Date(mainInstance.weekStart)
62 var weekEnd = new Date(weekStart.getFullYear(), weekStart.getMonth(), weekStart.getDate() + 7)
63
64 if (today >= weekStart && today < weekEnd) {
65 var currentHour = now.getHours() + now.getMinutes() / 60
66 var scrollPos = (currentHour * hourHeight) - (calendarFlickable.height / 2)
67 var maxScroll = Math.max(0, (24 * hourHeight) - calendarFlickable.height)
68 scrollAnim.targetY = Math.max(0, Math.min(scrollPos, maxScroll))
69 scrollAnim.start()
70 }
71 }
72
73 // Event creation dialog
74 Rectangle {
75 id: createEventOverlay
76 anchors.fill: parent
77 color: Qt.rgba(0, 0, 0, 0.5)
78 visible: showCreateDialog
79 z: 2000
80
81 MouseArea { anchors.fill: parent; onClicked: showCreateDialog = false }
82
83 Rectangle {
84 anchors.centerIn: parent
85 width: 400 * Style.uiScaleRatio
86 height: createDialogColumn.implicitHeight + 2 * Style.marginM
87 color: Color.mSurface
88 radius: Style.radiusM
89
90 MouseArea { anchors.fill: parent } // block clicks through
91
92 ColumnLayout {
93 id: createDialogColumn
94 anchors.fill: parent
95 anchors.margins: Style.marginM
96 spacing: Style.marginS
97
98 NText {
99 text: pluginApi.tr("panel.add_event")
100 font.pointSize: Style.fontSizeL; font.weight: Font.Bold
101 color: Color.mOnSurface
102 }
103
104 NText { text: pluginApi.tr("panel.summary"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
105 TextField {
106 id: createEventSummary
107 Layout.fillWidth: true
108 placeholderText: pluginApi.tr("panel.summary")
109 color: Color.mOnSurface
110 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS }
111 }
112
113 NText { text: pluginApi.tr("panel.date"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
114 TextField {
115 id: createEventDate
116 Layout.fillWidth: true
117 placeholderText: "YYYY-MM-DD"
118 color: Color.mOnSurface
119 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS }
120 }
121
122 RowLayout {
123 spacing: Style.marginS
124 ColumnLayout {
125 Layout.fillWidth: true
126 NText { text: pluginApi.tr("panel.start_time"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
127 TextField {
128 id: createEventStartTime
129 Layout.fillWidth: true
130 placeholderText: "HH:MM"
131 color: Color.mOnSurface
132 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS }
133 }
134 }
135 ColumnLayout {
136 Layout.fillWidth: true
137 NText { text: pluginApi.tr("panel.end_time"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
138 TextField {
139 id: createEventEndTime
140 Layout.fillWidth: true
141 placeholderText: "HH:MM"
142 color: Color.mOnSurface
143 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS }
144 }
145 }
146 }
147
148 NText { text: pluginApi.tr("panel.location"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
149 TextField {
150 id: createEventLocation
151 Layout.fillWidth: true
152 placeholderText: pluginApi.tr("panel.location")
153 color: Color.mOnSurface
154 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS }
155 }
156
157 NText { text: pluginApi.tr("panel.description"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
158 TextField {
159 id: createEventDescription
160 Layout.fillWidth: true
161 placeholderText: pluginApi.tr("panel.description")
162 color: Color.mOnSurface
163 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS }
164 }
165
166 NText { text: pluginApi.tr("panel.calendar_select"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
167 ComboBox {
168 id: calendarSelector
169 Layout.fillWidth: true
170 model: CalendarService.calendars || []
171 textRole: "name"
172 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS }
173 }
174
175 RowLayout {
176 Layout.fillWidth: true
177 spacing: Style.marginS
178
179 Item { Layout.fillWidth: true }
180
181 Rectangle {
182 Layout.preferredWidth: cancelBtn.implicitWidth + 2 * Style.marginM
183 Layout.preferredHeight: cancelBtn.implicitHeight + Style.marginS
184 color: Color.mSurfaceVariant; radius: Style.radiusS
185 NText {
186 id: cancelBtn; anchors.centerIn: parent
187 text: pluginApi.tr("panel.cancel"); color: Color.mOnSurfaceVariant
188 }
189 MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor; onClicked: showCreateDialog = false }
190 }
191
192 Rectangle {
193 Layout.preferredWidth: createBtn.implicitWidth + 2 * Style.marginM
194 Layout.preferredHeight: createBtn.implicitHeight + Style.marginS
195 color: Color.mPrimary; radius: Style.radiusS
196 opacity: createEventSummary.text.trim() !== "" ? 1.0 : 0.5
197 NText {
198 id: createBtn; anchors.centerIn: parent
199 text: pluginApi.tr("panel.create"); color: Color.mOnPrimary; font.weight: Font.Bold
200 }
201 MouseArea {
202 anchors.fill: parent; cursorShape: Qt.PointingHandCursor
203 onClicked: {
204 if (createEventSummary.text.trim() === "") return
205 var cal = CalendarService.calendars?.[calendarSelector.currentIndex]
206 var calUid = cal?.uid || ""
207 var dateParts = createEventDate.text.split("-")
208 var startParts = createEventStartTime.text.split(":")
209 var endParts = createEventEndTime.text.split(":")
210 var startDate = new Date(parseInt(dateParts[0]), parseInt(dateParts[1])-1, parseInt(dateParts[2]),
211 parseInt(startParts[0]), parseInt(startParts[1]), 0)
212 var endDate = new Date(parseInt(dateParts[0]), parseInt(dateParts[1])-1, parseInt(dateParts[2]),
213 parseInt(endParts[0]), parseInt(endParts[1]), 0)
214 mainInstance?.createEvent(calUid, createEventSummary.text.trim(),
215 Math.floor(startDate.getTime()/1000), Math.floor(endDate.getTime()/1000),
216 createEventLocation.text.trim(), createEventDescription.text.trim())
217 showCreateDialog = false
218 }
219 }
220 }
221 }
222 }
223 }
224 }
225
226 // Task creation dialog
227 Rectangle {
228 id: createTaskOverlay
229 anchors.fill: parent
230 color: Qt.rgba(0, 0, 0, 0.5)
231 visible: showCreateTaskDialog
232 z: 2000
233
234 MouseArea { anchors.fill: parent; onClicked: showCreateTaskDialog = false }
235
236 Rectangle {
237 anchors.centerIn: parent
238 width: 400 * Style.uiScaleRatio
239 height: createTaskDialogColumn.implicitHeight + 2 * Style.marginM
240 color: Color.mSurface
241 radius: Style.radiusM
242
243 MouseArea { anchors.fill: parent }
244
245 ColumnLayout {
246 id: createTaskDialogColumn
247 anchors.fill: parent
248 anchors.margins: Style.marginM
249 spacing: Style.marginS
250
251 NText {
252 text: pluginApi.tr("panel.add_task")
253 font.pointSize: Style.fontSizeL; font.weight: Font.Bold
254 color: Color.mOnSurface
255 }
256
257 NText { text: pluginApi.tr("panel.task_summary"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
258 TextField {
259 id: createTaskSummary
260 Layout.fillWidth: true
261 placeholderText: pluginApi.tr("panel.task_summary")
262 color: Color.mOnSurface
263 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS }
264 }
265
266 NText { text: pluginApi.tr("panel.due_date"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
267 TextField {
268 id: createTaskDueDate
269 Layout.fillWidth: true
270 placeholderText: "YYYY-MM-DD"
271 color: Color.mOnSurface
272 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS }
273 }
274
275 // Use end_time label to reflect deadline semantics
276 NText { text: pluginApi.tr("panel.end_time"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
277 TextField {
278 id: createTaskDueTime
279 Layout.fillWidth: true
280 placeholderText: "HH:MM"
281 color: Color.mOnSurface
282 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS }
283 }
284
285 NText { text: pluginApi.tr("panel.description"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
286 TextField {
287 id: createTaskDescription
288 Layout.fillWidth: true
289 placeholderText: pluginApi.tr("panel.description")
290 color: Color.mOnSurface
291 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS }
292 }
293
294 NText { text: pluginApi.tr("panel.priority"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
295 property int selectedPriority: 0
296 RowLayout {
297 spacing: Style.marginS
298 Repeater {
299 model: [
300 { label: pluginApi.tr("panel.priority_high"), value: 1 },
301 { label: pluginApi.tr("panel.priority_medium"), value: 5 },
302 { label: pluginApi.tr("panel.priority_low"), value: 9 }
303 ]
304 Rectangle {
305 Layout.preferredWidth: priLabel.implicitWidth + 2 * Style.marginM
306 Layout.preferredHeight: priLabel.implicitHeight + Style.marginS
307 color: createTaskDialogColumn.selectedPriority === modelData.value ? Color.mPrimary : Color.mSurfaceVariant
308 radius: Style.radiusS
309 NText {
310 id: priLabel; anchors.centerIn: parent
311 text: modelData.label
312 color: createTaskDialogColumn.selectedPriority === modelData.value ? Color.mOnPrimary : Color.mOnSurfaceVariant
313 font.weight: Font.Medium
314 }
315 MouseArea {
316 anchors.fill: parent; cursorShape: Qt.PointingHandCursor
317 onClicked: createTaskDialogColumn.selectedPriority =
318 createTaskDialogColumn.selectedPriority === modelData.value ? 0 : modelData.value
319 }
320 }
321 }
322 }
323
324 NText { text: pluginApi.tr("panel.task_list_select"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
325 ComboBox {
326 id: taskListSelector
327 Layout.fillWidth: true
328 model: mainInstance?.taskLists || []
329 textRole: "name"
330 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS }
331 }
332
333 RowLayout {
334 Layout.fillWidth: true
335 spacing: Style.marginS
336
337 Item { Layout.fillWidth: true }
338
339 Rectangle {
340 Layout.preferredWidth: taskCancelBtn.implicitWidth + 2 * Style.marginM
341 Layout.preferredHeight: taskCancelBtn.implicitHeight + Style.marginS
342 color: Color.mSurfaceVariant; radius: Style.radiusS
343 NText {
344 id: taskCancelBtn; anchors.centerIn: parent
345 text: pluginApi.tr("panel.cancel"); color: Color.mOnSurfaceVariant
346 }
347 MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor; onClicked: showCreateTaskDialog = false }
348 }
349
350 Rectangle {
351 Layout.preferredWidth: taskCreateBtn.implicitWidth + 2 * Style.marginM
352 Layout.preferredHeight: taskCreateBtn.implicitHeight + Style.marginS
353 color: Color.mPrimary; radius: Style.radiusS
354 opacity: createTaskSummary.text.trim() !== "" ? 1.0 : 0.5
355 NText {
356 id: taskCreateBtn; anchors.centerIn: parent
357 text: pluginApi.tr("panel.create"); color: Color.mOnPrimary; font.weight: Font.Bold
358 }
359 MouseArea {
360 anchors.fill: parent; cursorShape: Qt.PointingHandCursor
361 onClicked: {
362 if (createTaskSummary.text.trim() === "") return
363 var tl = mainInstance?.taskLists?.[taskListSelector.currentIndex]
364 var tlUid = tl?.uid || ""
365 var dueTs = 0
366 if (createTaskDueDate.text.trim() !== "") {
367 var dateParts = createTaskDueDate.text.split("-")
368 var timeParts = createTaskDueTime.text.split(":")
369 var h = createTaskDueTime.text.trim() === "" ? 0 : parseInt(timeParts[0])
370 var m = createTaskDueTime.text.trim() === "" ? 0 : parseInt(timeParts[1] || "0")
371 var d = new Date(parseInt(dateParts[0]), parseInt(dateParts[1]) - 1, parseInt(dateParts[2]), h, m, 0)
372 if (!isNaN(d.getTime())) dueTs = Math.floor(d.getTime() / 1000)
373 }
374 mainInstance?.createTodo(tlUid, createTaskSummary.text.trim(),
375 dueTs, createTaskDialogColumn.selectedPriority,
376 createTaskDescription.text.trim())
377 showCreateTaskDialog = false
378 }
379 }
380 }
381 }
382 }
383 }
384 }
385
386 // Event detail/edit popup
387 Rectangle {
388 id: eventDetailOverlay
389 anchors.fill: parent
390 color: Qt.rgba(0, 0, 0, 0.5)
391 visible: showEventDetailDialog
392 z: 2000
393
394 MouseArea { anchors.fill: parent; onClicked: { showEventDetailDialog = false; eventDetailEditMode = false; showDeleteConfirmation = false } }
395
396 Rectangle {
397 anchors.centerIn: parent
398 width: 420 * Style.uiScaleRatio
399 height: eventDetailColumn.implicitHeight + 2 * Style.marginM
400 color: Color.mSurface
401 radius: Style.radiusM
402
403 MouseArea { anchors.fill: parent }
404
405 ColumnLayout {
406 id: eventDetailColumn
407 anchors.fill: parent
408 anchors.margins: Style.marginM
409 spacing: Style.marginS
410
411 property var evt: mainInstance?.selectedEvent || {}
412
413 // View mode
414 ColumnLayout {
415 visible: !eventDetailEditMode && !showDeleteConfirmation
416 spacing: Style.marginS
417 Layout.fillWidth: true
418
419 NText {
420 text: eventDetailColumn.evt.title || ""
421 font.pointSize: Style.fontSizeL; font.weight: Font.Bold
422 color: Color.mOnSurface
423 wrapMode: Text.Wrap; Layout.fillWidth: true
424 }
425
426 RowLayout {
427 spacing: Style.marginS
428 NIcon { icon: "clock"; pointSize: Style.fontSizeS; color: Color.mOnSurfaceVariant }
429 NText {
430 text: {
431 var e = eventDetailColumn.evt
432 if (!e.startTime) return ""
433 return mainInstance?.formatDateTime(e.startTime) + " - " + mainInstance?.formatDateTime(e.endTime)
434 }
435 font.pointSize: Style.fontSizeS; color: Color.mOnSurfaceVariant
436 wrapMode: Text.Wrap; Layout.fillWidth: true
437 }
438 }
439
440 RowLayout {
441 visible: (eventDetailColumn.evt.location || "") !== ""
442 spacing: Style.marginS
443 NIcon { icon: "map-pin"; pointSize: Style.fontSizeS; color: Color.mOnSurfaceVariant }
444 NText {
445 text: eventDetailColumn.evt.location || ""
446 font.pointSize: Style.fontSizeS; color: Color.mOnSurfaceVariant
447 wrapMode: Text.Wrap; Layout.fillWidth: true
448 }
449 }
450
451 NText {
452 visible: (eventDetailColumn.evt.description || "") !== ""
453 text: eventDetailColumn.evt.description || ""
454 font.pointSize: Style.fontSizeS; color: Color.mOnSurfaceVariant
455 wrapMode: Text.Wrap; Layout.fillWidth: true
456 }
457
458 RowLayout {
459 Layout.fillWidth: true
460 spacing: Style.marginS
461
462 Item { Layout.fillWidth: true }
463
464 Rectangle {
465 Layout.preferredWidth: editEventBtn.implicitWidth + 2 * Style.marginM
466 Layout.preferredHeight: editEventBtn.implicitHeight + Style.marginS
467 color: Color.mPrimary; radius: Style.radiusS
468 visible: (eventDetailColumn.evt.eventUid || "") !== ""
469 NText {
470 id: editEventBtn; anchors.centerIn: parent
471 text: pluginApi.tr("panel.edit") || "Edit"
472 color: Color.mOnPrimary; font.weight: Font.Bold
473 }
474 MouseArea {
475 anchors.fill: parent; cursorShape: Qt.PointingHandCursor
476 onClicked: {
477 var e = eventDetailColumn.evt
478 editEventSummary.text = e.title || ""
479 editEventLocation.text = e.location || ""
480 editEventDescription.text = e.description || ""
481 if (e.startTime) {
482 var s = new Date(e.startTime)
483 editEventDate.text = s.getFullYear() + "-" + String(s.getMonth()+1).padStart(2,'0') + "-" + String(s.getDate()).padStart(2,'0')
484 editEventStartTime.text = String(s.getHours()).padStart(2,'0') + ":" + String(s.getMinutes()).padStart(2,'0')
485 }
486 if (e.endTime) {
487 var en = new Date(e.endTime)
488 editEventEndTime.text = String(en.getHours()).padStart(2,'0') + ":" + String(en.getMinutes()).padStart(2,'0')
489 }
490 eventDetailEditMode = true
491 }
492 }
493 }
494
495 Rectangle {
496 Layout.preferredWidth: deleteEventBtn.implicitWidth + 2 * Style.marginM
497 Layout.preferredHeight: deleteEventBtn.implicitHeight + Style.marginS
498 color: Color.mError; radius: Style.radiusS
499 visible: (eventDetailColumn.evt.eventUid || "") !== ""
500 NText {
501 id: deleteEventBtn; anchors.centerIn: parent
502 text: pluginApi.tr("panel.delete") || "Delete"
503 color: Color.mOnError; font.weight: Font.Bold
504 }
505 MouseArea {
506 anchors.fill: parent; cursorShape: Qt.PointingHandCursor
507 onClicked: showDeleteConfirmation = true
508 }
509 }
510
511 Rectangle {
512 Layout.preferredWidth: closeEventBtn.implicitWidth + 2 * Style.marginM
513 Layout.preferredHeight: closeEventBtn.implicitHeight + Style.marginS
514 color: Color.mSurfaceVariant; radius: Style.radiusS
515 NText {
516 id: closeEventBtn; anchors.centerIn: parent
517 text: pluginApi.tr("panel.close") || "Close"
518 color: Color.mOnSurfaceVariant
519 }
520 MouseArea {
521 anchors.fill: parent; cursorShape: Qt.PointingHandCursor
522 onClicked: { showEventDetailDialog = false; eventDetailEditMode = false }
523 }
524 }
525 }
526 }
527
528 // Delete confirmation mode
529 ColumnLayout {
530 visible: showDeleteConfirmation
531 spacing: Style.marginS
532 Layout.fillWidth: true
533
534 NText {
535 text: (pluginApi.tr("panel.delete_confirm") || "Delete this event?")
536 font.pointSize: Style.fontSizeM; font.weight: Font.Bold
537 color: Color.mOnSurface
538 }
539 NText {
540 text: eventDetailColumn.evt.title || ""
541 font.pointSize: Style.fontSizeS; color: Color.mOnSurfaceVariant
542 }
543
544 RowLayout {
545 Layout.fillWidth: true
546 spacing: Style.marginS
547 Item { Layout.fillWidth: true }
548
549 Rectangle {
550 Layout.preferredWidth: confirmDeleteBtn.implicitWidth + 2 * Style.marginM
551 Layout.preferredHeight: confirmDeleteBtn.implicitHeight + Style.marginS
552 color: Color.mError; radius: Style.radiusS
553 NText {
554 id: confirmDeleteBtn; anchors.centerIn: parent
555 text: pluginApi.tr("panel.delete") || "Delete"
556 color: Color.mOnError; font.weight: Font.Bold
557 }
558 MouseArea {
559 anchors.fill: parent; cursorShape: Qt.PointingHandCursor
560 onClicked: {
561 var e = eventDetailColumn.evt
562 mainInstance?.deleteEvent(e.calendarUid, e.eventUid)
563 showEventDetailDialog = false
564 showDeleteConfirmation = false
565 eventDetailEditMode = false
566 }
567 }
568 }
569
570 Rectangle {
571 Layout.preferredWidth: cancelDeleteBtn.implicitWidth + 2 * Style.marginM
572 Layout.preferredHeight: cancelDeleteBtn.implicitHeight + Style.marginS
573 color: Color.mSurfaceVariant; radius: Style.radiusS
574 NText {
575 id: cancelDeleteBtn; anchors.centerIn: parent
576 text: pluginApi.tr("panel.cancel"); color: Color.mOnSurfaceVariant
577 }
578 MouseArea {
579 anchors.fill: parent; cursorShape: Qt.PointingHandCursor
580 onClicked: showDeleteConfirmation = false
581 }
582 }
583 }
584 }
585
586 // Edit mode
587 ColumnLayout {
588 visible: eventDetailEditMode && !showDeleteConfirmation
589 spacing: Style.marginS
590 Layout.fillWidth: true
591
592 NText {
593 text: pluginApi.tr("panel.edit_event") || "Edit Event"
594 font.pointSize: Style.fontSizeL; font.weight: Font.Bold
595 color: Color.mOnSurface
596 }
597
598 NText { text: pluginApi.tr("panel.summary"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
599 TextField {
600 id: editEventSummary
601 Layout.fillWidth: true
602 color: Color.mOnSurface
603 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS }
604 }
605
606 NText { text: pluginApi.tr("panel.date"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
607 TextField {
608 id: editEventDate
609 Layout.fillWidth: true
610 placeholderText: "YYYY-MM-DD"
611 color: Color.mOnSurface
612 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS }
613 }
614
615 RowLayout {
616 spacing: Style.marginS
617 ColumnLayout {
618 Layout.fillWidth: true
619 NText { text: pluginApi.tr("panel.start_time"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
620 TextField {
621 id: editEventStartTime
622 Layout.fillWidth: true
623 placeholderText: "HH:MM"
624 color: Color.mOnSurface
625 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS }
626 }
627 }
628 ColumnLayout {
629 Layout.fillWidth: true
630 NText { text: pluginApi.tr("panel.end_time"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
631 TextField {
632 id: editEventEndTime
633 Layout.fillWidth: true
634 placeholderText: "HH:MM"
635 color: Color.mOnSurface
636 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS }
637 }
638 }
639 }
640
641 NText { text: pluginApi.tr("panel.location"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
642 TextField {
643 id: editEventLocation
644 Layout.fillWidth: true
645 color: Color.mOnSurface
646 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS }
647 }
648
649 NText { text: pluginApi.tr("panel.description"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
650 TextField {
651 id: editEventDescription
652 Layout.fillWidth: true
653 color: Color.mOnSurface
654 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS }
655 }
656
657 RowLayout {
658 Layout.fillWidth: true
659 spacing: Style.marginS
660 Item { Layout.fillWidth: true }
661
662 Rectangle {
663 Layout.preferredWidth: saveEventBtn.implicitWidth + 2 * Style.marginM
664 Layout.preferredHeight: saveEventBtn.implicitHeight + Style.marginS
665 color: Color.mPrimary; radius: Style.radiusS
666 NText {
667 id: saveEventBtn; anchors.centerIn: parent
668 text: pluginApi.tr("panel.save") || "Save"
669 color: Color.mOnPrimary; font.weight: Font.Bold
670 }
671 MouseArea {
672 anchors.fill: parent; cursorShape: Qt.PointingHandCursor
673 onClicked: {
674 var e = eventDetailColumn.evt
675 var dateParts = editEventDate.text.split("-")
676 var startParts = editEventStartTime.text.split(":")
677 var endParts = editEventEndTime.text.split(":")
678 var startDate = new Date(parseInt(dateParts[0]), parseInt(dateParts[1])-1, parseInt(dateParts[2]),
679 parseInt(startParts[0]), parseInt(startParts[1]), 0)
680 var endDate = new Date(parseInt(dateParts[0]), parseInt(dateParts[1])-1, parseInt(dateParts[2]),
681 parseInt(endParts[0]), parseInt(endParts[1]), 0)
682 mainInstance?.updateEvent(
683 e.calendarUid, e.eventUid,
684 editEventSummary.text.trim(),
685 editEventLocation.text.trim(),
686 editEventDescription.text.trim(),
687 Math.floor(startDate.getTime()/1000),
688 Math.floor(endDate.getTime()/1000))
689 showEventDetailDialog = false
690 eventDetailEditMode = false
691 }
692 }
693 }
694
695 Rectangle {
696 Layout.preferredWidth: editCancelBtn.implicitWidth + 2 * Style.marginM
697 Layout.preferredHeight: editCancelBtn.implicitHeight + Style.marginS
698 color: Color.mSurfaceVariant; radius: Style.radiusS
699 NText {
700 id: editCancelBtn; anchors.centerIn: parent
701 text: pluginApi.tr("panel.cancel"); color: Color.mOnSurfaceVariant
702 }
703 MouseArea {
704 anchors.fill: parent; cursorShape: Qt.PointingHandCursor
705 onClicked: eventDetailEditMode = false
706 }
707 }
708 }
709 }
710 }
711 }
712 }
713
714 // UI
715 Rectangle {
716 id: panelContainer
717 anchors.fill: parent
718 color: "transparent"
719
720 ColumnLayout {
721 anchors.fill: parent
722 anchors.margins: Style.marginM
723 spacing: Style.marginM
724
725 //Header Section
726 Rectangle {
727 id: header
728 Layout.fillWidth: true
729 Layout.preferredHeight: topHeaderHeight
730 color: Color.mSurfaceVariant
731 radius: Style.radiusM
732
733 RowLayout {
734 anchors.margins: Style.marginM
735 anchors.fill: parent
736
737 NIcon { icon: "calendar-week"; pointSize: Style.fontSizeXXL; color: Color.mPrimary }
738
739 ColumnLayout {
740 Layout.fillHeight: true
741 spacing: 0
742 NText {
743 text: pluginApi.tr("panel.header")
744 font.pointSize: Style.fontSizeL; font.weight: Font.Bold; color: Color.mOnSurface
745 }
746 RowLayout {
747 spacing: Style.marginS
748 NText {
749 text: mainInstance?.monthRangeText || ""
750 font.pointSize: Style.fontSizeS; font.weight: Font.Medium; color: Color.mOnSurfaceVariant
751 }
752 Rectangle {
753 Layout.preferredWidth: 8; Layout.preferredHeight: 8; radius: 4
754 color: mainInstance?.isLoading ? Color.mError :
755 mainInstance?.syncStatus?.includes("No") ? Color.mError : Color.mOnSurfaceVariant
756 }
757 NText {
758 text: mainInstance?.syncStatus || ""
759 font.pointSize: Style.fontSizeS; color: Color.mOnSurfaceVariant
760 }
761 }
762 }
763
764 Item { Layout.fillWidth: true }
765
766 RowLayout {
767 spacing: Style.marginS
768 NIconButton {
769 icon: "plus"; tooltipText: pluginApi.tr("panel.add_event")
770 onClicked: {
771 createEventSummary.text = ""
772 createEventLocation.text = ""
773 createEventDescription.text = ""
774 var now = new Date()
775 var startH = now.getHours() + 1
776 createEventDate.text = now.getFullYear() + "-" + String(now.getMonth()+1).padStart(2,'0') + "-" + String(now.getDate()).padStart(2,'0')
777 createEventStartTime.text = String(startH).padStart(2,'0') + ":00"
778 createEventEndTime.text = String(startH+1).padStart(2,'0') + ":00"
779 showCreateDialog = true
780 }
781 }
782 NIconButton {
783 icon: "clipboard-check"; tooltipText: pluginApi.tr("panel.add_task")
784 onClicked: {
785 createTaskSummary.text = ""
786 var now = new Date()
787 var startH = now.getHours() + 1
788 createTaskDueDate.text = now.getFullYear() + "-" + String(now.getMonth()+1).padStart(2,'0') + "-" + String(now.getDate()).padStart(2,'0')
789 createTaskDueTime.text = String(startH).padStart(2,'0') + ":00"
790 createTaskDescription.text = ""
791 createTaskDialogColumn.selectedPriority = 0
792 showCreateTaskDialog = true
793 }
794 }
795 NIconButton {
796 icon: mainInstance?.showCompletedTodos ? "eye-off" : "eye"
797 tooltipText: pluginApi.tr("panel.show_completed")
798 onClicked: {
799 if (mainInstance) {
800 mainInstance.showCompletedTodos = !mainInstance.showCompletedTodos
801 mainInstance.loadTodos()
802 }
803 }
804 }
805 NIconButton {
806 icon: "chevron-left"
807 onClicked: mainInstance?.navigateWeek(-7)
808 }
809 NIconButton {
810 icon: "calendar"; tooltipText: pluginApi.tr("panel.today")
811 onClicked: { mainInstance?.goToToday(); Qt.callLater(root.scrollToCurrentTime) }
812 }
813 NIconButton {
814 icon: "chevron-right"
815 onClicked: mainInstance?.navigateWeek(7)
816 }
817 NIconButton {
818 icon: "refresh"; tooltipText: I18n.tr("common.refresh")
819 onClicked: { mainInstance?.loadEvents(); mainInstance?.loadTodos() }
820 enabled: mainInstance ? !mainInstance.isLoading : false
821 }
822 NIconButton {
823 icon: "close"; tooltipText: I18n.tr("common.close")
824 onClicked: pluginApi.closePanel(pluginApi.panelOpenScreen)
825 }
826 }
827 }
828 }
829
830 // Calendar View
831 Rectangle {
832 Layout.fillWidth: true
833 Layout.fillHeight: true
834 color: Color.mSurfaceVariant
835 radius: Style.radiusM
836 clip: true
837
838 Column {
839 anchors.fill: parent
840 spacing: 0
841
842 //Day Headers
843 Rectangle {
844 id: dayHeaders
845 width: parent.width
846 height: 56
847 color: Color.mSurfaceVariant
848 radius: Style.radiusM
849
850 Row {
851 anchors.fill: parent
852 anchors.leftMargin: root.timeColumnWidth
853 spacing: root.daySpacing
854
855 Repeater {
856 model: 7
857 Rectangle {
858 width: mainInstance?.dayColumnWidth
859 height: parent.height
860 color: "transparent"
861 property date dayDate: mainInstance?.weekDates?.[index] || new Date()
862 property bool isToday: {
863 var today = new Date()
864 return dayDate.getDate() === today.getDate() &&
865 dayDate.getMonth() === today.getMonth() &&
866 dayDate.getFullYear() === today.getFullYear()
867 }
868 Rectangle {
869 anchors.fill: parent
870 anchors.margins: 4
871 color: Color.mSurfaceVariant
872 border.color: isToday ? Color.mPrimary : "transparent"
873 border.width: 2
874 radius: Style.radiusM
875 Column {
876 anchors.centerIn: parent
877 spacing: 2
878 NText {
879 anchors.horizontalCenter: parent.horizontalCenter
880 text: dayDate ? I18n.locale.dayName(dayDate.getDay(), Locale.ShortFormat).toUpperCase() : ""
881 color: isToday ? Color.mPrimary : Color.mOnSurface
882 font.pointSize: Style.fontSizeS; font.weight: Font.Medium
883 }
884 NText {
885 anchors.horizontalCenter: parent.horizontalCenter
886 text: dayDate ? ((dayDate.getDate() < 10 ? "0" : "") + dayDate.getDate()) : ""
887 color: isToday ? Color.mPrimary : Color.mOnSurface
888 font.pointSize: Style.fontSizeM; font.weight: Font.Bold
889 }
890 }
891 }
892 }
893 }
894 }
895 }
896 // All-day row
897 Rectangle {
898 id: allDayEventsSection
899 width: parent.width
900 height: mainInstance ? Math.round(mainInstance.allDaySectionHeight * Style.uiScaleRatio) : 0
901 color: Color.mSurfaceVariant
902 visible: height > 0
903
904 Item {
905 id: allDayEventsContainer
906 anchors.fill: parent
907 anchors.leftMargin: root.timeColumnWidth
908
909 Repeater {
910 model: 6
911 delegate: Rectangle {
912 width: 1; height: parent.height
913 x: (index + 1) * ((mainInstance?.dayColumnWidth) + (root.daySpacing)) - ((root.daySpacing) / 2)
914 color: Qt.alpha(mainInstance?.lineColor || Color.mOutline, mainInstance?.dayLineOpacitySetting || 0.9)
915 }
916 }
917
918 Repeater {
919 model: mainInstance?.allDayEventsWithLayout || []
920 delegate: Item {
921 property var eventData: modelData
922 property bool isTodoItem: eventData.isTodo || false
923 property bool isDeadline: eventData.isDeadlineMarker || false
924 x: eventData.startDay * ((mainInstance?.dayColumnWidth) + (root.daySpacing))
925 y: eventData.lane * 25
926 width: (eventData.spanDays * ((mainInstance?.dayColumnWidth) + (root.daySpacing))) - (root.daySpacing)
927 height: isDeadline ? 10 : 24
928
929 Rectangle {
930 anchors.fill: parent
931 color: isDeadline ? Color.mSecondary : (isTodoItem ? Color.mSecondary : Color.mTertiary)
932 radius: Style.radiusS
933 opacity: isTodoItem && eventData.todoStatus === "COMPLETED" ? 0.5 : 1.0
934 NText {
935 anchors.fill: parent; anchors.margins: 4
936 text: isDeadline ? "" : (isTodoItem ? (eventData.todoStatus === "COMPLETED" ? "\u2611 " : "\u2610 ") : "") + eventData.title
937 color: isDeadline ? Color.mOnSecondary : (isTodoItem ? Color.mOnSecondary : Color.mOnTertiary)
938 font.pointSize: Style.fontSizeXXS; font.weight: Font.Medium
939 font.strikeout: isTodoItem && eventData.todoStatus === "COMPLETED"
940 elide: Text.ElideRight; verticalAlignment: Text.AlignVCenter
941 }
942 }
943 MouseArea {
944 anchors.fill: parent
945 hoverEnabled: true
946 cursorShape: Qt.PointingHandCursor
947 onEntered: {
948 var tip = mainInstance?.getEventTooltip(eventData) || ""
949 TooltipService.show(parent, tip, "auto", Style.tooltipDelay, Settings.data.ui.fontFixed)
950 }
951 onClicked: {
952 if (isTodoItem) {
953 if (eventData.todoStatus === "COMPLETED")
954 mainInstance?.uncompleteTodo(eventData.calendarUid, eventData.todoUid)
955 else
956 mainInstance?.completeTodo(eventData.calendarUid, eventData.todoUid)
957 } else {
958 mainInstance?.handleEventClick(eventData)
959 showEventDetailDialog = true
960 eventDetailEditMode = false
961 showDeleteConfirmation = false
962 }
963 }
964 onExited: TooltipService.hide()
965 }
966 }
967 }
968 }
969 }
970 // Calendar flickable
971 Rectangle {
972 width: parent.width
973 height: parent.height - dayHeaders.height - (allDayEventsSection.visible ? allDayEventsSection.height : 0)
974 color: Color.mSurfaceVariant
975 radius: Style.radiusM
976 clip: true
977
978 Flickable {
979 id: calendarFlickable
980 anchors.fill: parent
981 clip: true
982 contentHeight: 24 * (root.hourHeight)
983 boundsBehavior: Flickable.DragOverBounds
984 onHeightChanged: Qt.callLater(root.adjustHourHeightForViewport)
985
986 Component.onCompleted: {
987 calendarFlickable.forceActiveFocus()
988 }
989
990 // Keyboard interaction
991 Keys.onPressed: function(event) {
992 if (event.key === Qt.Key_Up || event.key === Qt.Key_Down) {
993 var step = root.hourHeight
994 var targetY = event.key === Qt.Key_Up ? Math.max(0, contentY - step) :
995 Math.min(Math.max(0, contentHeight - height), contentY + step)
996 scrollAnim.targetY = targetY
997 scrollAnim.start()
998 event.accepted = true
999 } else if (event.key === Qt.Key_Left || event.key === Qt.Key_Right) {
1000 if (mainInstance) {
1001 mainInstance.navigateWeek(event.key === Qt.Key_Left ? -7 : 7)
1002 }
1003 event.accepted = true
1004 }
1005 }
1006
1007 NumberAnimation {
1008 id: scrollAnim
1009 target: calendarFlickable; property: "contentY"; duration: 100
1010 easing.type: Easing.OutCubic; property real targetY: 0; to: targetY
1011 }
1012
1013 ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
1014
1015 Row {
1016 width: parent.width
1017 height: parent.height
1018
1019 // Time Column
1020 Column {
1021 width: root.timeColumnWidth
1022 height: parent.height
1023 Repeater {
1024 model: 23
1025 Rectangle {
1026 width: root.timeColumnWidth
1027 height: root.hourHeight
1028 color: "transparent"
1029 NText {
1030 text: {
1031 var hour = index + 1
1032 if (mainInstance?.use12hourFormat) {
1033 var d = new Date(); d.setHours(hour, 0, 0, 0)
1034 return mainInstance.formatTime(d)
1035 }
1036 return (hour < 10 ? "0" : "") + hour + ':00'
1037 }
1038 anchors.right: parent.right
1039 anchors.rightMargin: Style.marginS
1040 anchors.verticalCenter: parent.top
1041 anchors.verticalCenterOffset: root.hourHeight
1042 font.pointSize: Style.fontSizeXS; color: Color.mOnSurfaceVariant
1043 }
1044 }
1045 }
1046 }
1047
1048 // Hour Rectangles
1049 Item {
1050 width: 7 * ((mainInstance?.dayColumnWidth) + (root.daySpacing))
1051 height: parent.height
1052
1053 Row {
1054 anchors.fill: parent
1055 spacing: root.daySpacing
1056 Repeater {
1057 model: 7
1058 Column {
1059 width: mainInstance?.dayColumnWidth
1060 height: parent.height
1061 Repeater {
1062 model: 24
1063 Rectangle { width: parent.width; height: 1; color: Color.mSurfaceVariant }
1064 }
1065 }
1066 }
1067 }
1068 // Hour Lines
1069 Repeater {
1070 model: 24
1071 Rectangle {
1072 width: parent.width; height: 1
1073 y: index * (root.hourHeight)
1074 color: Qt.alpha(mainInstance?.lineColor || Color.mOutline, mainInstance?.hourLineOpacitySetting || 0.5)
1075 }
1076 }
1077 // Day Lines
1078 Repeater {
1079 model: 6
1080 Rectangle {
1081 width: 1; height: parent.height
1082 x: (index + 1) * ((mainInstance?.dayColumnWidth) + (root.daySpacing)) - ((root.daySpacing) / 2)
1083 color: Qt.alpha(mainInstance?.lineColor || Color.mOutline, mainInstance?.dayLineOpacitySetting || 0.9)
1084 }
1085 }
1086
1087 // Event positioning
1088 Repeater {
1089 model: mainInstance?.eventsModel
1090 delegate: Item {
1091 property var eventData: model
1092 property int dayIndex: mainInstance?.getDisplayDayIndexForDate(model.startTime) ?? -1
1093 property real startHour: model.startTime.getHours() + model.startTime.getMinutes() / 60
1094 property real endHour: model.endTime.getHours() + model.endTime.getMinutes() / 60
1095 property real duration: Math.max(0, (model.endTime - model.startTime) / 3600000)
1096
1097 property real exactHeight: Math.max(1, duration * (root.hourHeight) - 1)
1098 property bool isCompact: exactHeight < 40
1099 property var overlapInfo: mainInstance?.overlappingEventsData?.[index] ?? {
1100 xOffset: 0, width: (mainInstance?.dayColumnWidth) - 8, lane: 0, totalLanes: 1
1101 }
1102 property real eventWidth: overlapInfo.width - 1
1103 property real eventXOffset: overlapInfo.xOffset
1104
1105 property bool isTodoItem: model.isTodo || false
1106 property bool isDeadline: model.isDeadlineMarker || false
1107 property color eventColor: isDeadline ? Color.mSecondary : (isTodoItem ? Color.mSecondary : Color.mPrimary)
1108 property color eventTextColor: isDeadline ? Color.mOnSecondary : (isTodoItem ? Color.mOnSecondary : Color.mOnPrimary)
1109
1110 visible: dayIndex >= 0 && dayIndex < 7 && duration > 0
1111 width: eventWidth
1112 height: isDeadline ? Math.max(8, Math.min(12, exactHeight)) : exactHeight
1113 x: dayIndex * ((mainInstance?.dayColumnWidth) + (root.daySpacing)) + eventXOffset
1114 y: startHour * (root.hourHeight)
1115 z: 100 + overlapInfo.lane
1116
1117 Rectangle {
1118 anchors.fill: parent
1119 color: eventColor
1120 radius: Style.radiusS
1121 opacity: isDeadline ? 0.95 : (isTodoItem && model.todoStatus === "COMPLETED" ? 0.5 : 0.9)
1122 clip: true
1123 Rectangle {
1124 visible: exactHeight < 5 && overlapInfo.lane > 0
1125 anchors.fill: parent
1126 color: "transparent"
1127 radius: parent.radius
1128 border.width: 1
1129 border.color: eventColor
1130 }
1131 Loader {
1132 anchors.fill: parent
1133 anchors.margins: exactHeight < 10 ? 1 : Style.marginS
1134 anchors.leftMargin: exactHeight < 10 ? 1 : Style.marginS + 3
1135 sourceComponent: isDeadline ? deadlineLayout : (isCompact ? compactLayout : normalLayout)
1136 }
1137 }
1138
1139 Component {
1140 id: normalLayout
1141 Column {
1142 spacing: 2
1143 width: parent.width - 3
1144 NText {
1145 visible: exactHeight >= 20
1146 text: (isTodoItem ? (model.todoStatus === "COMPLETED" ? "\u2611 " : "\u2610 ") : "") + model.title
1147 color: eventTextColor
1148 font.pointSize: Style.fontSizeXS; font.weight: Font.Medium
1149 font.strikeout: isTodoItem && model.todoStatus === "COMPLETED"
1150 elide: Text.ElideRight; width: parent.width
1151 }
1152 NText {
1153 visible: exactHeight >= 30 && !isTodoItem
1154 text: mainInstance?.formatTimeRangeForDisplay(model) || ""
1155 color: eventTextColor
1156 font.pointSize: Style.fontSizeXXS; opacity: 0.9
1157 elide: Text.ElideRight; width: parent.width
1158 }
1159 NText {
1160 visible: exactHeight >= 45 && model.location && model.location !== ""
1161 text: "\u26B2 " + (model.location || "")
1162 color: eventTextColor
1163 font.pointSize: Style.fontSizeXXS; opacity: 0.8
1164 elide: Text.ElideRight; width: parent.width
1165 }
1166 }
1167 }
1168
1169 Component {
1170 id: compactLayout
1171 NText {
1172 text: {
1173 var prefix = isTodoItem ? (model.todoStatus === "COMPLETED" ? "\u2611 " : "\u2610 ") : ""
1174 if (exactHeight < 15) return prefix + model.title
1175 if (isTodoItem) return prefix + model.title
1176 return model.title + " \u2022 " + (mainInstance?.formatTimeRangeForDisplay(model) || "")
1177 }
1178 color: eventTextColor
1179 font.pointSize: exactHeight < 15 ? Style.fontSizeXXS : Style.fontSizeXS
1180 font.weight: Font.Medium
1181 font.strikeout: isTodoItem && model.todoStatus === "COMPLETED"
1182 elide: Text.ElideRight; verticalAlignment: Text.AlignVCenter
1183 width: parent.width - 3
1184 }
1185 }
1186
1187 Component {
1188 id: deadlineLayout
1189 Rectangle {
1190 anchors.fill: parent
1191 color: eventColor
1192 radius: parent.radius
1193 opacity: 0.95
1194 }
1195 }
1196
1197 MouseArea {
1198 anchors.fill: parent
1199 hoverEnabled: true
1200 cursorShape: Qt.PointingHandCursor
1201 onEntered: {
1202 var tip = mainInstance?.getEventTooltip(model) || ""
1203 TooltipService.show(parent, tip, "auto", Style.tooltipDelay, Settings.data.ui.fontFixed)
1204 }
1205 onClicked: {
1206 if (isTodoItem) {
1207 if (model.todoStatus === "COMPLETED")
1208 mainInstance?.uncompleteTodo(model.calendarUid, model.todoUid)
1209 else
1210 mainInstance?.completeTodo(model.calendarUid, model.todoUid)
1211 } else {
1212 mainInstance?.handleEventClick(eventData)
1213 showEventDetailDialog = true
1214 eventDetailEditMode = false
1215 showDeleteConfirmation = false
1216 }
1217 }
1218 onExited: TooltipService.hide()
1219 }
1220 }
1221 }
1222
1223 // Time Indicator
1224 Rectangle {
1225 property var now: new Date()
1226 property date today: new Date(now.getFullYear(), now.getMonth(), now.getDate())
1227 property date weekStartDate: mainInstance?.weekStart ?? new Date()
1228 property date weekEndDate: mainInstance ?
1229 new Date(mainInstance.weekStart.getFullYear(), mainInstance.weekStart.getMonth(), mainInstance.weekStart.getDate() + 7) : new Date()
1230 property bool inCurrentWeek: today >= weekStartDate && today < weekEndDate
1231 property int currentDay: mainInstance?.getDayIndexForDate(now) ?? -1
1232 property real currentHour: now.getHours() + now.getMinutes() / 60
1233
1234 visible: inCurrentWeek && currentDay >= 0
1235 width: mainInstance?.dayColumnWidth
1236 height: 2
1237 x: currentDay * ((mainInstance?.dayColumnWidth) + (root.daySpacing))
1238 y: currentHour * (root.hourHeight)
1239 color: Color.mError
1240 radius: 1
1241 z: 1000
1242 Rectangle {
1243 width: 8; height: 8; radius: 4; color: Color.mError
1244 anchors.verticalCenter: parent.verticalCenter; x: -4
1245 }
1246 Timer {
1247 interval: 60000; running: true; repeat: true
1248 onTriggered: parent.now = new Date()
1249 }
1250 }
1251 }
1252 }
1253 }
1254 }
1255 }
1256 }
1257
1258 }
1259 }
1260}