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 HH:MM" 268 color: Color.mOnSurface 269 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS } 270 } 271 272 NText { text: pluginApi.tr("panel.description"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS } 273 TextField { 274 id: createTaskDescription 275 Layout.fillWidth: true 276 placeholderText: pluginApi.tr("panel.description") 277 color: Color.mOnSurface 278 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS } 279 } 280 281 NText { text: pluginApi.tr("panel.priority"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS } 282 property int selectedPriority: 0 283 RowLayout { 284 spacing: Style.marginS 285 Repeater { 286 model: [ 287 { label: pluginApi.tr("panel.priority_high"), value: 1 }, 288 { label: pluginApi.tr("panel.priority_medium"), value: 5 }, 289 { label: pluginApi.tr("panel.priority_low"), value: 9 } 290 ] 291 Rectangle { 292 Layout.preferredWidth: priLabel.implicitWidth + 2 * Style.marginM 293 Layout.preferredHeight: priLabel.implicitHeight + Style.marginS 294 color: createTaskDialogColumn.selectedPriority === modelData.value ? Color.mPrimary : Color.mSurfaceVariant 295 radius: Style.radiusS 296 NText { 297 id: priLabel; anchors.centerIn: parent 298 text: modelData.label 299 color: createTaskDialogColumn.selectedPriority === modelData.value ? Color.mOnPrimary : Color.mOnSurfaceVariant 300 font.weight: Font.Medium 301 } 302 MouseArea { 303 anchors.fill: parent; cursorShape: Qt.PointingHandCursor 304 onClicked: createTaskDialogColumn.selectedPriority = 305 createTaskDialogColumn.selectedPriority === modelData.value ? 0 : modelData.value 306 } 307 } 308 } 309 } 310 311 NText { text: pluginApi.tr("panel.task_list_select"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS } 312 ComboBox { 313 id: taskListSelector 314 Layout.fillWidth: true 315 model: mainInstance?.taskLists || [] 316 textRole: "name" 317 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS } 318 } 319 320 RowLayout { 321 Layout.fillWidth: true 322 spacing: Style.marginS 323 324 Item { Layout.fillWidth: true } 325 326 Rectangle { 327 Layout.preferredWidth: taskCancelBtn.implicitWidth + 2 * Style.marginM 328 Layout.preferredHeight: taskCancelBtn.implicitHeight + Style.marginS 329 color: Color.mSurfaceVariant; radius: Style.radiusS 330 NText { 331 id: taskCancelBtn; anchors.centerIn: parent 332 text: pluginApi.tr("panel.cancel"); color: Color.mOnSurfaceVariant 333 } 334 MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor; onClicked: showCreateTaskDialog = false } 335 } 336 337 Rectangle { 338 Layout.preferredWidth: taskCreateBtn.implicitWidth + 2 * Style.marginM 339 Layout.preferredHeight: taskCreateBtn.implicitHeight + Style.marginS 340 color: Color.mPrimary; radius: Style.radiusS 341 opacity: createTaskSummary.text.trim() !== "" ? 1.0 : 0.5 342 NText { 343 id: taskCreateBtn; anchors.centerIn: parent 344 text: pluginApi.tr("panel.create"); color: Color.mOnPrimary; font.weight: Font.Bold 345 } 346 MouseArea { 347 anchors.fill: parent; cursorShape: Qt.PointingHandCursor 348 onClicked: { 349 if (createTaskSummary.text.trim() === "") return 350 var tl = mainInstance?.taskLists?.[taskListSelector.currentIndex] 351 var tlUid = tl?.uid || "" 352 var dueTs = 0 353 if (createTaskDueDate.text.trim() !== "") { 354 var d = new Date(createTaskDueDate.text.trim()) 355 if (!isNaN(d.getTime())) dueTs = Math.floor(d.getTime() / 1000) 356 } 357 mainInstance?.createTodo(tlUid, createTaskSummary.text.trim(), 358 dueTs, createTaskDialogColumn.selectedPriority, 359 createTaskDescription.text.trim()) 360 showCreateTaskDialog = false 361 } 362 } 363 } 364 } 365 } 366 } 367 } 368 369 // UI 370 Rectangle { 371 id: panelContainer 372 anchors.fill: parent 373 color: "transparent" 374 375 ColumnLayout { 376 anchors.fill: parent 377 anchors.margins: Style.marginM 378 spacing: Style.marginM 379 380 //Header Section 381 Rectangle { 382 id: header 383 Layout.fillWidth: true 384 Layout.preferredHeight: topHeaderHeight 385 color: Color.mSurfaceVariant 386 radius: Style.radiusM 387 388 RowLayout { 389 anchors.margins: Style.marginM 390 anchors.fill: parent 391 392 NIcon { icon: "calendar-week"; pointSize: Style.fontSizeXXL; color: Color.mPrimary } 393 394 ColumnLayout { 395 Layout.fillHeight: true 396 spacing: 0 397 NText { 398 text: pluginApi.tr("panel.header") 399 font.pointSize: Style.fontSizeL; font.weight: Font.Bold; color: Color.mOnSurface 400 } 401 RowLayout { 402 spacing: Style.marginS 403 NText { 404 text: mainInstance?.monthRangeText || "" 405 font.pointSize: Style.fontSizeS; font.weight: Font.Medium; color: Color.mOnSurfaceVariant 406 } 407 Rectangle { 408 Layout.preferredWidth: 8; Layout.preferredHeight: 8; radius: 4 409 color: mainInstance?.isLoading ? Color.mError : 410 mainInstance?.syncStatus?.includes("No") ? Color.mError : Color.mOnSurfaceVariant 411 } 412 NText { 413 text: mainInstance?.syncStatus || "" 414 font.pointSize: Style.fontSizeS; color: Color.mOnSurfaceVariant 415 } 416 } 417 } 418 419 Item { Layout.fillWidth: true } 420 421 RowLayout { 422 spacing: Style.marginS 423 NIconButton { 424 icon: "plus"; tooltipText: pluginApi.tr("panel.add_event") 425 onClicked: { 426 createEventSummary.text = "" 427 createEventLocation.text = "" 428 createEventDescription.text = "" 429 var now = new Date() 430 var startH = now.getHours() + 1 431 createEventDate.text = now.getFullYear() + "-" + String(now.getMonth()+1).padStart(2,'0') + "-" + String(now.getDate()).padStart(2,'0') 432 createEventStartTime.text = String(startH).padStart(2,'0') + ":00" 433 createEventEndTime.text = String(startH+1).padStart(2,'0') + ":00" 434 showCreateDialog = true 435 } 436 } 437 NIconButton { 438 icon: "clipboard-check"; tooltipText: pluginApi.tr("panel.add_task") 439 onClicked: { 440 createTaskSummary.text = "" 441 createTaskDueDate.text = "" 442 createTaskDescription.text = "" 443 createTaskDialogColumn.selectedPriority = 0 444 showCreateTaskDialog = true 445 } 446 } 447 NIconButton { 448 icon: mainInstance?.showCompletedTodos ? "eye-off" : "eye" 449 tooltipText: pluginApi.tr("panel.show_completed") 450 onClicked: { 451 if (mainInstance) { 452 mainInstance.showCompletedTodos = !mainInstance.showCompletedTodos 453 mainInstance.loadTodos() 454 } 455 } 456 } 457 NIconButton { 458 icon: "chevron-left" 459 onClicked: mainInstance?.navigateWeek(-7) 460 } 461 NIconButton { 462 icon: "calendar"; tooltipText: pluginApi.tr("panel.today") 463 onClicked: { mainInstance?.goToToday(); Qt.callLater(root.scrollToCurrentTime) } 464 } 465 NIconButton { 466 icon: "chevron-right" 467 onClicked: mainInstance?.navigateWeek(7) 468 } 469 NIconButton { 470 icon: "refresh"; tooltipText: I18n.tr("common.refresh") 471 onClicked: { mainInstance?.loadEvents(); mainInstance?.loadTodos() } 472 enabled: mainInstance ? !mainInstance.isLoading : false 473 } 474 NIconButton { 475 icon: "close"; tooltipText: I18n.tr("common.close") 476 onClicked: pluginApi.closePanel(pluginApi.panelOpenScreen) 477 } 478 } 479 } 480 } 481 482 // Calendar View 483 Rectangle { 484 Layout.fillWidth: true 485 Layout.fillHeight: true 486 color: Color.mSurfaceVariant 487 radius: Style.radiusM 488 clip: true 489 490 Column { 491 anchors.fill: parent 492 spacing: 0 493 494 //Day Headers 495 Rectangle { 496 id: dayHeaders 497 width: parent.width 498 height: 56 499 color: Color.mSurfaceVariant 500 radius: Style.radiusM 501 502 Row { 503 anchors.fill: parent 504 anchors.leftMargin: root.timeColumnWidth 505 spacing: root.daySpacing 506 507 Repeater { 508 model: 7 509 Rectangle { 510 width: mainInstance?.dayColumnWidth 511 height: parent.height 512 color: "transparent" 513 property date dayDate: mainInstance?.weekDates?.[index] || new Date() 514 property bool isToday: { 515 var today = new Date() 516 return dayDate.getDate() === today.getDate() && 517 dayDate.getMonth() === today.getMonth() && 518 dayDate.getFullYear() === today.getFullYear() 519 } 520 Rectangle { 521 anchors.fill: parent 522 anchors.margins: 4 523 color: Color.mSurfaceVariant 524 border.color: isToday ? Color.mPrimary : "transparent" 525 border.width: 2 526 radius: Style.radiusM 527 Column { 528 anchors.centerIn: parent 529 spacing: 2 530 NText { 531 anchors.horizontalCenter: parent.horizontalCenter 532 text: dayDate ? I18n.locale.dayName(dayDate.getDay(), Locale.ShortFormat).toUpperCase() : "" 533 color: isToday ? Color.mPrimary : Color.mOnSurface 534 font.pointSize: Style.fontSizeS; font.weight: Font.Medium 535 } 536 NText { 537 anchors.horizontalCenter: parent.horizontalCenter 538 text: dayDate ? ((dayDate.getDate() < 10 ? "0" : "") + dayDate.getDate()) : "" 539 color: isToday ? Color.mPrimary : Color.mOnSurface 540 font.pointSize: Style.fontSizeM; font.weight: Font.Bold 541 } 542 } 543 } 544 } 545 } 546 } 547 } 548 // All-day row 549 Rectangle { 550 id: allDayEventsSection 551 width: parent.width 552 height: mainInstance ? Math.round(mainInstance.allDaySectionHeight * Style.uiScaleRatio) : 0 553 color: Color.mSurfaceVariant 554 visible: height > 0 555 556 Item { 557 id: allDayEventsContainer 558 anchors.fill: parent 559 anchors.leftMargin: root.timeColumnWidth 560 561 Repeater { 562 model: 6 563 delegate: Rectangle { 564 width: 1; height: parent.height 565 x: (index + 1) * ((mainInstance?.dayColumnWidth) + (root.daySpacing)) - ((root.daySpacing) / 2) 566 color: Qt.alpha(mainInstance?.lineColor || Color.mOutline, mainInstance?.dayLineOpacitySetting || 0.9) 567 } 568 } 569 570 Repeater { 571 model: mainInstance?.allDayEventsWithLayout || [] 572 delegate: Item { 573 property var eventData: modelData 574 property bool isTodoItem: eventData.isTodo || false 575 x: eventData.startDay * ((mainInstance?.dayColumnWidth) + (root.daySpacing)) 576 y: eventData.lane * 25 577 width: (eventData.spanDays * ((mainInstance?.dayColumnWidth) + (root.daySpacing))) - (root.daySpacing) 578 height: 24 579 580 Rectangle { 581 anchors.fill: parent 582 color: isTodoItem ? Color.mSecondary : Color.mTertiary 583 radius: Style.radiusS 584 opacity: isTodoItem && eventData.todoStatus === "COMPLETED" ? 0.5 : 1.0 585 NText { 586 anchors.fill: parent; anchors.margins: 4 587 text: (isTodoItem ? (eventData.todoStatus === "COMPLETED" ? "\u2611 " : "\u2610 ") : "") + eventData.title 588 color: isTodoItem ? Color.mOnSecondary : Color.mOnTertiary 589 font.pointSize: Style.fontSizeXXS; font.weight: Font.Medium 590 font.strikeout: isTodoItem && eventData.todoStatus === "COMPLETED" 591 elide: Text.ElideRight; verticalAlignment: Text.AlignVCenter 592 } 593 } 594 MouseArea { 595 anchors.fill: parent 596 hoverEnabled: true 597 cursorShape: Qt.PointingHandCursor 598 onEntered: { 599 var tip = mainInstance?.getEventTooltip(eventData) || "" 600 TooltipService.show(parent, tip, "auto", Style.tooltipDelay, Settings.data.ui.fontFixed) 601 } 602 onClicked: { 603 if (isTodoItem) { 604 if (eventData.todoStatus === "COMPLETED") 605 mainInstance?.uncompleteTodo(eventData.calendarUid, eventData.todoUid) 606 else 607 mainInstance?.completeTodo(eventData.calendarUid, eventData.todoUid) 608 } else { 609 mainInstance?.handleEventClick(eventData) 610 } 611 } 612 onExited: TooltipService.hide() 613 } 614 } 615 } 616 } 617 } 618 // Calendar flickable 619 Rectangle { 620 width: parent.width 621 height: parent.height - dayHeaders.height - (allDayEventsSection.visible ? allDayEventsSection.height : 0) 622 color: Color.mSurfaceVariant 623 radius: Style.radiusM 624 clip: true 625 626 Flickable { 627 id: calendarFlickable 628 anchors.fill: parent 629 clip: true 630 contentHeight: 24 * (root.hourHeight) 631 boundsBehavior: Flickable.DragOverBounds 632 onHeightChanged: Qt.callLater(root.adjustHourHeightForViewport) 633 634 Component.onCompleted: { 635 calendarFlickable.forceActiveFocus() 636 } 637 638 // Keyboard interaction 639 Keys.onPressed: function(event) { 640 if (event.key === Qt.Key_Up || event.key === Qt.Key_Down) { 641 var step = root.hourHeight 642 var targetY = event.key === Qt.Key_Up ? Math.max(0, contentY - step) : 643 Math.min(Math.max(0, contentHeight - height), contentY + step) 644 scrollAnim.targetY = targetY 645 scrollAnim.start() 646 event.accepted = true 647 } else if (event.key === Qt.Key_Left || event.key === Qt.Key_Right) { 648 if (mainInstance) { 649 mainInstance.navigateWeek(event.key === Qt.Key_Left ? -7 : 7) 650 } 651 event.accepted = true 652 } 653 } 654 655 NumberAnimation { 656 id: scrollAnim 657 target: calendarFlickable; property: "contentY"; duration: 100 658 easing.type: Easing.OutCubic; property real targetY: 0; to: targetY 659 } 660 661 ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } 662 663 Row { 664 width: parent.width 665 height: parent.height 666 667 // Time Column 668 Column { 669 width: root.timeColumnWidth 670 height: parent.height 671 Repeater { 672 model: 23 673 Rectangle { 674 width: root.timeColumnWidth 675 height: root.hourHeight 676 color: "transparent" 677 NText { 678 text: { 679 var hour = index + 1 680 if (mainInstance?.use12hourFormat) { 681 var d = new Date(); d.setHours(hour, 0, 0, 0) 682 return mainInstance.formatTime(d) 683 } 684 return (hour < 10 ? "0" : "") + hour + ':00' 685 } 686 anchors.right: parent.right 687 anchors.rightMargin: Style.marginS 688 anchors.verticalCenter: parent.top 689 anchors.verticalCenterOffset: root.hourHeight 690 font.pointSize: Style.fontSizeXS; color: Color.mOnSurfaceVariant 691 } 692 } 693 } 694 } 695 696 // Hour Rectangles 697 Item { 698 width: 7 * ((mainInstance?.dayColumnWidth) + (root.daySpacing)) 699 height: parent.height 700 701 Row { 702 anchors.fill: parent 703 spacing: root.daySpacing 704 Repeater { 705 model: 7 706 Column { 707 width: mainInstance?.dayColumnWidth 708 height: parent.height 709 Repeater { 710 model: 24 711 Rectangle { width: parent.width; height: 1; color: Color.mSurfaceVariant } 712 } 713 } 714 } 715 } 716 // Hour Lines 717 Repeater { 718 model: 24 719 Rectangle { 720 width: parent.width; height: 1 721 y: index * (root.hourHeight) 722 color: Qt.alpha(mainInstance?.lineColor || Color.mOutline, mainInstance?.hourLineOpacitySetting || 0.5) 723 } 724 } 725 // Day Lines 726 Repeater { 727 model: 6 728 Rectangle { 729 width: 1; height: parent.height 730 x: (index + 1) * ((mainInstance?.dayColumnWidth) + (root.daySpacing)) - ((root.daySpacing) / 2) 731 color: Qt.alpha(mainInstance?.lineColor || Color.mOutline, mainInstance?.dayLineOpacitySetting || 0.9) 732 } 733 } 734 735 // Event positioning 736 Repeater { 737 model: mainInstance?.eventsModel 738 delegate: Item { 739 property var eventData: model 740 property int dayIndex: mainInstance?.getDisplayDayIndexForDate(model.startTime) ?? -1 741 property real startHour: model.startTime.getHours() + model.startTime.getMinutes() / 60 742 property real endHour: model.endTime.getHours() + model.endTime.getMinutes() / 60 743 property real duration: Math.max(0, (model.endTime - model.startTime) / 3600000) 744 745 property real exactHeight: Math.max(1, duration * (root.hourHeight) - 1) 746 property bool isCompact: exactHeight < 40 747 property var overlapInfo: mainInstance?.overlappingEventsData?.[index] ?? { 748 xOffset: 0, width: (mainInstance?.dayColumnWidth) - 8, lane: 0, totalLanes: 1 749 } 750 property real eventWidth: overlapInfo.width - 1 751 property real eventXOffset: overlapInfo.xOffset 752 753 visible: dayIndex >= 0 && dayIndex < 7 && duration > 0 754 width: eventWidth 755 height: exactHeight 756 x: dayIndex * ((mainInstance?.dayColumnWidth) + (root.daySpacing)) + eventXOffset 757 y: startHour * (root.hourHeight) 758 z: 100 + overlapInfo.lane 759 760 property bool isTodoItem: model.isTodo || false 761 property color eventColor: isTodoItem ? Color.mSecondary : Color.mPrimary 762 property color eventTextColor: isTodoItem ? Color.mOnSecondary : Color.mOnPrimary 763 764 Rectangle { 765 anchors.fill: parent 766 color: eventColor 767 radius: Style.radiusS 768 opacity: isTodoItem && model.todoStatus === "COMPLETED" ? 0.5 : 0.9 769 clip: true 770 Rectangle { 771 visible: exactHeight < 5 && overlapInfo.lane > 0 772 anchors.fill: parent 773 color: "transparent" 774 radius: parent.radius 775 border.width: 1 776 border.color: eventColor 777 } 778 Loader { 779 anchors.fill: parent 780 anchors.margins: exactHeight < 10 ? 1 : Style.marginS 781 anchors.leftMargin: exactHeight < 10 ? 1 : Style.marginS + 3 782 sourceComponent: isCompact ? compactLayout : normalLayout 783 } 784 } 785 786 Component { 787 id: normalLayout 788 Column { 789 spacing: 2 790 width: parent.width - 3 791 NText { 792 visible: exactHeight >= 20 793 text: (isTodoItem ? (model.todoStatus === "COMPLETED" ? "\u2611 " : "\u2610 ") : "") + model.title 794 color: eventTextColor 795 font.pointSize: Style.fontSizeXS; font.weight: Font.Medium 796 font.strikeout: isTodoItem && model.todoStatus === "COMPLETED" 797 elide: Text.ElideRight; width: parent.width 798 } 799 NText { 800 visible: exactHeight >= 30 && !isTodoItem 801 text: mainInstance?.formatTimeRangeForDisplay(model) || "" 802 color: eventTextColor 803 font.pointSize: Style.fontSizeXXS; opacity: 0.9 804 elide: Text.ElideRight; width: parent.width 805 } 806 NText { 807 visible: exactHeight >= 45 && model.location && model.location !== "" 808 text: "\u26B2 " + (model.location || "") 809 color: eventTextColor 810 font.pointSize: Style.fontSizeXXS; opacity: 0.8 811 elide: Text.ElideRight; width: parent.width 812 } 813 } 814 } 815 816 Component { 817 id: compactLayout 818 NText { 819 text: { 820 var prefix = isTodoItem ? (model.todoStatus === "COMPLETED" ? "\u2611 " : "\u2610 ") : "" 821 if (exactHeight < 15) return prefix + model.title 822 if (isTodoItem) return prefix + model.title 823 return model.title + " \u2022 " + (mainInstance?.formatTimeRangeForDisplay(model) || "") 824 } 825 color: eventTextColor 826 font.pointSize: exactHeight < 15 ? Style.fontSizeXXS : Style.fontSizeXS 827 font.weight: Font.Medium 828 font.strikeout: isTodoItem && model.todoStatus === "COMPLETED" 829 elide: Text.ElideRight; verticalAlignment: Text.AlignVCenter 830 width: parent.width - 3 831 } 832 } 833 834 MouseArea { 835 anchors.fill: parent 836 hoverEnabled: true 837 cursorShape: Qt.PointingHandCursor 838 onEntered: { 839 var tip = mainInstance?.getEventTooltip(model) || "" 840 TooltipService.show(parent, tip, "auto", Style.tooltipDelay, Settings.data.ui.fontFixed) 841 } 842 onClicked: { 843 if (isTodoItem) { 844 if (model.todoStatus === "COMPLETED") 845 mainInstance?.uncompleteTodo(model.calendarUid, model.todoUid) 846 else 847 mainInstance?.completeTodo(model.calendarUid, model.todoUid) 848 } else { 849 mainInstance?.handleEventClick(eventData) 850 } 851 } 852 onExited: TooltipService.hide() 853 } 854 } 855 } 856 857 // Time Indicator 858 Rectangle { 859 property var now: new Date() 860 property date today: new Date(now.getFullYear(), now.getMonth(), now.getDate()) 861 property date weekStartDate: mainInstance?.weekStart ?? new Date() 862 property date weekEndDate: mainInstance ? 863 new Date(mainInstance.weekStart.getFullYear(), mainInstance.weekStart.getMonth(), mainInstance.weekStart.getDate() + 7) : new Date() 864 property bool inCurrentWeek: today >= weekStartDate && today < weekEndDate 865 property int currentDay: mainInstance?.getDayIndexForDate(now) ?? -1 866 property real currentHour: now.getHours() + now.getMinutes() / 60 867 868 visible: inCurrentWeek && currentDay >= 0 869 width: mainInstance?.dayColumnWidth 870 height: 2 871 x: currentDay * ((mainInstance?.dayColumnWidth) + (root.daySpacing)) 872 y: currentHour * (root.hourHeight) 873 color: Color.mError 874 radius: 1 875 z: 1000 876 Rectangle { 877 width: 8; height: 8; radius: 4; color: Color.mError 878 anchors.verticalCenter: parent.verticalCenter; x: -4 879 } 880 Timer { 881 interval: 60000; running: true; repeat: true 882 onTriggered: parent.now = new Date() 883 } 884 } 885 } 886 } 887 } 888 } 889 } 890 } 891 892 } 893 } 894}