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 property bool isDeadline: eventData.isDeadlineMarker || false 593 x: eventData.startDay * ((mainInstance?.dayColumnWidth) + (root.daySpacing)) 594 y: eventData.lane * 25 595 width: (eventData.spanDays * ((mainInstance?.dayColumnWidth) + (root.daySpacing))) - (root.daySpacing) 596 height: isDeadline ? 10 : 24 597 598 Rectangle { 599 anchors.fill: parent 600 color: isDeadline ? Color.mSecondary : (isTodoItem ? Color.mSecondary : Color.mTertiary) 601 radius: Style.radiusS 602 opacity: isTodoItem && eventData.todoStatus === "COMPLETED" ? 0.5 : 1.0 603 NText { 604 anchors.fill: parent; anchors.margins: 4 605 text: isDeadline ? "" : (isTodoItem ? (eventData.todoStatus === "COMPLETED" ? "\u2611 " : "\u2610 ") : "") + eventData.title 606 color: isDeadline ? Color.mOnSecondary : (isTodoItem ? Color.mOnSecondary : Color.mOnTertiary) 607 font.pointSize: Style.fontSizeXXS; font.weight: Font.Medium 608 font.strikeout: isTodoItem && eventData.todoStatus === "COMPLETED" 609 elide: Text.ElideRight; verticalAlignment: Text.AlignVCenter 610 } 611 } 612 MouseArea { 613 anchors.fill: parent 614 hoverEnabled: true 615 cursorShape: Qt.PointingHandCursor 616 onEntered: { 617 var tip = mainInstance?.getEventTooltip(eventData) || "" 618 TooltipService.show(parent, tip, "auto", Style.tooltipDelay, Settings.data.ui.fontFixed) 619 } 620 onClicked: { 621 if (isTodoItem) { 622 if (eventData.todoStatus === "COMPLETED") 623 mainInstance?.uncompleteTodo(eventData.calendarUid, eventData.todoUid) 624 else 625 mainInstance?.completeTodo(eventData.calendarUid, eventData.todoUid) 626 } else { 627 mainInstance?.handleEventClick(eventData) 628 } 629 } 630 onExited: TooltipService.hide() 631 } 632 } 633 } 634 } 635 } 636 // Calendar flickable 637 Rectangle { 638 width: parent.width 639 height: parent.height - dayHeaders.height - (allDayEventsSection.visible ? allDayEventsSection.height : 0) 640 color: Color.mSurfaceVariant 641 radius: Style.radiusM 642 clip: true 643 644 Flickable { 645 id: calendarFlickable 646 anchors.fill: parent 647 clip: true 648 contentHeight: 24 * (root.hourHeight) 649 boundsBehavior: Flickable.DragOverBounds 650 onHeightChanged: Qt.callLater(root.adjustHourHeightForViewport) 651 652 Component.onCompleted: { 653 calendarFlickable.forceActiveFocus() 654 } 655 656 // Keyboard interaction 657 Keys.onPressed: function(event) { 658 if (event.key === Qt.Key_Up || event.key === Qt.Key_Down) { 659 var step = root.hourHeight 660 var targetY = event.key === Qt.Key_Up ? Math.max(0, contentY - step) : 661 Math.min(Math.max(0, contentHeight - height), contentY + step) 662 scrollAnim.targetY = targetY 663 scrollAnim.start() 664 event.accepted = true 665 } else if (event.key === Qt.Key_Left || event.key === Qt.Key_Right) { 666 if (mainInstance) { 667 mainInstance.navigateWeek(event.key === Qt.Key_Left ? -7 : 7) 668 } 669 event.accepted = true 670 } 671 } 672 673 NumberAnimation { 674 id: scrollAnim 675 target: calendarFlickable; property: "contentY"; duration: 100 676 easing.type: Easing.OutCubic; property real targetY: 0; to: targetY 677 } 678 679 ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } 680 681 Row { 682 width: parent.width 683 height: parent.height 684 685 // Time Column 686 Column { 687 width: root.timeColumnWidth 688 height: parent.height 689 Repeater { 690 model: 23 691 Rectangle { 692 width: root.timeColumnWidth 693 height: root.hourHeight 694 color: "transparent" 695 NText { 696 text: { 697 var hour = index + 1 698 if (mainInstance?.use12hourFormat) { 699 var d = new Date(); d.setHours(hour, 0, 0, 0) 700 return mainInstance.formatTime(d) 701 } 702 return (hour < 10 ? "0" : "") + hour + ':00' 703 } 704 anchors.right: parent.right 705 anchors.rightMargin: Style.marginS 706 anchors.verticalCenter: parent.top 707 anchors.verticalCenterOffset: root.hourHeight 708 font.pointSize: Style.fontSizeXS; color: Color.mOnSurfaceVariant 709 } 710 } 711 } 712 } 713 714 // Hour Rectangles 715 Item { 716 width: 7 * ((mainInstance?.dayColumnWidth) + (root.daySpacing)) 717 height: parent.height 718 719 Row { 720 anchors.fill: parent 721 spacing: root.daySpacing 722 Repeater { 723 model: 7 724 Column { 725 width: mainInstance?.dayColumnWidth 726 height: parent.height 727 Repeater { 728 model: 24 729 Rectangle { width: parent.width; height: 1; color: Color.mSurfaceVariant } 730 } 731 } 732 } 733 } 734 // Hour Lines 735 Repeater { 736 model: 24 737 Rectangle { 738 width: parent.width; height: 1 739 y: index * (root.hourHeight) 740 color: Qt.alpha(mainInstance?.lineColor || Color.mOutline, mainInstance?.hourLineOpacitySetting || 0.5) 741 } 742 } 743 // Day Lines 744 Repeater { 745 model: 6 746 Rectangle { 747 width: 1; height: parent.height 748 x: (index + 1) * ((mainInstance?.dayColumnWidth) + (root.daySpacing)) - ((root.daySpacing) / 2) 749 color: Qt.alpha(mainInstance?.lineColor || Color.mOutline, mainInstance?.dayLineOpacitySetting || 0.9) 750 } 751 } 752 753 // Event positioning 754 Repeater { 755 model: mainInstance?.eventsModel 756 delegate: Item { 757 property var eventData: model 758 property int dayIndex: mainInstance?.getDisplayDayIndexForDate(model.startTime) ?? -1 759 property real startHour: model.startTime.getHours() + model.startTime.getMinutes() / 60 760 property real endHour: model.endTime.getHours() + model.endTime.getMinutes() / 60 761 property real duration: Math.max(0, (model.endTime - model.startTime) / 3600000) 762 763 property real exactHeight: Math.max(1, duration * (root.hourHeight) - 1) 764 property bool isCompact: exactHeight < 40 765 property var overlapInfo: mainInstance?.overlappingEventsData?.[index] ?? { 766 xOffset: 0, width: (mainInstance?.dayColumnWidth) - 8, lane: 0, totalLanes: 1 767 } 768 property real eventWidth: overlapInfo.width - 1 769 property real eventXOffset: overlapInfo.xOffset 770 771 property bool isTodoItem: model.isTodo || false 772 property bool isDeadline: model.isDeadlineMarker || false 773 property color eventColor: isDeadline ? Color.mSecondary : (isTodoItem ? Color.mSecondary : Color.mPrimary) 774 property color eventTextColor: isDeadline ? Color.mOnSecondary : (isTodoItem ? Color.mOnSecondary : Color.mOnPrimary) 775 776 visible: dayIndex >= 0 && dayIndex < 7 && duration > 0 777 width: eventWidth 778 height: isDeadline ? Math.max(8, Math.min(12, exactHeight)) : exactHeight 779 x: dayIndex * ((mainInstance?.dayColumnWidth) + (root.daySpacing)) + eventXOffset 780 y: startHour * (root.hourHeight) 781 z: 100 + overlapInfo.lane 782 783 Rectangle { 784 anchors.fill: parent 785 color: eventColor 786 radius: Style.radiusS 787 opacity: isDeadline ? 0.95 : (isTodoItem && model.todoStatus === "COMPLETED" ? 0.5 : 0.9) 788 clip: true 789 Rectangle { 790 visible: exactHeight < 5 && overlapInfo.lane > 0 791 anchors.fill: parent 792 color: "transparent" 793 radius: parent.radius 794 border.width: 1 795 border.color: eventColor 796 } 797 Loader { 798 anchors.fill: parent 799 anchors.margins: exactHeight < 10 ? 1 : Style.marginS 800 anchors.leftMargin: exactHeight < 10 ? 1 : Style.marginS + 3 801 sourceComponent: isDeadline ? deadlineLayout : (isCompact ? compactLayout : normalLayout) 802 } 803 } 804 805 Component { 806 id: normalLayout 807 Column { 808 spacing: 2 809 width: parent.width - 3 810 NText { 811 visible: exactHeight >= 20 812 text: (isTodoItem ? (model.todoStatus === "COMPLETED" ? "\u2611 " : "\u2610 ") : "") + model.title 813 color: eventTextColor 814 font.pointSize: Style.fontSizeXS; font.weight: Font.Medium 815 font.strikeout: isTodoItem && model.todoStatus === "COMPLETED" 816 elide: Text.ElideRight; width: parent.width 817 } 818 NText { 819 visible: exactHeight >= 30 && !isTodoItem 820 text: mainInstance?.formatTimeRangeForDisplay(model) || "" 821 color: eventTextColor 822 font.pointSize: Style.fontSizeXXS; opacity: 0.9 823 elide: Text.ElideRight; width: parent.width 824 } 825 NText { 826 visible: exactHeight >= 45 && model.location && model.location !== "" 827 text: "\u26B2 " + (model.location || "") 828 color: eventTextColor 829 font.pointSize: Style.fontSizeXXS; opacity: 0.8 830 elide: Text.ElideRight; width: parent.width 831 } 832 } 833 } 834 835 Component { 836 id: compactLayout 837 NText { 838 text: { 839 var prefix = isTodoItem ? (model.todoStatus === "COMPLETED" ? "\u2611 " : "\u2610 ") : "" 840 if (exactHeight < 15) return prefix + model.title 841 if (isTodoItem) return prefix + model.title 842 return model.title + " \u2022 " + (mainInstance?.formatTimeRangeForDisplay(model) || "") 843 } 844 color: eventTextColor 845 font.pointSize: exactHeight < 15 ? Style.fontSizeXXS : Style.fontSizeXS 846 font.weight: Font.Medium 847 font.strikeout: isTodoItem && model.todoStatus === "COMPLETED" 848 elide: Text.ElideRight; verticalAlignment: Text.AlignVCenter 849 width: parent.width - 3 850 } 851 } 852 853 Component { 854 id: deadlineLayout 855 Rectangle { 856 anchors.fill: parent 857 color: eventColor 858 radius: parent.radius 859 opacity: 0.95 860 } 861 } 862 863 MouseArea { 864 anchors.fill: parent 865 hoverEnabled: true 866 cursorShape: Qt.PointingHandCursor 867 onEntered: { 868 var tip = mainInstance?.getEventTooltip(model) || "" 869 TooltipService.show(parent, tip, "auto", Style.tooltipDelay, Settings.data.ui.fontFixed) 870 } 871 onClicked: { 872 if (isTodoItem) { 873 if (model.todoStatus === "COMPLETED") 874 mainInstance?.uncompleteTodo(model.calendarUid, model.todoUid) 875 else 876 mainInstance?.completeTodo(model.calendarUid, model.todoUid) 877 } else { 878 mainInstance?.handleEventClick(eventData) 879 } 880 } 881 onExited: TooltipService.hide() 882 } 883 } 884 } 885 886 // Time Indicator 887 Rectangle { 888 property var now: new Date() 889 property date today: new Date(now.getFullYear(), now.getMonth(), now.getDate()) 890 property date weekStartDate: mainInstance?.weekStart ?? new Date() 891 property date weekEndDate: mainInstance ? 892 new Date(mainInstance.weekStart.getFullYear(), mainInstance.weekStart.getMonth(), mainInstance.weekStart.getDate() + 7) : new Date() 893 property bool inCurrentWeek: today >= weekStartDate && today < weekEndDate 894 property int currentDay: mainInstance?.getDayIndexForDate(now) ?? -1 895 property real currentHour: now.getHours() + now.getMinutes() / 60 896 897 visible: inCurrentWeek && currentDay >= 0 898 width: mainInstance?.dayColumnWidth 899 height: 2 900 x: currentDay * ((mainInstance?.dayColumnWidth) + (root.daySpacing)) 901 y: currentHour * (root.hourHeight) 902 color: Color.mError 903 radius: 1 904 z: 1000 905 Rectangle { 906 width: 8; height: 8; radius: 4; color: Color.mError 907 anchors.verticalCenter: parent.verticalCenter; x: -4 908 } 909 Timer { 910 interval: 60000; running: true; repeat: true 911 onTriggered: parent.now = new Date() 912 } 913 } 914 } 915 } 916 } 917 } 918 } 919 } 920 921 } 922 } 923}