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