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