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 property bool isDeadline: eventData.isDeadlineMarker || false
593 x: eventData.startDay * ((mainInstance?.dayColumnWidth) + (root.daySpacing))
594 y: eventData.lane * 25
595 width: (eventData.spanDays * ((mainInstance?.dayColumnWidth) + (root.daySpacing))) - (root.daySpacing)
596 height: isDeadline ? 10 : 24
597
598 Rectangle {
599 anchors.fill: parent
600 color: isDeadline ? Color.mSecondary : (isTodoItem ? Color.mSecondary : Color.mTertiary)
601 radius: Style.radiusS
602 opacity: isTodoItem && eventData.todoStatus === "COMPLETED" ? 0.5 : 1.0
603 NText {
604 anchors.fill: parent; anchors.margins: 4
605 text: isDeadline ? "" : (isTodoItem ? (eventData.todoStatus === "COMPLETED" ? "\u2611 " : "\u2610 ") : "") + eventData.title
606 color: isDeadline ? Color.mOnSecondary : (isTodoItem ? Color.mOnSecondary : Color.mOnTertiary)
607 font.pointSize: Style.fontSizeXXS; font.weight: Font.Medium
608 font.strikeout: isTodoItem && eventData.todoStatus === "COMPLETED"
609 elide: Text.ElideRight; verticalAlignment: Text.AlignVCenter
610 }
611 }
612 MouseArea {
613 anchors.fill: parent
614 hoverEnabled: true
615 cursorShape: Qt.PointingHandCursor
616 onEntered: {
617 var tip = mainInstance?.getEventTooltip(eventData) || ""
618 TooltipService.show(parent, tip, "auto", Style.tooltipDelay, Settings.data.ui.fontFixed)
619 }
620 onClicked: {
621 if (isTodoItem) {
622 if (eventData.todoStatus === "COMPLETED")
623 mainInstance?.uncompleteTodo(eventData.calendarUid, eventData.todoUid)
624 else
625 mainInstance?.completeTodo(eventData.calendarUid, eventData.todoUid)
626 } else {
627 mainInstance?.handleEventClick(eventData)
628 }
629 }
630 onExited: TooltipService.hide()
631 }
632 }
633 }
634 }
635 }
636 // Calendar flickable
637 Rectangle {
638 width: parent.width
639 height: parent.height - dayHeaders.height - (allDayEventsSection.visible ? allDayEventsSection.height : 0)
640 color: Color.mSurfaceVariant
641 radius: Style.radiusM
642 clip: true
643
644 Flickable {
645 id: calendarFlickable
646 anchors.fill: parent
647 clip: true
648 contentHeight: 24 * (root.hourHeight)
649 boundsBehavior: Flickable.DragOverBounds
650 onHeightChanged: Qt.callLater(root.adjustHourHeightForViewport)
651
652 Component.onCompleted: {
653 calendarFlickable.forceActiveFocus()
654 }
655
656 // Keyboard interaction
657 Keys.onPressed: function(event) {
658 if (event.key === Qt.Key_Up || event.key === Qt.Key_Down) {
659 var step = root.hourHeight
660 var targetY = event.key === Qt.Key_Up ? Math.max(0, contentY - step) :
661 Math.min(Math.max(0, contentHeight - height), contentY + step)
662 scrollAnim.targetY = targetY
663 scrollAnim.start()
664 event.accepted = true
665 } else if (event.key === Qt.Key_Left || event.key === Qt.Key_Right) {
666 if (mainInstance) {
667 mainInstance.navigateWeek(event.key === Qt.Key_Left ? -7 : 7)
668 }
669 event.accepted = true
670 }
671 }
672
673 NumberAnimation {
674 id: scrollAnim
675 target: calendarFlickable; property: "contentY"; duration: 100
676 easing.type: Easing.OutCubic; property real targetY: 0; to: targetY
677 }
678
679 ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
680
681 Row {
682 width: parent.width
683 height: parent.height
684
685 // Time Column
686 Column {
687 width: root.timeColumnWidth
688 height: parent.height
689 Repeater {
690 model: 23
691 Rectangle {
692 width: root.timeColumnWidth
693 height: root.hourHeight
694 color: "transparent"
695 NText {
696 text: {
697 var hour = index + 1
698 if (mainInstance?.use12hourFormat) {
699 var d = new Date(); d.setHours(hour, 0, 0, 0)
700 return mainInstance.formatTime(d)
701 }
702 return (hour < 10 ? "0" : "") + hour + ':00'
703 }
704 anchors.right: parent.right
705 anchors.rightMargin: Style.marginS
706 anchors.verticalCenter: parent.top
707 anchors.verticalCenterOffset: root.hourHeight
708 font.pointSize: Style.fontSizeXS; color: Color.mOnSurfaceVariant
709 }
710 }
711 }
712 }
713
714 // Hour Rectangles
715 Item {
716 width: 7 * ((mainInstance?.dayColumnWidth) + (root.daySpacing))
717 height: parent.height
718
719 Row {
720 anchors.fill: parent
721 spacing: root.daySpacing
722 Repeater {
723 model: 7
724 Column {
725 width: mainInstance?.dayColumnWidth
726 height: parent.height
727 Repeater {
728 model: 24
729 Rectangle { width: parent.width; height: 1; color: Color.mSurfaceVariant }
730 }
731 }
732 }
733 }
734 // Hour Lines
735 Repeater {
736 model: 24
737 Rectangle {
738 width: parent.width; height: 1
739 y: index * (root.hourHeight)
740 color: Qt.alpha(mainInstance?.lineColor || Color.mOutline, mainInstance?.hourLineOpacitySetting || 0.5)
741 }
742 }
743 // Day Lines
744 Repeater {
745 model: 6
746 Rectangle {
747 width: 1; height: parent.height
748 x: (index + 1) * ((mainInstance?.dayColumnWidth) + (root.daySpacing)) - ((root.daySpacing) / 2)
749 color: Qt.alpha(mainInstance?.lineColor || Color.mOutline, mainInstance?.dayLineOpacitySetting || 0.9)
750 }
751 }
752
753 // Event positioning
754 Repeater {
755 model: mainInstance?.eventsModel
756 delegate: Item {
757 property var eventData: model
758 property int dayIndex: mainInstance?.getDisplayDayIndexForDate(model.startTime) ?? -1
759 property real startHour: model.startTime.getHours() + model.startTime.getMinutes() / 60
760 property real endHour: model.endTime.getHours() + model.endTime.getMinutes() / 60
761 property real duration: Math.max(0, (model.endTime - model.startTime) / 3600000)
762
763 property real exactHeight: Math.max(1, duration * (root.hourHeight) - 1)
764 property bool isCompact: exactHeight < 40
765 property var overlapInfo: mainInstance?.overlappingEventsData?.[index] ?? {
766 xOffset: 0, width: (mainInstance?.dayColumnWidth) - 8, lane: 0, totalLanes: 1
767 }
768 property real eventWidth: overlapInfo.width - 1
769 property real eventXOffset: overlapInfo.xOffset
770
771 property bool isTodoItem: model.isTodo || false
772 property bool isDeadline: model.isDeadlineMarker || false
773 property color eventColor: isDeadline ? Color.mSecondary : (isTodoItem ? Color.mSecondary : Color.mPrimary)
774 property color eventTextColor: isDeadline ? Color.mOnSecondary : (isTodoItem ? Color.mOnSecondary : Color.mOnPrimary)
775
776 visible: dayIndex >= 0 && dayIndex < 7 && duration > 0
777 width: eventWidth
778 height: isDeadline ? Math.max(8, Math.min(12, exactHeight)) : exactHeight
779 x: dayIndex * ((mainInstance?.dayColumnWidth) + (root.daySpacing)) + eventXOffset
780 y: startHour * (root.hourHeight)
781 z: 100 + overlapInfo.lane
782
783 Rectangle {
784 anchors.fill: parent
785 color: eventColor
786 radius: Style.radiusS
787 opacity: isDeadline ? 0.95 : (isTodoItem && model.todoStatus === "COMPLETED" ? 0.5 : 0.9)
788 clip: true
789 Rectangle {
790 visible: exactHeight < 5 && overlapInfo.lane > 0
791 anchors.fill: parent
792 color: "transparent"
793 radius: parent.radius
794 border.width: 1
795 border.color: eventColor
796 }
797 Loader {
798 anchors.fill: parent
799 anchors.margins: exactHeight < 10 ? 1 : Style.marginS
800 anchors.leftMargin: exactHeight < 10 ? 1 : Style.marginS + 3
801 sourceComponent: isDeadline ? deadlineLayout : (isCompact ? compactLayout : normalLayout)
802 }
803 }
804
805 Component {
806 id: normalLayout
807 Column {
808 spacing: 2
809 width: parent.width - 3
810 NText {
811 visible: exactHeight >= 20
812 text: (isTodoItem ? (model.todoStatus === "COMPLETED" ? "\u2611 " : "\u2610 ") : "") + model.title
813 color: eventTextColor
814 font.pointSize: Style.fontSizeXS; font.weight: Font.Medium
815 font.strikeout: isTodoItem && model.todoStatus === "COMPLETED"
816 elide: Text.ElideRight; width: parent.width
817 }
818 NText {
819 visible: exactHeight >= 30 && !isTodoItem
820 text: mainInstance?.formatTimeRangeForDisplay(model) || ""
821 color: eventTextColor
822 font.pointSize: Style.fontSizeXXS; opacity: 0.9
823 elide: Text.ElideRight; width: parent.width
824 }
825 NText {
826 visible: exactHeight >= 45 && model.location && model.location !== ""
827 text: "\u26B2 " + (model.location || "")
828 color: eventTextColor
829 font.pointSize: Style.fontSizeXXS; opacity: 0.8
830 elide: Text.ElideRight; width: parent.width
831 }
832 }
833 }
834
835 Component {
836 id: compactLayout
837 NText {
838 text: {
839 var prefix = isTodoItem ? (model.todoStatus === "COMPLETED" ? "\u2611 " : "\u2610 ") : ""
840 if (exactHeight < 15) return prefix + model.title
841 if (isTodoItem) return prefix + model.title
842 return model.title + " \u2022 " + (mainInstance?.formatTimeRangeForDisplay(model) || "")
843 }
844 color: eventTextColor
845 font.pointSize: exactHeight < 15 ? Style.fontSizeXXS : Style.fontSizeXS
846 font.weight: Font.Medium
847 font.strikeout: isTodoItem && model.todoStatus === "COMPLETED"
848 elide: Text.ElideRight; verticalAlignment: Text.AlignVCenter
849 width: parent.width - 3
850 }
851 }
852
853 Component {
854 id: deadlineLayout
855 Rectangle {
856 anchors.fill: parent
857 color: eventColor
858 radius: parent.radius
859 opacity: 0.95
860 }
861 }
862
863 MouseArea {
864 anchors.fill: parent
865 hoverEnabled: true
866 cursorShape: Qt.PointingHandCursor
867 onEntered: {
868 var tip = mainInstance?.getEventTooltip(model) || ""
869 TooltipService.show(parent, tip, "auto", Style.tooltipDelay, Settings.data.ui.fontFixed)
870 }
871 onClicked: {
872 if (isTodoItem) {
873 if (model.todoStatus === "COMPLETED")
874 mainInstance?.uncompleteTodo(model.calendarUid, model.todoUid)
875 else
876 mainInstance?.completeTodo(model.calendarUid, model.todoUid)
877 } else {
878 mainInstance?.handleEventClick(eventData)
879 }
880 }
881 onExited: TooltipService.hide()
882 }
883 }
884 }
885
886 // Time Indicator
887 Rectangle {
888 property var now: new Date()
889 property date today: new Date(now.getFullYear(), now.getMonth(), now.getDate())
890 property date weekStartDate: mainInstance?.weekStart ?? new Date()
891 property date weekEndDate: mainInstance ?
892 new Date(mainInstance.weekStart.getFullYear(), mainInstance.weekStart.getMonth(), mainInstance.weekStart.getDate() + 7) : new Date()
893 property bool inCurrentWeek: today >= weekStartDate && today < weekEndDate
894 property int currentDay: mainInstance?.getDayIndexForDate(now) ?? -1
895 property real currentHour: now.getHours() + now.getMinutes() / 60
896
897 visible: inCurrentWeek && currentDay >= 0
898 width: mainInstance?.dayColumnWidth
899 height: 2
900 x: currentDay * ((mainInstance?.dayColumnWidth) + (root.daySpacing))
901 y: currentHour * (root.hourHeight)
902 color: Color.mError
903 radius: 1
904 z: 1000
905 Rectangle {
906 width: 8; height: 8; radius: 4; color: Color.mError
907 anchors.verticalCenter: parent.verticalCenter; x: -4
908 }
909 Timer {
910 interval: 60000; running: true; repeat: true
911 onTriggered: parent.now = new Date()
912 }
913 }
914 }
915 }
916 }
917 }
918 }
919 }
920
921 }
922 }
923}