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