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