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: 700 * 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
25 property real hourHeight: 50 * Style.uiScaleRatio
26 property real timeColumnWidth: 65 * Style.uiScaleRatio
27 property real daySpacing: 1 * Style.uiScaleRatio
28
29 // Attempt at live syncing
30 Connections {
31 target: CalendarService
32 enabled: root.visible
33 function onEventsChanged() {
34 if (mainInstance) {
35 Qt.callLater(() => {
36 mainInstance.updateEventsFromService()
37 mainInstance.calculateAllDayEventLayout()
38 })
39 }
40 }
41 }
42
43 Component.onCompleted: mainInstance?.initializePlugin()
44 onVisibleChanged: if (visible && mainInstance) {
45 mainInstance.updateEventsFromService()
46 mainInstance.goToToday()
47 Qt.callLater(root.scrollToCurrentTime)
48 }
49
50 // Scroll to time indicator position
51 function scrollToCurrentTime() {
52 if (!mainInstance || !calendarFlickable) return
53 var now = new Date(), today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
54 var weekStart = new Date(mainInstance.weekStart)
55 var weekEnd = new Date(weekStart.getFullYear(), weekStart.getMonth(), weekStart.getDate() + 7)
56
57 if (today >= weekStart && today < weekEnd) {
58 var currentHour = now.getHours() + now.getMinutes() / 60
59 var scrollPos = (currentHour * hourHeight) - (calendarFlickable.height / 2)
60 var maxScroll = Math.max(0, (24 * hourHeight) - calendarFlickable.height)
61 scrollAnim.targetY = Math.max(0, Math.min(scrollPos, maxScroll))
62 scrollAnim.start()
63 }
64 }
65
66 // Event creation dialog
67 Rectangle {
68 id: createEventOverlay
69 anchors.fill: parent
70 color: Qt.rgba(0, 0, 0, 0.5)
71 visible: showCreateDialog
72 z: 2000
73
74 MouseArea { anchors.fill: parent; onClicked: showCreateDialog = false }
75
76 Rectangle {
77 anchors.centerIn: parent
78 width: 400 * Style.uiScaleRatio
79 height: createDialogColumn.implicitHeight + 2 * Style.marginM
80 color: Color.mSurface
81 radius: Style.radiusM
82
83 MouseArea { anchors.fill: parent } // block clicks through
84
85 ColumnLayout {
86 id: createDialogColumn
87 anchors.fill: parent
88 anchors.margins: Style.marginM
89 spacing: Style.marginS
90
91 NText {
92 text: pluginApi.tr("panel.add_event")
93 font.pointSize: Style.fontSizeL; font.weight: Font.Bold
94 color: Color.mOnSurface
95 }
96
97 NText { text: pluginApi.tr("panel.summary"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
98 TextField {
99 id: createEventSummary
100 Layout.fillWidth: true
101 placeholderText: pluginApi.tr("panel.summary")
102 color: Color.mOnSurface
103 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS }
104 }
105
106 NText { text: pluginApi.tr("panel.date"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
107 TextField {
108 id: createEventDate
109 Layout.fillWidth: true
110 placeholderText: "YYYY-MM-DD"
111 color: Color.mOnSurface
112 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS }
113 }
114
115 RowLayout {
116 spacing: Style.marginS
117 ColumnLayout {
118 Layout.fillWidth: true
119 NText { text: pluginApi.tr("panel.start_time"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
120 TextField {
121 id: createEventStartTime
122 Layout.fillWidth: true
123 placeholderText: "HH:MM"
124 color: Color.mOnSurface
125 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS }
126 }
127 }
128 ColumnLayout {
129 Layout.fillWidth: true
130 NText { text: pluginApi.tr("panel.end_time"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
131 TextField {
132 id: createEventEndTime
133 Layout.fillWidth: true
134 placeholderText: "HH:MM"
135 color: Color.mOnSurface
136 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS }
137 }
138 }
139 }
140
141 NText { text: pluginApi.tr("panel.location"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
142 TextField {
143 id: createEventLocation
144 Layout.fillWidth: true
145 placeholderText: pluginApi.tr("panel.location")
146 color: Color.mOnSurface
147 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS }
148 }
149
150 NText { text: pluginApi.tr("panel.description"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
151 TextField {
152 id: createEventDescription
153 Layout.fillWidth: true
154 placeholderText: pluginApi.tr("panel.description")
155 color: Color.mOnSurface
156 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS }
157 }
158
159 NText { text: pluginApi.tr("panel.calendar_select"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS }
160 ComboBox {
161 id: calendarSelector
162 Layout.fillWidth: true
163 model: CalendarService.calendars || []
164 textRole: "name"
165 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS }
166 }
167
168 RowLayout {
169 Layout.fillWidth: true
170 spacing: Style.marginS
171
172 Item { Layout.fillWidth: true }
173
174 Rectangle {
175 Layout.preferredWidth: cancelBtn.implicitWidth + 2 * Style.marginM
176 Layout.preferredHeight: cancelBtn.implicitHeight + Style.marginS
177 color: Color.mSurfaceVariant; radius: Style.radiusS
178 NText {
179 id: cancelBtn; anchors.centerIn: parent
180 text: pluginApi.tr("panel.cancel"); color: Color.mOnSurfaceVariant
181 }
182 MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor; onClicked: showCreateDialog = false }
183 }
184
185 Rectangle {
186 Layout.preferredWidth: createBtn.implicitWidth + 2 * Style.marginM
187 Layout.preferredHeight: createBtn.implicitHeight + Style.marginS
188 color: Color.mPrimary; radius: Style.radiusS
189 opacity: createEventSummary.text.trim() !== "" ? 1.0 : 0.5
190 NText {
191 id: createBtn; anchors.centerIn: parent
192 text: pluginApi.tr("panel.create"); color: Color.mOnPrimary; font.weight: Font.Bold
193 }
194 MouseArea {
195 anchors.fill: parent; cursorShape: Qt.PointingHandCursor
196 onClicked: {
197 if (createEventSummary.text.trim() === "") return
198 var cal = CalendarService.calendars?.[calendarSelector.currentIndex]
199 var calUid = cal?.uid || ""
200 var dateParts = createEventDate.text.split("-")
201 var startParts = createEventStartTime.text.split(":")
202 var endParts = createEventEndTime.text.split(":")
203 var startDate = new Date(parseInt(dateParts[0]), parseInt(dateParts[1])-1, parseInt(dateParts[2]),
204 parseInt(startParts[0]), parseInt(startParts[1]), 0)
205 var endDate = new Date(parseInt(dateParts[0]), parseInt(dateParts[1])-1, parseInt(dateParts[2]),
206 parseInt(endParts[0]), parseInt(endParts[1]), 0)
207 mainInstance?.createEvent(calUid, createEventSummary.text.trim(),
208 Math.floor(startDate.getTime()/1000), Math.floor(endDate.getTime()/1000),
209 createEventLocation.text.trim(), createEventDescription.text.trim())
210 showCreateDialog = false
211 }
212 }
213 }
214 }
215 }
216 }
217 }
218
219 // UI
220 Rectangle {
221 id: panelContainer
222 anchors.fill: parent
223 color: "transparent"
224
225 ColumnLayout {
226 anchors.fill: parent
227 anchors.margins: Style.marginM
228 spacing: Style.marginM
229
230 //Header Section
231 Rectangle {
232 id: header
233 Layout.fillWidth: true
234 Layout.preferredHeight: topHeaderHeight
235 color: Color.mSurfaceVariant
236 radius: Style.radiusM
237
238 RowLayout {
239 anchors.margins: Style.marginM
240 anchors.fill: parent
241
242 NIcon { icon: "calendar-week"; pointSize: Style.fontSizeXXL; color: Color.mPrimary }
243
244 ColumnLayout {
245 Layout.fillHeight: true
246 spacing: 0
247 NText {
248 text: pluginApi.tr("panel.header")
249 font.pointSize: Style.fontSizeL; font.weight: Font.Bold; color: Color.mOnSurface
250 }
251 RowLayout {
252 spacing: Style.marginS
253 NText {
254 text: mainInstance?.monthRangeText || ""
255 font.pointSize: Style.fontSizeS; font.weight: Font.Medium; color: Color.mOnSurfaceVariant
256 }
257 Rectangle {
258 Layout.preferredWidth: 8; Layout.preferredHeight: 8; radius: 4
259 color: mainInstance?.isLoading ? Color.mError :
260 mainInstance?.syncStatus?.includes("No") ? Color.mError : Color.mOnSurfaceVariant
261 }
262 NText {
263 text: mainInstance?.syncStatus || ""
264 font.pointSize: Style.fontSizeS; color: Color.mOnSurfaceVariant
265 }
266 }
267 }
268
269 Item { Layout.fillWidth: true }
270
271 RowLayout {
272 spacing: Style.marginS
273 NIconButton {
274 icon: "plus"; tooltipText: pluginApi.tr("panel.add_event")
275 onClicked: {
276 createEventSummary.text = ""
277 createEventLocation.text = ""
278 createEventDescription.text = ""
279 var now = new Date()
280 var startH = now.getHours() + 1
281 createEventDate.text = now.getFullYear() + "-" + String(now.getMonth()+1).padStart(2,'0') + "-" + String(now.getDate()).padStart(2,'0')
282 createEventStartTime.text = String(startH).padStart(2,'0') + ":00"
283 createEventEndTime.text = String(startH+1).padStart(2,'0') + ":00"
284 showCreateDialog = true
285 }
286 }
287 NIconButton {
288 icon: "chevron-left"
289 onClicked: { mainInstance?.navigateWeek(-7); mainInstance?.loadEvents() }
290 }
291 NIconButton {
292 icon: "calendar"; tooltipText: pluginApi.tr("panel.today")
293 onClicked: { mainInstance?.goToToday(); mainInstance?.loadEvents(); Qt.callLater(root.scrollToCurrentTime) }
294 }
295 NIconButton {
296 icon: "chevron-right"
297 onClicked: { mainInstance?.navigateWeek(7); mainInstance?.loadEvents() }
298 }
299 NIconButton {
300 icon: "refresh"; tooltipText: I18n.tr("common.refresh")
301 onClicked: mainInstance?.loadEvents()
302 enabled: mainInstance ? !mainInstance.isLoading : false
303 }
304 NIconButton {
305 icon: "close"; tooltipText: I18n.tr("common.close")
306 onClicked: pluginApi.closePanel(pluginApi.panelOpenScreen)
307 }
308 }
309 }
310 }
311
312 // Calendar
313 Rectangle {
314 Layout.fillWidth: true
315 Layout.fillHeight: true
316 color: Color.mSurfaceVariant
317 radius: Style.radiusM
318 clip: true
319
320 Column {
321 anchors.fill: parent
322 spacing: 0
323
324 //Day Headers
325 Rectangle {
326 id: dayHeaders
327 width: parent.width
328 height: 56
329 color: Color.mSurfaceVariant
330 radius: Style.radiusM
331
332 Row {
333 anchors.fill: parent
334 anchors.leftMargin: root.timeColumnWidth
335 spacing: root.daySpacing
336
337 Repeater {
338 model: 7
339 Rectangle {
340 width: mainInstance?.dayColumnWidth
341 height: parent.height
342 color: "transparent"
343 property date dayDate: mainInstance?.weekDates?.[index] || new Date()
344 property bool isToday: {
345 var today = new Date()
346 return dayDate.getDate() === today.getDate() &&
347 dayDate.getMonth() === today.getMonth() &&
348 dayDate.getFullYear() === today.getFullYear()
349 }
350 Rectangle {
351 anchors.fill: parent
352 anchors.margins: 4
353 color: Color.mSurfaceVariant
354 border.color: isToday ? Color.mPrimary : "transparent"
355 border.width: 2
356 radius: Style.radiusM
357 Column {
358 anchors.centerIn: parent
359 spacing: 2
360 NText {
361 anchors.horizontalCenter: parent.horizontalCenter
362 text: dayDate ? I18n.locale.dayName(dayDate.getDay(), Locale.ShortFormat).toUpperCase() : ""
363 color: isToday ? Color.mPrimary : Color.mOnSurface
364 font.pointSize: Style.fontSizeS; font.weight: Font.Medium
365 }
366 NText {
367 anchors.horizontalCenter: parent.horizontalCenter
368 text: dayDate ? ((dayDate.getDate() < 10 ? "0" : "") + dayDate.getDate()) : ""
369 color: isToday ? Color.mPrimary : Color.mOnSurface
370 font.pointSize: Style.fontSizeM; font.weight: Font.Bold
371 }
372 }
373 }
374 }
375 }
376 }
377 }
378 // All-day row
379 Rectangle {
380 id: allDayEventsSection
381 width: parent.width
382 height: mainInstance ? Math.round(mainInstance.allDaySectionHeight * Style.uiScaleRatio) : 0
383 color: Color.mSurfaceVariant
384 visible: height > 0
385
386 Item {
387 id: allDayEventsContainer
388 anchors.fill: parent
389 anchors.leftMargin: root.timeColumnWidth
390
391 Repeater {
392 model: 6
393 delegate: Rectangle {
394 width: 1; height: parent.height
395 x: (index + 1) * ((mainInstance?.dayColumnWidth) + (root.daySpacing)) - ((root.daySpacing) / 2)
396 color: Qt.alpha(mainInstance?.lineColor || Color.mOutline, mainInstance?.dayLineOpacitySetting || 0.9)
397 }
398 }
399
400 Repeater {
401 model: mainInstance?.allDayEventsWithLayout || []
402 delegate: Item {
403 property var eventData: modelData
404 x: eventData.startDay * ((mainInstance?.dayColumnWidth) + (root.daySpacing))
405 y: eventData.lane * 25
406 width: (eventData.spanDays * ((mainInstance?.dayColumnWidth) + (root.daySpacing))) - (root.daySpacing)
407 height: 24
408
409 Rectangle {
410 anchors.fill: parent
411 color: Color.mTertiary
412 radius: Style.radiusS
413 NText {
414 anchors.fill: parent; anchors.margins: 4
415 text: eventData.title
416 color: Color.mOnTertiary
417 font.pointSize: Style.fontSizeXXS; font.weight: Font.Medium
418 elide: Text.ElideRight; verticalAlignment: Text.AlignVCenter
419 }
420 }
421 MouseArea {
422 anchors.fill: parent
423 hoverEnabled: true
424 cursorShape: Qt.PointingHandCursor
425 onEntered: {
426 var tip = mainInstance?.getEventTooltip(eventData) || ""
427 TooltipService.show(parent, tip, "auto", Style.tooltipDelay, Settings.data.ui.fontFixed)
428 }
429 onClicked: mainInstance?.handleEventClick(eventData)
430 onExited: TooltipService.hide()
431 }
432 }
433 }
434 }
435 }
436 // Calendar flickable
437 Rectangle {
438 width: parent.width
439 height: parent.height - dayHeaders.height - (allDayEventsSection.visible ? allDayEventsSection.height : 0)
440 color: Color.mSurfaceVariant
441 radius: Style.radiusM
442 clip: true
443
444 Flickable {
445 id: calendarFlickable
446 anchors.fill: parent
447 clip: true
448 contentHeight: 24 * (root.hourHeight)
449 boundsBehavior: Flickable.DragOverBounds
450
451 Component.onCompleted: {
452 calendarFlickable.forceActiveFocus()
453 }
454
455 // Keyboard interaction
456 Keys.onPressed: function(event) {
457 if (event.key === Qt.Key_Up || event.key === Qt.Key_Down) {
458 var step = root.hourHeight
459 var targetY = event.key === Qt.Key_Up ? Math.max(0, contentY - step) :
460 Math.min(Math.max(0, contentHeight - height), contentY + step)
461 scrollAnim.targetY = targetY
462 scrollAnim.start()
463 event.accepted = true
464 } else if (event.key === Qt.Key_Left || event.key === Qt.Key_Right) {
465 if (mainInstance) {
466 mainInstance.navigateWeek(event.key === Qt.Key_Left ? -7 : 7)
467 mainInstance.loadEvents()
468 }
469 event.accepted = true
470 }
471 }
472
473 NumberAnimation {
474 id: scrollAnim
475 target: calendarFlickable; property: "contentY"; duration: 100
476 easing.type: Easing.OutCubic; property real targetY: 0; to: targetY
477 }
478
479 ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
480
481 Row {
482 width: parent.width
483 height: parent.height
484
485 // Time Column
486 Column {
487 width: root.timeColumnWidth
488 height: parent.height
489 Repeater {
490 model: 23
491 Rectangle {
492 width: root.timeColumnWidth
493 height: root.hourHeight
494 color: "transparent"
495 NText {
496 text: {
497 var hour = index + 1
498 if (mainInstance?.use12hourFormat) {
499 var d = new Date(); d.setHours(hour, 0, 0, 0)
500 return mainInstance.formatTime(d)
501 }
502 return (hour < 10 ? "0" : "") + hour + ':00'
503 }
504 anchors.right: parent.right
505 anchors.rightMargin: Style.marginS
506 anchors.verticalCenter: parent.top
507 anchors.verticalCenterOffset: root.hourHeight
508 font.pointSize: Style.fontSizeXS; color: Color.mOnSurfaceVariant
509 }
510 }
511 }
512 }
513
514 // Hour Rectangles
515 Item {
516 width: 7 * ((mainInstance?.dayColumnWidth) + (root.daySpacing))
517 height: parent.height
518
519 Row {
520 anchors.fill: parent
521 spacing: root.daySpacing
522 Repeater {
523 model: 7
524 Column {
525 width: mainInstance?.dayColumnWidth
526 height: parent.height
527 Repeater {
528 model: 24
529 Rectangle { width: parent.width; height: 1; color: Color.mSurfaceVariant }
530 }
531 }
532 }
533 }
534 // Hour Lines
535 Repeater {
536 model: 24
537 Rectangle {
538 width: parent.width; height: 1
539 y: index * (root.hourHeight)
540 color: Qt.alpha(mainInstance?.lineColor || Color.mOutline, mainInstance?.hourLineOpacitySetting || 0.5)
541 }
542 }
543 // Day Lines
544 Repeater {
545 model: 6
546 Rectangle {
547 width: 1; height: parent.height
548 x: (index + 1) * ((mainInstance?.dayColumnWidth) + (root.daySpacing)) - ((root.daySpacing) / 2)
549 color: Qt.alpha(mainInstance?.lineColor || Color.mOutline, mainInstance?.dayLineOpacitySetting || 0.9)
550 }
551 }
552
553 // Event positioning
554 Repeater {
555 model: mainInstance?.eventsModel
556 delegate: Item {
557 property var eventData: model
558 property int dayIndex: mainInstance?.getDisplayDayIndexForDate(model.startTime) ?? -1
559 property real startHour: model.startTime.getHours() + model.startTime.getMinutes() / 60
560 property real endHour: model.endTime.getHours() + model.endTime.getMinutes() / 60
561 property real duration: Math.max(0, (model.endTime - model.startTime) / 3600000)
562
563 property real exactHeight: Math.max(1, duration * (mainInstance?.hourHeight || 50) - 1)
564 property bool isCompact: exactHeight < 40
565 property var overlapInfo: mainInstance?.overlappingEventsData?.[index] ?? {
566 xOffset: 0, width: (mainInstance?.dayColumnWidth) - 8, lane: 0, totalLanes: 1
567 }
568 property real eventWidth: overlapInfo.width - 1
569 property real eventXOffset: overlapInfo.xOffset
570
571 visible: dayIndex >= 0 && dayIndex < 7 && duration > 0
572 width: eventWidth
573 height: exactHeight
574 x: dayIndex * ((mainInstance?.dayColumnWidth) + (root.daySpacing)) + eventXOffset
575 y: startHour * (mainInstance?.hourHeight || 50)
576 z: 100 + overlapInfo.lane
577
578 Rectangle {
579 anchors.fill: parent
580 color: Color.mPrimary
581 radius: Style.radiusS
582 opacity: 0.9
583 clip: true
584 Rectangle {
585 visible: exactHeight < 5 && overlapInfo.lane > 0
586 anchors.fill: parent
587 color: "transparent"
588 radius: parent.radius
589 border.width: 1
590 border.color: Color.mPrimary
591 }
592 Loader {
593 anchors.fill: parent
594 anchors.margins: exactHeight < 10 ? 1 : Style.marginS
595 anchors.leftMargin: exactHeight < 10 ? 1 : Style.marginS + 3
596 sourceComponent: isCompact ? compactLayout : normalLayout
597 }
598 }
599
600 Component {
601 id: normalLayout
602 Column {
603 spacing: 2
604 width: parent.width - 3
605 NText {
606 visible: exactHeight >= 20
607 text: model.title
608 color: Color.mOnPrimary
609 font.pointSize: Style.fontSizeXS; font.weight: Font.Medium
610 elide: Text.ElideRight; width: parent.width
611 }
612 NText {
613 visible: exactHeight >= 30
614 text: mainInstance?.formatTimeRangeForDisplay(model) || ""
615 color: Color.mOnPrimary
616 font.pointSize: Style.fontSizeXXS; opacity: 0.9
617 elide: Text.ElideRight; width: parent.width
618 }
619 NText {
620 visible: exactHeight >= 45 && model.location && model.location !== ""
621 text: "\u26B2 " + (model.location || "")
622 color: Color.mOnPrimary
623 font.pointSize: Style.fontSizeXXS; opacity: 0.8
624 elide: Text.ElideRight; width: parent.width
625 }
626 }
627 }
628
629 Component {
630 id: compactLayout
631 NText {
632 text: exactHeight < 15 ? model.title :
633 model.title + " • " + (mainInstance?.formatTimeRangeForDisplay(model) || "")
634 color: Color.mOnPrimary
635 font.pointSize: exactHeight < 15 ? Style.fontSizeXXS : Style.fontSizeXS
636 font.weight: Font.Medium
637 elide: Text.ElideRight; verticalAlignment: Text.AlignVCenter
638 width: parent.width - 3
639 }
640 }
641
642 MouseArea {
643 anchors.fill: parent
644 hoverEnabled: true
645 cursorShape: Qt.PointingHandCursor
646 onEntered: {
647 var tip = mainInstance?.getEventTooltip(model) || ""
648 TooltipService.show(parent, tip, "auto", Style.tooltipDelay, Settings.data.ui.fontFixed)
649 }
650 onClicked: mainInstance?.handleEventClick(eventData)
651 onExited: TooltipService.hide()
652 }
653 }
654 }
655
656 // Time Indicator
657 Rectangle {
658 property var now: new Date()
659 property date today: new Date(now.getFullYear(), now.getMonth(), now.getDate())
660 property date weekStartDate: mainInstance?.weekStart ?? new Date()
661 property date weekEndDate: mainInstance ?
662 new Date(mainInstance.weekStart.getFullYear(), mainInstance.weekStart.getMonth(), mainInstance.weekStart.getDate() + 7) : new Date()
663 property bool inCurrentWeek: today >= weekStartDate && today < weekEndDate
664 property int currentDay: mainInstance?.getDayIndexForDate(now) ?? -1
665 property real currentHour: now.getHours() + now.getMinutes() / 60
666
667 visible: inCurrentWeek && currentDay >= 0
668 width: mainInstance?.dayColumnWidth
669 height: 2
670 x: currentDay * ((mainInstance?.dayColumnWidth) + (root.daySpacing))
671 y: currentHour * (root.hourHeight)
672 color: Color.mError
673 radius: 1
674 z: 1000
675 Rectangle {
676 width: 8; height: 8; radius: 4; color: Color.mError
677 anchors.verticalCenter: parent.verticalCenter; x: -4
678 }
679 Timer {
680 interval: 60000; running: true; repeat: true
681 onTriggered: parent.now = new Date()
682 }
683 }
684 }
685 }
686 }
687 }
688 }
689 }
690 }
691 }
692}