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