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