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 property bool showEventDetailDialog: false 26 property bool eventDetailEditMode: false 27 property bool showDeleteConfirmation: false 28 property bool showTodoDetailDialog: false 29 property bool todoDetailEditMode: false 30 property bool showTodoDeleteConfirmation: false 31 32 property real defaultHourHeight: 50 * Style.uiScaleRatio 33 property real minHourHeight: 32 * Style.uiScaleRatio 34 property real hourHeight: defaultHourHeight 35 property real timeColumnWidth: 65 * Style.uiScaleRatio 36 property real daySpacing: 1 * Style.uiScaleRatio 37 38 // Panel doesn't need its own CalendarService connection - Main.qml handles it. 39 // When panel opens, trigger a fresh load if needed. 40 Component.onCompleted: { 41 mainInstance?.initializePlugin() 42 Qt.callLater(root.adjustHourHeightForViewport) 43 } 44 onVisibleChanged: if (visible && mainInstance) { 45 mainInstance.refreshView() 46 mainInstance.goToToday() 47 Qt.callLater(root.scrollToCurrentTime) 48 mainInstance.loadTodos() 49 Qt.callLater(root.adjustHourHeightForViewport) 50 } 51 52 function adjustHourHeightForViewport() { 53 if (!calendarFlickable || calendarFlickable.height <= 0) return 54 // Target showing 08:30–24:00 (~15.5 hours) without scroll; fall back to min height if space is tight. 55 var target = calendarFlickable.height / 15.5 56 var newHeight = Math.max(minHourHeight, Math.min(defaultHourHeight, target)) 57 if (Math.abs(newHeight - hourHeight) > 0.5) hourHeight = newHeight 58 } 59 60 // Scroll to time indicator position 61 function scrollToCurrentTime() { 62 if (!mainInstance || !calendarFlickable) return 63 var now = new Date(), today = new Date(now.getFullYear(), now.getMonth(), now.getDate()) 64 var weekStart = new Date(mainInstance.weekStart) 65 var weekEnd = new Date(weekStart.getFullYear(), weekStart.getMonth(), weekStart.getDate() + 7) 66 67 if (today >= weekStart && today < weekEnd) { 68 var currentHour = now.getHours() + now.getMinutes() / 60 69 var scrollPos = (currentHour * hourHeight) - (calendarFlickable.height / 2) 70 var maxScroll = Math.max(0, (24 * hourHeight) - calendarFlickable.height) 71 scrollAnim.targetY = Math.max(0, Math.min(scrollPos, maxScroll)) 72 scrollAnim.start() 73 } 74 } 75 76 // Event creation dialog 77 Rectangle { 78 id: createEventOverlay 79 anchors.fill: parent 80 color: Qt.rgba(0, 0, 0, 0.5) 81 visible: showCreateDialog 82 z: 2000 83 84 MouseArea { anchors.fill: parent; onClicked: showCreateDialog = false } 85 86 Rectangle { 87 anchors.centerIn: parent 88 width: 400 * Style.uiScaleRatio 89 height: createDialogColumn.implicitHeight + 2 * Style.marginM 90 color: Color.mSurface 91 radius: Style.radiusM 92 93 MouseArea { anchors.fill: parent } // block clicks through 94 95 ColumnLayout { 96 id: createDialogColumn 97 anchors.fill: parent 98 anchors.margins: Style.marginM 99 spacing: Style.marginS 100 101 NText { 102 text: pluginApi.tr("panel.add_event") 103 font.pointSize: Style.fontSizeL; font.weight: Font.Bold 104 color: Color.mOnSurface 105 } 106 107 NText { text: pluginApi.tr("panel.summary"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS } 108 TextField { 109 id: createEventSummary 110 Layout.fillWidth: true 111 placeholderText: pluginApi.tr("panel.summary") 112 color: Color.mOnSurface 113 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS } 114 } 115 116 NText { text: pluginApi.tr("panel.date"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS } 117 TextField { 118 id: createEventDate 119 Layout.fillWidth: true 120 placeholderText: "YYYY-MM-DD" 121 color: Color.mOnSurface 122 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS } 123 } 124 125 RowLayout { 126 spacing: Style.marginS 127 ColumnLayout { 128 Layout.fillWidth: true 129 NText { text: pluginApi.tr("panel.start_time"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS } 130 TextField { 131 id: createEventStartTime 132 Layout.fillWidth: true 133 placeholderText: "HH:MM" 134 color: Color.mOnSurface 135 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS } 136 } 137 } 138 ColumnLayout { 139 Layout.fillWidth: true 140 NText { text: pluginApi.tr("panel.end_time"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS } 141 TextField { 142 id: createEventEndTime 143 Layout.fillWidth: true 144 placeholderText: "HH:MM" 145 color: Color.mOnSurface 146 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS } 147 } 148 } 149 } 150 151 NText { text: pluginApi.tr("panel.location"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS } 152 TextField { 153 id: createEventLocation 154 Layout.fillWidth: true 155 placeholderText: pluginApi.tr("panel.location") 156 color: Color.mOnSurface 157 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS } 158 } 159 160 NText { text: pluginApi.tr("panel.description"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS } 161 TextField { 162 id: createEventDescription 163 Layout.fillWidth: true 164 placeholderText: pluginApi.tr("panel.description") 165 color: Color.mOnSurface 166 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS } 167 } 168 169 NText { text: pluginApi.tr("panel.calendar_select"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS } 170 ComboBox { 171 id: calendarSelector 172 Layout.fillWidth: true 173 model: CalendarService.calendars || [] 174 textRole: "name" 175 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS } 176 } 177 178 RowLayout { 179 Layout.fillWidth: true 180 spacing: Style.marginS 181 182 Item { Layout.fillWidth: true } 183 184 Rectangle { 185 Layout.preferredWidth: cancelBtn.implicitWidth + 2 * Style.marginM 186 Layout.preferredHeight: cancelBtn.implicitHeight + Style.marginS 187 color: Color.mSurfaceVariant; radius: Style.radiusS 188 NText { 189 id: cancelBtn; anchors.centerIn: parent 190 text: pluginApi.tr("panel.cancel"); color: Color.mOnSurfaceVariant 191 } 192 MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor; onClicked: showCreateDialog = false } 193 } 194 195 Rectangle { 196 Layout.preferredWidth: createBtn.implicitWidth + 2 * Style.marginM 197 Layout.preferredHeight: createBtn.implicitHeight + Style.marginS 198 color: Color.mPrimary; radius: Style.radiusS 199 opacity: createEventSummary.text.trim() !== "" ? 1.0 : 0.5 200 NText { 201 id: createBtn; anchors.centerIn: parent 202 text: pluginApi.tr("panel.create"); color: Color.mOnPrimary; font.weight: Font.Bold 203 } 204 MouseArea { 205 anchors.fill: parent; cursorShape: Qt.PointingHandCursor 206 onClicked: { 207 if (createEventSummary.text.trim() === "") return 208 var cal = CalendarService.calendars?.[calendarSelector.currentIndex] 209 var calUid = cal?.uid || "" 210 var dateParts = createEventDate.text.split("-") 211 var startParts = createEventStartTime.text.split(":") 212 var endParts = createEventEndTime.text.split(":") 213 var startDate = new Date(parseInt(dateParts[0]), parseInt(dateParts[1])-1, parseInt(dateParts[2]), 214 parseInt(startParts[0]), parseInt(startParts[1]), 0) 215 var endDate = new Date(parseInt(dateParts[0]), parseInt(dateParts[1])-1, parseInt(dateParts[2]), 216 parseInt(endParts[0]), parseInt(endParts[1]), 0) 217 mainInstance?.createEvent(calUid, createEventSummary.text.trim(), 218 Math.floor(startDate.getTime()/1000), Math.floor(endDate.getTime()/1000), 219 createEventLocation.text.trim(), createEventDescription.text.trim()) 220 showCreateDialog = false 221 } 222 } 223 } 224 } 225 } 226 } 227 } 228 229 // Task creation dialog 230 Rectangle { 231 id: createTaskOverlay 232 anchors.fill: parent 233 color: Qt.rgba(0, 0, 0, 0.5) 234 visible: showCreateTaskDialog 235 z: 2000 236 237 MouseArea { anchors.fill: parent; onClicked: showCreateTaskDialog = false } 238 239 Rectangle { 240 anchors.centerIn: parent 241 width: 400 * Style.uiScaleRatio 242 height: createTaskDialogColumn.implicitHeight + 2 * Style.marginM 243 color: Color.mSurface 244 radius: Style.radiusM 245 246 MouseArea { anchors.fill: parent } 247 248 ColumnLayout { 249 id: createTaskDialogColumn 250 anchors.fill: parent 251 anchors.margins: Style.marginM 252 spacing: Style.marginS 253 254 NText { 255 text: pluginApi.tr("panel.add_task") 256 font.pointSize: Style.fontSizeL; font.weight: Font.Bold 257 color: Color.mOnSurface 258 } 259 260 NText { text: pluginApi.tr("panel.task_summary"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS } 261 TextField { 262 id: createTaskSummary 263 Layout.fillWidth: true 264 placeholderText: pluginApi.tr("panel.task_summary") 265 color: Color.mOnSurface 266 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS } 267 } 268 269 NText { text: pluginApi.tr("panel.due_date"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS } 270 TextField { 271 id: createTaskDueDate 272 Layout.fillWidth: true 273 placeholderText: "YYYY-MM-DD" 274 color: Color.mOnSurface 275 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS } 276 } 277 278 // Use end_time label to reflect deadline semantics 279 NText { text: pluginApi.tr("panel.end_time"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS } 280 TextField { 281 id: createTaskDueTime 282 Layout.fillWidth: true 283 placeholderText: "HH:MM" 284 color: Color.mOnSurface 285 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS } 286 } 287 288 NText { text: pluginApi.tr("panel.description"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS } 289 TextField { 290 id: createTaskDescription 291 Layout.fillWidth: true 292 placeholderText: pluginApi.tr("panel.description") 293 color: Color.mOnSurface 294 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS } 295 } 296 297 NText { text: pluginApi.tr("panel.priority"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS } 298 property int selectedPriority: 0 299 RowLayout { 300 spacing: Style.marginS 301 Repeater { 302 model: [ 303 { label: pluginApi.tr("panel.priority_high"), value: 1 }, 304 { label: pluginApi.tr("panel.priority_medium"), value: 5 }, 305 { label: pluginApi.tr("panel.priority_low"), value: 9 } 306 ] 307 Rectangle { 308 Layout.preferredWidth: priLabel.implicitWidth + 2 * Style.marginM 309 Layout.preferredHeight: priLabel.implicitHeight + Style.marginS 310 color: createTaskDialogColumn.selectedPriority === modelData.value ? Color.mPrimary : Color.mSurfaceVariant 311 radius: Style.radiusS 312 NText { 313 id: priLabel; anchors.centerIn: parent 314 text: modelData.label 315 color: createTaskDialogColumn.selectedPriority === modelData.value ? Color.mOnPrimary : Color.mOnSurfaceVariant 316 font.weight: Font.Medium 317 } 318 MouseArea { 319 anchors.fill: parent; cursorShape: Qt.PointingHandCursor 320 onClicked: createTaskDialogColumn.selectedPriority = 321 createTaskDialogColumn.selectedPriority === modelData.value ? 0 : modelData.value 322 } 323 } 324 } 325 } 326 327 NText { text: pluginApi.tr("panel.task_list_select"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS } 328 ComboBox { 329 id: taskListSelector 330 Layout.fillWidth: true 331 model: mainInstance?.taskLists || [] 332 textRole: "name" 333 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS } 334 } 335 336 RowLayout { 337 Layout.fillWidth: true 338 spacing: Style.marginS 339 340 Item { Layout.fillWidth: true } 341 342 Rectangle { 343 Layout.preferredWidth: taskCancelBtn.implicitWidth + 2 * Style.marginM 344 Layout.preferredHeight: taskCancelBtn.implicitHeight + Style.marginS 345 color: Color.mSurfaceVariant; radius: Style.radiusS 346 NText { 347 id: taskCancelBtn; anchors.centerIn: parent 348 text: pluginApi.tr("panel.cancel"); color: Color.mOnSurfaceVariant 349 } 350 MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor; onClicked: showCreateTaskDialog = false } 351 } 352 353 Rectangle { 354 Layout.preferredWidth: taskCreateBtn.implicitWidth + 2 * Style.marginM 355 Layout.preferredHeight: taskCreateBtn.implicitHeight + Style.marginS 356 color: Color.mPrimary; radius: Style.radiusS 357 opacity: createTaskSummary.text.trim() !== "" ? 1.0 : 0.5 358 NText { 359 id: taskCreateBtn; anchors.centerIn: parent 360 text: pluginApi.tr("panel.create"); color: Color.mOnPrimary; font.weight: Font.Bold 361 } 362 MouseArea { 363 anchors.fill: parent; cursorShape: Qt.PointingHandCursor 364 onClicked: { 365 if (createTaskSummary.text.trim() === "") return 366 var tl = mainInstance?.taskLists?.[taskListSelector.currentIndex] 367 var tlUid = tl?.uid || "" 368 var dueTs = 0 369 if (createTaskDueDate.text.trim() !== "") { 370 var dateParts = createTaskDueDate.text.split("-") 371 var timeParts = createTaskDueTime.text.split(":") 372 var h = createTaskDueTime.text.trim() === "" ? 0 : parseInt(timeParts[0]) 373 var m = createTaskDueTime.text.trim() === "" ? 0 : parseInt(timeParts[1] || "0") 374 var d = new Date(parseInt(dateParts[0]), parseInt(dateParts[1]) - 1, parseInt(dateParts[2]), h, m, 0) 375 if (!isNaN(d.getTime())) dueTs = Math.floor(d.getTime() / 1000) 376 } 377 mainInstance?.createTodo(tlUid, createTaskSummary.text.trim(), 378 dueTs, createTaskDialogColumn.selectedPriority, 379 createTaskDescription.text.trim()) 380 showCreateTaskDialog = false 381 } 382 } 383 } 384 } 385 } 386 } 387 } 388 389 // Event detail/edit popup 390 Rectangle { 391 id: eventDetailOverlay 392 anchors.fill: parent 393 color: Qt.rgba(0, 0, 0, 0.5) 394 visible: showEventDetailDialog 395 z: 2000 396 397 MouseArea { anchors.fill: parent; onClicked: { showEventDetailDialog = false; eventDetailEditMode = false; showDeleteConfirmation = false } } 398 399 Rectangle { 400 anchors.centerIn: parent 401 width: 420 * Style.uiScaleRatio 402 height: eventDetailColumn.implicitHeight + 2 * Style.marginM 403 color: Color.mSurface 404 radius: Style.radiusM 405 406 MouseArea { anchors.fill: parent } 407 408 ColumnLayout { 409 id: eventDetailColumn 410 anchors.fill: parent 411 anchors.margins: Style.marginM 412 spacing: Style.marginS 413 414 property var evt: mainInstance?.selectedEvent || {} 415 416 // View mode 417 ColumnLayout { 418 visible: !eventDetailEditMode && !showDeleteConfirmation 419 spacing: Style.marginS 420 Layout.fillWidth: true 421 422 NText { 423 text: eventDetailColumn.evt.title || "" 424 font.pointSize: Style.fontSizeL; font.weight: Font.Bold 425 color: Color.mOnSurface 426 wrapMode: Text.Wrap; Layout.fillWidth: true 427 } 428 429 RowLayout { 430 spacing: Style.marginS 431 NIcon { icon: "clock"; pointSize: Style.fontSizeS; color: Color.mOnSurfaceVariant } 432 NText { 433 text: { 434 var e = eventDetailColumn.evt 435 if (!e.startTime) return "" 436 return mainInstance?.formatDateTime(e.startTime) + " - " + mainInstance?.formatDateTime(e.endTime) 437 } 438 font.pointSize: Style.fontSizeS; color: Color.mOnSurfaceVariant 439 wrapMode: Text.Wrap; Layout.fillWidth: true 440 } 441 } 442 443 RowLayout { 444 visible: (eventDetailColumn.evt.location || "") !== "" 445 spacing: Style.marginS 446 NIcon { icon: "map-pin"; pointSize: Style.fontSizeS; color: Color.mOnSurfaceVariant } 447 NText { 448 text: eventDetailColumn.evt.location || "" 449 font.pointSize: Style.fontSizeS; color: Color.mOnSurfaceVariant 450 wrapMode: Text.Wrap; Layout.fillWidth: true 451 } 452 } 453 454 NText { 455 visible: (eventDetailColumn.evt.description || "") !== "" 456 text: eventDetailColumn.evt.description || "" 457 font.pointSize: Style.fontSizeS; color: Color.mOnSurfaceVariant 458 wrapMode: Text.Wrap; Layout.fillWidth: true 459 } 460 461 RowLayout { 462 Layout.fillWidth: true 463 spacing: Style.marginS 464 465 Item { Layout.fillWidth: true } 466 467 Rectangle { 468 Layout.preferredWidth: editEventBtn.implicitWidth + 2 * Style.marginM 469 Layout.preferredHeight: editEventBtn.implicitHeight + Style.marginS 470 color: Color.mPrimary; radius: Style.radiusS 471 visible: (eventDetailColumn.evt.eventUid || "") !== "" 472 NText { 473 id: editEventBtn; anchors.centerIn: parent 474 text: pluginApi.tr("panel.edit") || "Edit" 475 color: Color.mOnPrimary; font.weight: Font.Bold 476 } 477 MouseArea { 478 anchors.fill: parent; cursorShape: Qt.PointingHandCursor 479 onClicked: { 480 var e = eventDetailColumn.evt 481 editEventSummary.text = e.title || "" 482 editEventLocation.text = e.location || "" 483 editEventDescription.text = e.description || "" 484 if (e.startTime) { 485 var s = new Date(e.startTime) 486 editEventDate.text = s.getFullYear() + "-" + String(s.getMonth()+1).padStart(2,'0') + "-" + String(s.getDate()).padStart(2,'0') 487 editEventStartTime.text = String(s.getHours()).padStart(2,'0') + ":" + String(s.getMinutes()).padStart(2,'0') 488 } 489 if (e.endTime) { 490 var en = new Date(e.endTime) 491 editEventEndTime.text = String(en.getHours()).padStart(2,'0') + ":" + String(en.getMinutes()).padStart(2,'0') 492 } 493 eventDetailEditMode = true 494 } 495 } 496 } 497 498 Rectangle { 499 Layout.preferredWidth: deleteEventBtn.implicitWidth + 2 * Style.marginM 500 Layout.preferredHeight: deleteEventBtn.implicitHeight + Style.marginS 501 color: Color.mError; radius: Style.radiusS 502 visible: (eventDetailColumn.evt.eventUid || "") !== "" 503 NText { 504 id: deleteEventBtn; anchors.centerIn: parent 505 text: pluginApi.tr("panel.delete") || "Delete" 506 color: Color.mOnError; font.weight: Font.Bold 507 } 508 MouseArea { 509 anchors.fill: parent; cursorShape: Qt.PointingHandCursor 510 onClicked: showDeleteConfirmation = true 511 } 512 } 513 514 Rectangle { 515 Layout.preferredWidth: closeEventBtn.implicitWidth + 2 * Style.marginM 516 Layout.preferredHeight: closeEventBtn.implicitHeight + Style.marginS 517 color: Color.mSurfaceVariant; radius: Style.radiusS 518 NText { 519 id: closeEventBtn; anchors.centerIn: parent 520 text: pluginApi.tr("panel.close") || "Close" 521 color: Color.mOnSurfaceVariant 522 } 523 MouseArea { 524 anchors.fill: parent; cursorShape: Qt.PointingHandCursor 525 onClicked: { showEventDetailDialog = false; eventDetailEditMode = false } 526 } 527 } 528 } 529 } 530 531 // Delete confirmation mode 532 ColumnLayout { 533 visible: showDeleteConfirmation 534 spacing: Style.marginS 535 Layout.fillWidth: true 536 537 NText { 538 text: (pluginApi.tr("panel.delete_confirm") || "Delete this event?") 539 font.pointSize: Style.fontSizeM; font.weight: Font.Bold 540 color: Color.mOnSurface 541 } 542 NText { 543 text: eventDetailColumn.evt.title || "" 544 font.pointSize: Style.fontSizeS; color: Color.mOnSurfaceVariant 545 } 546 547 RowLayout { 548 Layout.fillWidth: true 549 spacing: Style.marginS 550 Item { Layout.fillWidth: true } 551 552 Rectangle { 553 Layout.preferredWidth: confirmDeleteBtn.implicitWidth + 2 * Style.marginM 554 Layout.preferredHeight: confirmDeleteBtn.implicitHeight + Style.marginS 555 color: Color.mError; radius: Style.radiusS 556 NText { 557 id: confirmDeleteBtn; anchors.centerIn: parent 558 text: pluginApi.tr("panel.delete") || "Delete" 559 color: Color.mOnError; font.weight: Font.Bold 560 } 561 MouseArea { 562 anchors.fill: parent; cursorShape: Qt.PointingHandCursor 563 onClicked: { 564 var e = eventDetailColumn.evt 565 mainInstance?.deleteEvent(e.calendarUid, e.eventUid) 566 showEventDetailDialog = false 567 showDeleteConfirmation = false 568 eventDetailEditMode = false 569 } 570 } 571 } 572 573 Rectangle { 574 Layout.preferredWidth: cancelDeleteBtn.implicitWidth + 2 * Style.marginM 575 Layout.preferredHeight: cancelDeleteBtn.implicitHeight + Style.marginS 576 color: Color.mSurfaceVariant; radius: Style.radiusS 577 NText { 578 id: cancelDeleteBtn; anchors.centerIn: parent 579 text: pluginApi.tr("panel.cancel"); color: Color.mOnSurfaceVariant 580 } 581 MouseArea { 582 anchors.fill: parent; cursorShape: Qt.PointingHandCursor 583 onClicked: showDeleteConfirmation = false 584 } 585 } 586 } 587 } 588 589 // Edit mode 590 ColumnLayout { 591 visible: eventDetailEditMode && !showDeleteConfirmation 592 spacing: Style.marginS 593 Layout.fillWidth: true 594 595 NText { 596 text: pluginApi.tr("panel.edit_event") || "Edit Event" 597 font.pointSize: Style.fontSizeL; font.weight: Font.Bold 598 color: Color.mOnSurface 599 } 600 601 NText { text: pluginApi.tr("panel.summary"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS } 602 TextField { 603 id: editEventSummary 604 Layout.fillWidth: true 605 color: Color.mOnSurface 606 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS } 607 } 608 609 NText { text: pluginApi.tr("panel.date"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS } 610 TextField { 611 id: editEventDate 612 Layout.fillWidth: true 613 placeholderText: "YYYY-MM-DD" 614 color: Color.mOnSurface 615 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS } 616 } 617 618 RowLayout { 619 spacing: Style.marginS 620 ColumnLayout { 621 Layout.fillWidth: true 622 NText { text: pluginApi.tr("panel.start_time"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS } 623 TextField { 624 id: editEventStartTime 625 Layout.fillWidth: true 626 placeholderText: "HH:MM" 627 color: Color.mOnSurface 628 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS } 629 } 630 } 631 ColumnLayout { 632 Layout.fillWidth: true 633 NText { text: pluginApi.tr("panel.end_time"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS } 634 TextField { 635 id: editEventEndTime 636 Layout.fillWidth: true 637 placeholderText: "HH:MM" 638 color: Color.mOnSurface 639 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS } 640 } 641 } 642 } 643 644 NText { text: pluginApi.tr("panel.location"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS } 645 TextField { 646 id: editEventLocation 647 Layout.fillWidth: true 648 color: Color.mOnSurface 649 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS } 650 } 651 652 NText { text: pluginApi.tr("panel.description"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS } 653 TextField { 654 id: editEventDescription 655 Layout.fillWidth: true 656 color: Color.mOnSurface 657 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS } 658 } 659 660 RowLayout { 661 Layout.fillWidth: true 662 spacing: Style.marginS 663 Item { Layout.fillWidth: true } 664 665 Rectangle { 666 Layout.preferredWidth: saveEventBtn.implicitWidth + 2 * Style.marginM 667 Layout.preferredHeight: saveEventBtn.implicitHeight + Style.marginS 668 color: Color.mPrimary; radius: Style.radiusS 669 NText { 670 id: saveEventBtn; anchors.centerIn: parent 671 text: pluginApi.tr("panel.save") || "Save" 672 color: Color.mOnPrimary; font.weight: Font.Bold 673 } 674 MouseArea { 675 anchors.fill: parent; cursorShape: Qt.PointingHandCursor 676 onClicked: { 677 var e = eventDetailColumn.evt 678 var dateParts = editEventDate.text.split("-") 679 var startParts = editEventStartTime.text.split(":") 680 var endParts = editEventEndTime.text.split(":") 681 var startDate = new Date(parseInt(dateParts[0]), parseInt(dateParts[1])-1, parseInt(dateParts[2]), 682 parseInt(startParts[0]), parseInt(startParts[1]), 0) 683 var endDate = new Date(parseInt(dateParts[0]), parseInt(dateParts[1])-1, parseInt(dateParts[2]), 684 parseInt(endParts[0]), parseInt(endParts[1]), 0) 685 mainInstance?.updateEvent( 686 e.calendarUid, e.eventUid, 687 editEventSummary.text.trim(), 688 editEventLocation.text.trim(), 689 editEventDescription.text.trim(), 690 Math.floor(startDate.getTime()/1000), 691 Math.floor(endDate.getTime()/1000)) 692 showEventDetailDialog = false 693 eventDetailEditMode = false 694 } 695 } 696 } 697 698 Rectangle { 699 Layout.preferredWidth: editCancelBtn.implicitWidth + 2 * Style.marginM 700 Layout.preferredHeight: editCancelBtn.implicitHeight + Style.marginS 701 color: Color.mSurfaceVariant; radius: Style.radiusS 702 NText { 703 id: editCancelBtn; anchors.centerIn: parent 704 text: pluginApi.tr("panel.cancel"); color: Color.mOnSurfaceVariant 705 } 706 MouseArea { 707 anchors.fill: parent; cursorShape: Qt.PointingHandCursor 708 onClicked: eventDetailEditMode = false 709 } 710 } 711 } 712 } 713 } 714 } 715 } 716 717 // Todo detail/edit popup 718 Rectangle { 719 id: todoDetailOverlay 720 anchors.fill: parent 721 color: Qt.rgba(0, 0, 0, 0.5) 722 visible: showTodoDetailDialog 723 z: 2000 724 725 MouseArea { anchors.fill: parent; onClicked: { showTodoDetailDialog = false; todoDetailEditMode = false; showTodoDeleteConfirmation = false } } 726 727 Rectangle { 728 anchors.centerIn: parent 729 width: 420 * Style.uiScaleRatio 730 height: todoDetailColumn.implicitHeight + 2 * Style.marginM 731 color: Color.mSurface 732 radius: Style.radiusM 733 734 MouseArea { anchors.fill: parent } 735 736 ColumnLayout { 737 id: todoDetailColumn 738 anchors.fill: parent 739 anchors.margins: Style.marginM 740 spacing: Style.marginS 741 742 property var todo: mainInstance?.selectedTodo || {} 743 744 // View mode 745 ColumnLayout { 746 visible: !todoDetailEditMode && !showTodoDeleteConfirmation 747 spacing: Style.marginS 748 Layout.fillWidth: true 749 750 NText { 751 text: (todoDetailColumn.todo.status === "COMPLETED" ? "\u2611 " : "\u2610 ") + (todoDetailColumn.todo.summary || "") 752 font.pointSize: Style.fontSizeL; font.weight: Font.Bold 753 color: Color.mOnSurface 754 wrapMode: Text.Wrap; Layout.fillWidth: true 755 } 756 757 RowLayout { 758 visible: todoDetailColumn.todo.due != null 759 spacing: Style.marginS 760 NIcon { icon: "clock"; pointSize: Style.fontSizeS; color: Color.mOnSurfaceVariant } 761 NText { 762 text: todoDetailColumn.todo.due ? mainInstance?.formatDateTime(new Date(todoDetailColumn.todo.due)) || "" : "" 763 font.pointSize: Style.fontSizeS; color: Color.mOnSurfaceVariant 764 } 765 } 766 767 RowLayout { 768 visible: (todoDetailColumn.todo.priority || 0) > 0 769 spacing: Style.marginS 770 NIcon { icon: "flag"; pointSize: Style.fontSizeS; color: Color.mOnSurfaceVariant } 771 NText { 772 text: { 773 var p = todoDetailColumn.todo.priority || 0 774 return p <= 4 ? (pluginApi.tr("panel.priority") + ": " + pluginApi.tr("panel.priority_high")) : 775 p <= 6 ? (pluginApi.tr("panel.priority") + ": " + pluginApi.tr("panel.priority_medium")) : 776 (pluginApi.tr("panel.priority") + ": " + pluginApi.tr("panel.priority_low")) 777 } 778 font.pointSize: Style.fontSizeS; color: Color.mOnSurfaceVariant 779 } 780 } 781 782 NText { 783 visible: (todoDetailColumn.todo.description || "") !== "" 784 text: todoDetailColumn.todo.description || "" 785 font.pointSize: Style.fontSizeS; color: Color.mOnSurfaceVariant 786 wrapMode: Text.Wrap; Layout.fillWidth: true 787 } 788 789 RowLayout { 790 Layout.fillWidth: true 791 spacing: Style.marginS 792 793 // Toggle complete button 794 Rectangle { 795 Layout.preferredWidth: toggleTodoBtn.implicitWidth + 2 * Style.marginM 796 Layout.preferredHeight: toggleTodoBtn.implicitHeight + Style.marginS 797 color: Color.mSecondary; radius: Style.radiusS 798 NText { 799 id: toggleTodoBtn; anchors.centerIn: parent 800 text: todoDetailColumn.todo.status === "COMPLETED" ? "\u2610" : "\u2611" 801 color: Color.mOnSecondary; font.weight: Font.Bold 802 } 803 MouseArea { 804 anchors.fill: parent; cursorShape: Qt.PointingHandCursor 805 onClicked: { 806 var t = todoDetailColumn.todo 807 if (t.status === "COMPLETED") 808 mainInstance?.uncompleteTodo(t.calendarUid, t.todoUid) 809 else 810 mainInstance?.completeTodo(t.calendarUid, t.todoUid) 811 showTodoDetailDialog = false 812 } 813 } 814 } 815 816 Item { Layout.fillWidth: true } 817 818 Rectangle { 819 Layout.preferredWidth: editTodoBtn.implicitWidth + 2 * Style.marginM 820 Layout.preferredHeight: editTodoBtn.implicitHeight + Style.marginS 821 color: Color.mPrimary; radius: Style.radiusS 822 NText { 823 id: editTodoBtn; anchors.centerIn: parent 824 text: pluginApi.tr("panel.edit"); color: Color.mOnPrimary; font.weight: Font.Bold 825 } 826 MouseArea { 827 anchors.fill: parent; cursorShape: Qt.PointingHandCursor 828 onClicked: { 829 var t = todoDetailColumn.todo 830 editTodoSummary.text = t.summary || "" 831 editTodoDescription.text = t.description || "" 832 if (t.due) { 833 var d = new Date(t.due) 834 editTodoDueDate.text = d.getFullYear() + "-" + String(d.getMonth()+1).padStart(2,'0') + "-" + String(d.getDate()).padStart(2,'0') 835 editTodoDueTime.text = String(d.getHours()).padStart(2,'0') + ":" + String(d.getMinutes()).padStart(2,'0') 836 } else { 837 editTodoDueDate.text = "" 838 editTodoDueTime.text = "" 839 } 840 editTodoPriority = t.priority || 0 841 todoDetailEditMode = true 842 } 843 } 844 } 845 846 Rectangle { 847 Layout.preferredWidth: deleteTodoBtn.implicitWidth + 2 * Style.marginM 848 Layout.preferredHeight: deleteTodoBtn.implicitHeight + Style.marginS 849 color: Color.mError; radius: Style.radiusS 850 NText { 851 id: deleteTodoBtn; anchors.centerIn: parent 852 text: pluginApi.tr("panel.delete"); color: Color.mOnError; font.weight: Font.Bold 853 } 854 MouseArea { 855 anchors.fill: parent; cursorShape: Qt.PointingHandCursor 856 onClicked: showTodoDeleteConfirmation = true 857 } 858 } 859 860 Rectangle { 861 Layout.preferredWidth: closeTodoBtn.implicitWidth + 2 * Style.marginM 862 Layout.preferredHeight: closeTodoBtn.implicitHeight + Style.marginS 863 color: Color.mSurfaceVariant; radius: Style.radiusS 864 NText { 865 id: closeTodoBtn; anchors.centerIn: parent 866 text: pluginApi.tr("panel.close"); color: Color.mOnSurfaceVariant 867 } 868 MouseArea { 869 anchors.fill: parent; cursorShape: Qt.PointingHandCursor 870 onClicked: { showTodoDetailDialog = false; todoDetailEditMode = false } 871 } 872 } 873 } 874 } 875 876 // Delete confirmation 877 ColumnLayout { 878 visible: showTodoDeleteConfirmation 879 spacing: Style.marginS 880 Layout.fillWidth: true 881 882 NText { 883 text: pluginApi.tr("panel.delete_task_confirm") 884 font.pointSize: Style.fontSizeM; font.weight: Font.Bold; color: Color.mOnSurface 885 } 886 NText { text: todoDetailColumn.todo.summary || ""; font.pointSize: Style.fontSizeS; color: Color.mOnSurfaceVariant } 887 888 RowLayout { 889 Layout.fillWidth: true; spacing: Style.marginS 890 Item { Layout.fillWidth: true } 891 Rectangle { 892 Layout.preferredWidth: confirmDeleteTodoBtn.implicitWidth + 2 * Style.marginM 893 Layout.preferredHeight: confirmDeleteTodoBtn.implicitHeight + Style.marginS 894 color: Color.mError; radius: Style.radiusS 895 NText { id: confirmDeleteTodoBtn; anchors.centerIn: parent; text: pluginApi.tr("panel.delete"); color: Color.mOnError; font.weight: Font.Bold } 896 MouseArea { 897 anchors.fill: parent; cursorShape: Qt.PointingHandCursor 898 onClicked: { 899 var t = todoDetailColumn.todo 900 mainInstance?.deleteTodo(t.calendarUid, t.todoUid) 901 showTodoDetailDialog = false; showTodoDeleteConfirmation = false; todoDetailEditMode = false 902 } 903 } 904 } 905 Rectangle { 906 Layout.preferredWidth: cancelDeleteTodoBtn.implicitWidth + 2 * Style.marginM 907 Layout.preferredHeight: cancelDeleteTodoBtn.implicitHeight + Style.marginS 908 color: Color.mSurfaceVariant; radius: Style.radiusS 909 NText { id: cancelDeleteTodoBtn; anchors.centerIn: parent; text: pluginApi.tr("panel.cancel"); color: Color.mOnSurfaceVariant } 910 MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor; onClicked: showTodoDeleteConfirmation = false } 911 } 912 } 913 } 914 915 // Edit mode 916 property int editTodoPriority: 0 917 ColumnLayout { 918 visible: todoDetailEditMode && !showTodoDeleteConfirmation 919 spacing: Style.marginS 920 Layout.fillWidth: true 921 922 NText { text: pluginApi.tr("panel.edit_task"); font.pointSize: Style.fontSizeL; font.weight: Font.Bold; color: Color.mOnSurface } 923 924 NText { text: pluginApi.tr("panel.task_summary"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS } 925 TextField { 926 id: editTodoSummary; Layout.fillWidth: true; color: Color.mOnSurface 927 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS } 928 } 929 930 NText { text: pluginApi.tr("panel.due_date"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS } 931 TextField { 932 id: editTodoDueDate; Layout.fillWidth: true; placeholderText: "YYYY-MM-DD"; color: Color.mOnSurface 933 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS } 934 } 935 936 NText { text: pluginApi.tr("panel.end_time"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS } 937 TextField { 938 id: editTodoDueTime; Layout.fillWidth: true; placeholderText: "HH:MM"; color: Color.mOnSurface 939 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS } 940 } 941 942 NText { text: pluginApi.tr("panel.description"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS } 943 TextField { 944 id: editTodoDescription; Layout.fillWidth: true; color: Color.mOnSurface 945 background: Rectangle { color: Color.mSurfaceVariant; radius: Style.radiusS } 946 } 947 948 NText { text: pluginApi.tr("panel.priority"); color: Color.mOnSurfaceVariant; font.pointSize: Style.fontSizeS } 949 RowLayout { 950 spacing: Style.marginS 951 Repeater { 952 model: [ 953 { label: pluginApi.tr("panel.priority_high"), value: 1 }, 954 { label: pluginApi.tr("panel.priority_medium"), value: 5 }, 955 { label: pluginApi.tr("panel.priority_low"), value: 9 } 956 ] 957 Rectangle { 958 Layout.preferredWidth: editPriLabel.implicitWidth + 2 * Style.marginM 959 Layout.preferredHeight: editPriLabel.implicitHeight + Style.marginS 960 color: todoDetailColumn.editTodoPriority === modelData.value ? Color.mPrimary : Color.mSurfaceVariant 961 radius: Style.radiusS 962 NText { 963 id: editPriLabel; anchors.centerIn: parent 964 text: modelData.label 965 color: todoDetailColumn.editTodoPriority === modelData.value ? Color.mOnPrimary : Color.mOnSurfaceVariant 966 font.weight: Font.Medium 967 } 968 MouseArea { 969 anchors.fill: parent; cursorShape: Qt.PointingHandCursor 970 onClicked: todoDetailColumn.editTodoPriority = 971 todoDetailColumn.editTodoPriority === modelData.value ? 0 : modelData.value 972 } 973 } 974 } 975 } 976 977 RowLayout { 978 Layout.fillWidth: true; spacing: Style.marginS 979 Item { Layout.fillWidth: true } 980 Rectangle { 981 Layout.preferredWidth: saveTodoBtn.implicitWidth + 2 * Style.marginM 982 Layout.preferredHeight: saveTodoBtn.implicitHeight + Style.marginS 983 color: Color.mPrimary; radius: Style.radiusS 984 NText { id: saveTodoBtn; anchors.centerIn: parent; text: pluginApi.tr("panel.save"); color: Color.mOnPrimary; font.weight: Font.Bold } 985 MouseArea { 986 anchors.fill: parent; cursorShape: Qt.PointingHandCursor 987 onClicked: { 988 var t = todoDetailColumn.todo 989 var dueTs = 0 990 if (editTodoDueDate.text.trim() !== "") { 991 var dateParts = editTodoDueDate.text.split("-") 992 var timeParts = editTodoDueTime.text.split(":") 993 var h = editTodoDueTime.text.trim() === "" ? 0 : parseInt(timeParts[0]) 994 var m = editTodoDueTime.text.trim() === "" ? 0 : parseInt(timeParts[1] || "0") 995 var d = new Date(parseInt(dateParts[0]), parseInt(dateParts[1]) - 1, parseInt(dateParts[2]), h, m, 0) 996 if (!isNaN(d.getTime())) dueTs = Math.floor(d.getTime() / 1000) 997 } 998 mainInstance?.updateTodoFields( 999 t.calendarUid, t.todoUid, 1000 editTodoSummary.text.trim(), 1001 editTodoDescription.text.trim(), 1002 dueTs, todoDetailColumn.editTodoPriority) 1003 showTodoDetailDialog = false; todoDetailEditMode = false 1004 } 1005 } 1006 } 1007 Rectangle { 1008 Layout.preferredWidth: editTodoCancelBtn.implicitWidth + 2 * Style.marginM 1009 Layout.preferredHeight: editTodoCancelBtn.implicitHeight + Style.marginS 1010 color: Color.mSurfaceVariant; radius: Style.radiusS 1011 NText { id: editTodoCancelBtn; anchors.centerIn: parent; text: pluginApi.tr("panel.cancel"); color: Color.mOnSurfaceVariant } 1012 MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor; onClicked: todoDetailEditMode = false } 1013 } 1014 } 1015 } 1016 } 1017 } 1018 } 1019 1020 // UI 1021 Rectangle { 1022 id: panelContainer 1023 anchors.fill: parent 1024 color: "transparent" 1025 1026 ColumnLayout { 1027 anchors.fill: parent 1028 anchors.margins: Style.marginM 1029 spacing: Style.marginM 1030 1031 //Header Section 1032 Rectangle { 1033 id: header 1034 Layout.fillWidth: true 1035 Layout.preferredHeight: topHeaderHeight 1036 color: Color.mSurfaceVariant 1037 radius: Style.radiusM 1038 1039 RowLayout { 1040 anchors.margins: Style.marginM 1041 anchors.fill: parent 1042 1043 NIcon { icon: "calendar-week"; pointSize: Style.fontSizeXXL; color: Color.mPrimary } 1044 1045 ColumnLayout { 1046 Layout.fillHeight: true 1047 spacing: 0 1048 NText { 1049 text: pluginApi.tr("panel.header") 1050 font.pointSize: Style.fontSizeL; font.weight: Font.Bold; color: Color.mOnSurface 1051 } 1052 RowLayout { 1053 spacing: Style.marginS 1054 NText { 1055 text: mainInstance?.monthRangeText || "" 1056 font.pointSize: Style.fontSizeS; font.weight: Font.Medium; color: Color.mOnSurfaceVariant 1057 } 1058 Rectangle { 1059 Layout.preferredWidth: 8; Layout.preferredHeight: 8; radius: 4 1060 color: mainInstance?.isLoading ? Color.mError : 1061 mainInstance?.syncStatus?.includes("No") ? Color.mError : Color.mOnSurfaceVariant 1062 } 1063 NText { 1064 text: mainInstance?.syncStatus || "" 1065 font.pointSize: Style.fontSizeS; color: Color.mOnSurfaceVariant 1066 } 1067 } 1068 } 1069 1070 Item { Layout.fillWidth: true } 1071 1072 RowLayout { 1073 spacing: Style.marginS 1074 NIconButton { 1075 icon: "plus"; tooltipText: pluginApi.tr("panel.add_event") 1076 onClicked: { 1077 createEventSummary.text = "" 1078 createEventLocation.text = "" 1079 createEventDescription.text = "" 1080 var now = new Date() 1081 var startH = now.getHours() + 1 1082 createEventDate.text = now.getFullYear() + "-" + String(now.getMonth()+1).padStart(2,'0') + "-" + String(now.getDate()).padStart(2,'0') 1083 createEventStartTime.text = String(startH).padStart(2,'0') + ":00" 1084 createEventEndTime.text = String(startH+1).padStart(2,'0') + ":00" 1085 showCreateDialog = true 1086 } 1087 } 1088 NIconButton { 1089 icon: "clipboard-check"; tooltipText: pluginApi.tr("panel.add_task") 1090 onClicked: { 1091 createTaskSummary.text = "" 1092 var now = new Date() 1093 var startH = now.getHours() + 1 1094 createTaskDueDate.text = now.getFullYear() + "-" + String(now.getMonth()+1).padStart(2,'0') + "-" + String(now.getDate()).padStart(2,'0') 1095 createTaskDueTime.text = String(startH).padStart(2,'0') + ":00" 1096 createTaskDescription.text = "" 1097 createTaskDialogColumn.selectedPriority = 0 1098 showCreateTaskDialog = true 1099 } 1100 } 1101 NIconButton { 1102 icon: mainInstance?.showCompletedTodos ? "eye-off" : "eye" 1103 tooltipText: pluginApi.tr("panel.show_completed") 1104 onClicked: { 1105 if (mainInstance) { 1106 mainInstance.showCompletedTodos = !mainInstance.showCompletedTodos 1107 mainInstance.loadTodos() 1108 } 1109 } 1110 } 1111 NIconButton { 1112 icon: "chevron-left" 1113 onClicked: mainInstance?.navigateWeek(-7) 1114 } 1115 NIconButton { 1116 icon: "calendar"; tooltipText: pluginApi.tr("panel.today") 1117 onClicked: { mainInstance?.goToToday(); Qt.callLater(root.scrollToCurrentTime) } 1118 } 1119 NIconButton { 1120 icon: "chevron-right" 1121 onClicked: mainInstance?.navigateWeek(7) 1122 } 1123 NIconButton { 1124 icon: "refresh"; tooltipText: I18n.tr("common.refresh") 1125 onClicked: { mainInstance?.loadEvents(); mainInstance?.loadTodos() } 1126 enabled: mainInstance ? !mainInstance.isLoading : false 1127 } 1128 NIconButton { 1129 icon: "close"; tooltipText: I18n.tr("common.close") 1130 onClicked: pluginApi.closePanel(pluginApi.panelOpenScreen) 1131 } 1132 } 1133 } 1134 } 1135 1136 // Calendar View 1137 Rectangle { 1138 Layout.fillWidth: true 1139 Layout.fillHeight: true 1140 color: Color.mSurfaceVariant 1141 radius: Style.radiusM 1142 clip: true 1143 1144 Column { 1145 anchors.fill: parent 1146 spacing: 0 1147 1148 //Day Headers 1149 Rectangle { 1150 id: dayHeaders 1151 width: parent.width 1152 height: 56 1153 color: Color.mSurfaceVariant 1154 radius: Style.radiusM 1155 1156 Row { 1157 anchors.fill: parent 1158 anchors.leftMargin: root.timeColumnWidth 1159 spacing: root.daySpacing 1160 1161 Repeater { 1162 model: 7 1163 Rectangle { 1164 width: mainInstance?.dayColumnWidth 1165 height: parent.height 1166 color: "transparent" 1167 property date dayDate: mainInstance?.weekDates?.[index] || new Date() 1168 property bool isToday: { 1169 var today = new Date() 1170 return dayDate.getDate() === today.getDate() && 1171 dayDate.getMonth() === today.getMonth() && 1172 dayDate.getFullYear() === today.getFullYear() 1173 } 1174 Rectangle { 1175 anchors.fill: parent 1176 anchors.margins: 4 1177 color: Color.mSurfaceVariant 1178 border.color: isToday ? Color.mPrimary : "transparent" 1179 border.width: 2 1180 radius: Style.radiusM 1181 Column { 1182 anchors.centerIn: parent 1183 spacing: 2 1184 NText { 1185 anchors.horizontalCenter: parent.horizontalCenter 1186 text: dayDate ? I18n.locale.dayName(dayDate.getDay(), Locale.ShortFormat).toUpperCase() : "" 1187 color: isToday ? Color.mPrimary : Color.mOnSurface 1188 font.pointSize: Style.fontSizeS; font.weight: Font.Medium 1189 } 1190 NText { 1191 anchors.horizontalCenter: parent.horizontalCenter 1192 text: dayDate ? ((dayDate.getDate() < 10 ? "0" : "") + dayDate.getDate()) : "" 1193 color: isToday ? Color.mPrimary : Color.mOnSurface 1194 font.pointSize: Style.fontSizeM; font.weight: Font.Bold 1195 } 1196 } 1197 } 1198 } 1199 } 1200 } 1201 } 1202 // All-day row 1203 Rectangle { 1204 id: allDayEventsSection 1205 width: parent.width 1206 height: mainInstance ? Math.round(mainInstance.allDaySectionHeight * Style.uiScaleRatio) : 0 1207 color: Color.mSurfaceVariant 1208 visible: height > 0 1209 1210 Item { 1211 id: allDayEventsContainer 1212 anchors.fill: parent 1213 anchors.leftMargin: root.timeColumnWidth 1214 1215 Repeater { 1216 model: 6 1217 delegate: Rectangle { 1218 width: 1; height: parent.height 1219 x: (index + 1) * ((mainInstance?.dayColumnWidth) + (root.daySpacing)) - ((root.daySpacing) / 2) 1220 color: Qt.alpha(mainInstance?.lineColor || Color.mOutline, mainInstance?.dayLineOpacitySetting || 0.9) 1221 } 1222 } 1223 1224 Repeater { 1225 model: mainInstance?.allDayEventsWithLayout || [] 1226 delegate: Item { 1227 property var eventData: modelData 1228 property bool isTodoItem: eventData.isTodo || false 1229 property bool isDeadline: eventData.isDeadlineMarker || false 1230 x: eventData.startDay * ((mainInstance?.dayColumnWidth) + (root.daySpacing)) 1231 y: eventData.lane * 25 1232 width: (eventData.spanDays * ((mainInstance?.dayColumnWidth) + (root.daySpacing))) - (root.daySpacing) 1233 height: isDeadline ? 10 : 24 1234 1235 Rectangle { 1236 anchors.fill: parent 1237 color: isDeadline ? Color.mSecondary : (isTodoItem ? Color.mSecondary : Color.mTertiary) 1238 radius: Style.radiusS 1239 opacity: isTodoItem && eventData.todoStatus === "COMPLETED" ? 0.5 : 1.0 1240 NText { 1241 anchors.fill: parent; anchors.margins: 4 1242 text: isDeadline ? "" : (isTodoItem ? (eventData.todoStatus === "COMPLETED" ? "\u2611 " : "\u2610 ") : "") + eventData.title 1243 color: isDeadline ? Color.mOnSecondary : (isTodoItem ? Color.mOnSecondary : Color.mOnTertiary) 1244 font.pointSize: Style.fontSizeXXS; font.weight: Font.Medium 1245 font.strikeout: isTodoItem && eventData.todoStatus === "COMPLETED" 1246 elide: Text.ElideRight; verticalAlignment: Text.AlignVCenter 1247 } 1248 } 1249 MouseArea { 1250 anchors.fill: parent 1251 hoverEnabled: true 1252 cursorShape: Qt.PointingHandCursor 1253 onEntered: { 1254 var tip = mainInstance?.getEventTooltip(eventData) || "" 1255 TooltipService.show(parent, tip, "auto", Style.tooltipDelay, Settings.data.ui.fontFixed) 1256 } 1257 onClicked: { 1258 if (isTodoItem) { 1259 mainInstance?.handleTodoClick(eventData) 1260 showTodoDetailDialog = true 1261 todoDetailEditMode = false 1262 showTodoDeleteConfirmation = false 1263 } else { 1264 mainInstance?.handleEventClick(eventData) 1265 showEventDetailDialog = true 1266 eventDetailEditMode = false 1267 showDeleteConfirmation = false 1268 } 1269 } 1270 onExited: TooltipService.hide() 1271 } 1272 } 1273 } 1274 } 1275 } 1276 // Calendar flickable 1277 Rectangle { 1278 width: parent.width 1279 height: parent.height - dayHeaders.height - (allDayEventsSection.visible ? allDayEventsSection.height : 0) 1280 color: Color.mSurfaceVariant 1281 radius: Style.radiusM 1282 clip: true 1283 1284 Flickable { 1285 id: calendarFlickable 1286 anchors.fill: parent 1287 clip: true 1288 contentHeight: 24 * (root.hourHeight) 1289 boundsBehavior: Flickable.DragOverBounds 1290 onHeightChanged: Qt.callLater(root.adjustHourHeightForViewport) 1291 1292 Component.onCompleted: { 1293 calendarFlickable.forceActiveFocus() 1294 } 1295 1296 // Keyboard interaction 1297 Keys.onPressed: function(event) { 1298 if (event.key === Qt.Key_Up || event.key === Qt.Key_Down) { 1299 var step = root.hourHeight 1300 var targetY = event.key === Qt.Key_Up ? Math.max(0, contentY - step) : 1301 Math.min(Math.max(0, contentHeight - height), contentY + step) 1302 scrollAnim.targetY = targetY 1303 scrollAnim.start() 1304 event.accepted = true 1305 } else if (event.key === Qt.Key_Left || event.key === Qt.Key_Right) { 1306 if (mainInstance) { 1307 mainInstance.navigateWeek(event.key === Qt.Key_Left ? -7 : 7) 1308 } 1309 event.accepted = true 1310 } 1311 } 1312 1313 NumberAnimation { 1314 id: scrollAnim 1315 target: calendarFlickable; property: "contentY"; duration: 100 1316 easing.type: Easing.OutCubic; property real targetY: 0; to: targetY 1317 } 1318 1319 ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } 1320 1321 Row { 1322 width: parent.width 1323 height: parent.height 1324 1325 // Time Column 1326 Column { 1327 width: root.timeColumnWidth 1328 height: parent.height 1329 Repeater { 1330 model: 23 1331 Rectangle { 1332 width: root.timeColumnWidth 1333 height: root.hourHeight 1334 color: "transparent" 1335 NText { 1336 text: { 1337 var hour = index + 1 1338 if (mainInstance?.use12hourFormat) { 1339 var d = new Date(); d.setHours(hour, 0, 0, 0) 1340 return mainInstance.formatTime(d) 1341 } 1342 return (hour < 10 ? "0" : "") + hour + ':00' 1343 } 1344 anchors.right: parent.right 1345 anchors.rightMargin: Style.marginS 1346 anchors.verticalCenter: parent.top 1347 anchors.verticalCenterOffset: root.hourHeight 1348 font.pointSize: Style.fontSizeXS; color: Color.mOnSurfaceVariant 1349 } 1350 } 1351 } 1352 } 1353 1354 // Hour Rectangles 1355 Item { 1356 width: 7 * ((mainInstance?.dayColumnWidth) + (root.daySpacing)) 1357 height: parent.height 1358 1359 Row { 1360 anchors.fill: parent 1361 spacing: root.daySpacing 1362 Repeater { 1363 model: 7 1364 Column { 1365 width: mainInstance?.dayColumnWidth 1366 height: parent.height 1367 Repeater { 1368 model: 24 1369 Rectangle { width: parent.width; height: 1; color: Color.mSurfaceVariant } 1370 } 1371 } 1372 } 1373 } 1374 // Hour Lines 1375 Repeater { 1376 model: 24 1377 Rectangle { 1378 width: parent.width; height: 1 1379 y: index * (root.hourHeight) 1380 color: Qt.alpha(mainInstance?.lineColor || Color.mOutline, mainInstance?.hourLineOpacitySetting || 0.5) 1381 } 1382 } 1383 // Day Lines 1384 Repeater { 1385 model: 6 1386 Rectangle { 1387 width: 1; height: parent.height 1388 x: (index + 1) * ((mainInstance?.dayColumnWidth) + (root.daySpacing)) - ((root.daySpacing) / 2) 1389 color: Qt.alpha(mainInstance?.lineColor || Color.mOutline, mainInstance?.dayLineOpacitySetting || 0.9) 1390 } 1391 } 1392 1393 // Event positioning 1394 Repeater { 1395 model: mainInstance?.eventsModel 1396 delegate: Item { 1397 property var eventData: model 1398 property int dayIndex: mainInstance?.getDisplayDayIndexForDate(model.startTime) ?? -1 1399 property real startHour: model.startTime.getHours() + model.startTime.getMinutes() / 60 1400 property real endHour: model.endTime.getHours() + model.endTime.getMinutes() / 60 1401 property real duration: Math.max(0, (model.endTime - model.startTime) / 3600000) 1402 1403 property real exactHeight: Math.max(1, duration * (root.hourHeight) - 1) 1404 property bool isCompact: exactHeight < 40 1405 property var overlapInfo: mainInstance?.overlappingEventsData?.[index] ?? { 1406 xOffset: 0, width: (mainInstance?.dayColumnWidth) - 8, lane: 0, totalLanes: 1 1407 } 1408 property real eventWidth: overlapInfo.width - 1 1409 property real eventXOffset: overlapInfo.xOffset 1410 1411 property bool isTodoItem: model.isTodo || false 1412 property bool isDeadline: model.isDeadlineMarker || false 1413 property color eventColor: isDeadline ? Color.mSecondary : (isTodoItem ? Color.mSecondary : Color.mPrimary) 1414 property color eventTextColor: isDeadline ? Color.mOnSecondary : (isTodoItem ? Color.mOnSecondary : Color.mOnPrimary) 1415 1416 visible: dayIndex >= 0 && dayIndex < 7 && duration > 0 1417 width: eventWidth 1418 height: isDeadline ? Math.max(8, Math.min(12, exactHeight)) : exactHeight 1419 x: dayIndex * ((mainInstance?.dayColumnWidth) + (root.daySpacing)) + eventXOffset 1420 y: startHour * (root.hourHeight) 1421 z: 100 + overlapInfo.lane 1422 1423 Rectangle { 1424 anchors.fill: parent 1425 color: eventColor 1426 radius: Style.radiusS 1427 opacity: isDeadline ? 0.95 : (isTodoItem && model.todoStatus === "COMPLETED" ? 0.5 : 0.9) 1428 clip: true 1429 Rectangle { 1430 visible: exactHeight < 5 && overlapInfo.lane > 0 1431 anchors.fill: parent 1432 color: "transparent" 1433 radius: parent.radius 1434 border.width: 1 1435 border.color: eventColor 1436 } 1437 Loader { 1438 anchors.fill: parent 1439 anchors.margins: exactHeight < 10 ? 1 : Style.marginS 1440 anchors.leftMargin: exactHeight < 10 ? 1 : Style.marginS + 3 1441 sourceComponent: isDeadline ? deadlineLayout : (isCompact ? compactLayout : normalLayout) 1442 } 1443 } 1444 1445 Component { 1446 id: normalLayout 1447 Column { 1448 spacing: 2 1449 width: parent.width - 3 1450 NText { 1451 visible: exactHeight >= 20 1452 text: (isTodoItem ? (model.todoStatus === "COMPLETED" ? "\u2611 " : "\u2610 ") : "") + model.title 1453 color: eventTextColor 1454 font.pointSize: Style.fontSizeXS; font.weight: Font.Medium 1455 font.strikeout: isTodoItem && model.todoStatus === "COMPLETED" 1456 elide: Text.ElideRight; width: parent.width 1457 } 1458 NText { 1459 visible: exactHeight >= 30 && !isTodoItem 1460 text: mainInstance?.formatTimeRangeForDisplay(model) || "" 1461 color: eventTextColor 1462 font.pointSize: Style.fontSizeXXS; opacity: 0.9 1463 elide: Text.ElideRight; width: parent.width 1464 } 1465 NText { 1466 visible: exactHeight >= 45 && model.location && model.location !== "" 1467 text: "\u26B2 " + (model.location || "") 1468 color: eventTextColor 1469 font.pointSize: Style.fontSizeXXS; opacity: 0.8 1470 elide: Text.ElideRight; width: parent.width 1471 } 1472 } 1473 } 1474 1475 Component { 1476 id: compactLayout 1477 NText { 1478 text: { 1479 var prefix = isTodoItem ? (model.todoStatus === "COMPLETED" ? "\u2611 " : "\u2610 ") : "" 1480 if (exactHeight < 15) return prefix + model.title 1481 if (isTodoItem) return prefix + model.title 1482 return model.title + " \u2022 " + (mainInstance?.formatTimeRangeForDisplay(model) || "") 1483 } 1484 color: eventTextColor 1485 font.pointSize: exactHeight < 15 ? Style.fontSizeXXS : Style.fontSizeXS 1486 font.weight: Font.Medium 1487 font.strikeout: isTodoItem && model.todoStatus === "COMPLETED" 1488 elide: Text.ElideRight; verticalAlignment: Text.AlignVCenter 1489 width: parent.width - 3 1490 } 1491 } 1492 1493 Component { 1494 id: deadlineLayout 1495 Rectangle { 1496 anchors.fill: parent 1497 color: eventColor 1498 radius: parent.radius 1499 opacity: 0.95 1500 } 1501 } 1502 1503 MouseArea { 1504 anchors.fill: parent 1505 hoverEnabled: true 1506 cursorShape: Qt.PointingHandCursor 1507 onEntered: { 1508 var tip = mainInstance?.getEventTooltip(model) || "" 1509 TooltipService.show(parent, tip, "auto", Style.tooltipDelay, Settings.data.ui.fontFixed) 1510 } 1511 onClicked: { 1512 if (isTodoItem) { 1513 mainInstance?.handleTodoClick(model) 1514 showTodoDetailDialog = true 1515 todoDetailEditMode = false 1516 showTodoDeleteConfirmation = false 1517 } else { 1518 mainInstance?.handleEventClick(eventData) 1519 showEventDetailDialog = true 1520 eventDetailEditMode = false 1521 showDeleteConfirmation = false 1522 } 1523 } 1524 onExited: TooltipService.hide() 1525 } 1526 } 1527 } 1528 1529 // Time Indicator 1530 Rectangle { 1531 property var now: new Date() 1532 property date today: new Date(now.getFullYear(), now.getMonth(), now.getDate()) 1533 property date weekStartDate: mainInstance?.weekStart ?? new Date() 1534 property date weekEndDate: mainInstance ? 1535 new Date(mainInstance.weekStart.getFullYear(), mainInstance.weekStart.getMonth(), mainInstance.weekStart.getDate() + 7) : new Date() 1536 property bool inCurrentWeek: today >= weekStartDate && today < weekEndDate 1537 property int currentDay: mainInstance?.getDayIndexForDate(now) ?? -1 1538 property real currentHour: now.getHours() + now.getMinutes() / 60 1539 1540 visible: inCurrentWeek && currentDay >= 0 1541 width: mainInstance?.dayColumnWidth 1542 height: 2 1543 x: currentDay * ((mainInstance?.dayColumnWidth) + (root.daySpacing)) 1544 y: currentHour * (root.hourHeight) 1545 color: Color.mError 1546 radius: 1 1547 z: 1000 1548 Rectangle { 1549 width: 8; height: 8; radius: 4; color: Color.mError 1550 anchors.verticalCenter: parent.verticalCenter; x: -4 1551 } 1552 Timer { 1553 interval: 60000; running: true; repeat: true 1554 onTriggered: parent.now = new Date() 1555 } 1556 } 1557 } 1558 } 1559 } 1560 } 1561 } 1562 } 1563 1564 } 1565 } 1566}