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