Personal noctalia plugins collection

feat: render tasks inline on calendar grid instead of separate tab

Tasks with due dates now appear directly on the weekly calendar view,
similar to Google Calendar. Removes the tab bar UI in favor of a
unified calendar view where todos are visually distinct (secondary
color, checkbox prefix) and clickable to toggle completion.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+150 -304
+78 -11
weekly-calendar/Main.qml
··· 22 22 property ListModel todosModel: ListModel {} 23 23 property var taskLists: [] 24 24 property bool todosLoading: false 25 - property string todoSyncStatus: "" 26 25 property bool showCompletedTodos: false 27 26 28 27 property real dayColumnWidth: 120 * Style.uiScaleRatio ··· 204 203 if (!CalendarService.available) { 205 204 syncStatus = pluginApi.tr("panel.no_service") 206 205 } else if (!CalendarService.events?.length) { 207 - syncStatus = pluginApi.tr("panel.no_events") 206 + var todoStats = processTodosForWeek() 207 + if (todoStats.count > 0) { 208 + syncStatus = pluginApi.tr("panel.no_events") + ", " + 209 + todoStats.count + " " + (todoStats.count === 1 ? pluginApi.tr("panel.task") : pluginApi.tr("panel.tasks")) 210 + } else { 211 + syncStatus = pluginApi.tr("panel.no_events") 212 + } 208 213 } else { 209 214 var stats = processCalendarEvents(CalendarService.events) 210 - syncStatus = stats.timedCount === 1 211 - ? `${stats.timedCount} ${pluginApi.tr("panel.event")}, ${stats.allDayCount} ${pluginApi.tr("panel.allday")}` 212 - : `${stats.timedCount} ${pluginApi.tr("panel.events")}, ${stats.allDayCount} ${pluginApi.tr("panel.allday")}` 215 + var todoStats = processTodosForWeek() 216 + var parts = [] 217 + parts.push(stats.timedCount === 1 218 + ? `${stats.timedCount} ${pluginApi.tr("panel.event")}` 219 + : `${stats.timedCount} ${pluginApi.tr("panel.events")}`) 220 + parts.push(`${stats.allDayCount} ${pluginApi.tr("panel.allday")}`) 221 + if (todoStats.count > 0) 222 + parts.push(todoStats.count + " " + (todoStats.count === 1 ? pluginApi.tr("panel.task") : pluginApi.tr("panel.tasks"))) 223 + syncStatus = parts.join(", ") 213 224 } 214 225 215 226 isLoading = false ··· 256 267 return {timedCount: timedCount, allDayCount: allDayCount} 257 268 } 258 269 270 + function processTodosForWeek() { 271 + var weekStartDate = new Date(weekStart) 272 + var weekEndDate = new Date(weekEnd) 273 + var count = 0 274 + 275 + for (var i = 0; i < todosModel.count; i++) { 276 + var todo = todosModel.get(i) 277 + if (!todo.due) continue 278 + 279 + var dueDate = new Date(todo.due) 280 + if (isNaN(dueDate.getTime())) continue 281 + if (dueDate < weekStartDate || dueDate >= weekEndDate) continue 282 + if (!showCompletedTodos && todo.status === "COMPLETED") continue 283 + 284 + var isDueAllDay = (dueDate.getHours() === 0 && dueDate.getMinutes() === 0) 285 + var endDate = isDueAllDay ? new Date(dueDate.getTime() + 86400000) 286 + : new Date(dueDate.getTime() + 1800000) 287 + 288 + var todoEvent = { 289 + id: "todo-" + todo.uid, 290 + title: todo.summary, 291 + description: todo.description || "", 292 + location: "", 293 + startTime: dueDate, 294 + endTime: endDate, 295 + allDay: isDueAllDay, 296 + multiDay: false, 297 + daySpan: 1, 298 + isTodo: true, 299 + todoUid: todo.uid, 300 + calendarUid: todo.calendarUid, 301 + todoStatus: todo.status, 302 + todoPriority: todo.priority, 303 + } 304 + 305 + if (isDueAllDay) { 306 + allDayEventsModel.append(todoEvent) 307 + } else { 308 + eventsModel.append(todoEvent) 309 + } 310 + count++ 311 + } 312 + 313 + // Recalculate layouts after adding todos 314 + if (count > 0) { 315 + calculateAllDayEventLayout() 316 + updateOverlappingEvents() 317 + eventsModel.layoutChanged() 318 + allDayEventsModel.layoutChanged() 319 + } 320 + 321 + return { count: count } 322 + } 323 + 259 324 function clearEventModels() { eventsModel.clear(); allDayEventsModel.clear() } 260 325 261 326 function processTimedEventIntoArray(eventObj, target) { ··· 284 349 id: id, title: event.summary || "Untitled Event", description: event.description || "", 285 350 location: event.location || "", startTime: start, endTime: end, allDay: allDay, multiDay: multiDay, 286 351 daySpan: daySpan, rawStart: event.start, rawEnd: event.end, duration: (event.end - event.start) / 3600, 287 - endsAtMidnight: endsMidnight 352 + endsAtMidnight: endsMidnight, isTodo: false, todoUid: "", calendarUid: "", todoStatus: "", todoPriority: 0 288 353 } 289 354 } 290 355 ··· 293 358 id: event.id + "-part-" + partIdx, title: event.title, description: event.description, 294 359 location: event.location, startTime: start, endTime: end, allDay: false, multiDay: true, 295 360 daySpan: 1, fullStartTime: event.startTime, fullEndTime: event.endTime, isPart: true, 296 - partDay: new Date(day), partIndex: partNum, totalParts: total 361 + partDay: new Date(day), partIndex: partNum, totalParts: total, 362 + isTodo: false, todoUid: "", calendarUid: "", todoStatus: "", todoPriority: 0 297 363 } 298 364 } 299 365 ··· 371 437 startTime: event.startTime, endTime: event.endTime, allDay: event.allDay, multiDay: event.multiDay, 372 438 daySpan: event.daySpan, rawStart: event.rawStart, rawEnd: event.rawEnd, duration: event.duration, 373 439 endsAtMidnight: event.endsAtMidnight, fullStartTime: event.fullStartTime, fullEndTime: event.fullEndTime, 374 - startDay: startDay, spanDays: spanDays, lane: lane, isContinuation: isCont 440 + startDay: startDay, spanDays: spanDays, lane: lane, isContinuation: isCont, 441 + isTodo: event.isTodo || false, todoUid: event.todoUid || "", calendarUid: event.calendarUid || "", 442 + todoStatus: event.todoStatus || "", todoPriority: event.todoPriority || 0 375 443 } 376 444 } 377 445 ··· 647 715 for (var i = 0; i < result.length; i++) { 648 716 todosModel.append(result[i]) 649 717 } 650 - todoSyncStatus = result.length + (result.length === 1 ? " task" : " tasks") 718 + // Re-process events to include updated todos on the calendar 719 + Qt.callLater(updateEventsFromService) 651 720 } 652 721 } catch(e) { 653 722 console.error("[weekly-calendar] Failed to parse todos: " + listTodosStdout) 654 - todoSyncStatus = pluginApi ? pluginApi.tr("panel.task_error") : "Error" 655 723 } 656 724 } else { 657 725 console.error("[weekly-calendar] list-todos.py failed: " + listTodosStderr) 658 - todoSyncStatus = pluginApi ? pluginApi.tr("panel.task_error") : "Error" 659 726 } 660 727 listTodosStdout = "" 661 728 listTodosStderr = ""
+68 -287
weekly-calendar/Panel.qml
··· 22 22 23 23 property bool showCreateDialog: false 24 24 property bool showCreateTaskDialog: false 25 - property int currentTab: 0 // 0 = Calendar, 1 = Tasks 26 25 27 26 property real hourHeight: 50 * Style.uiScaleRatio 28 27 property real timeColumnWidth: 65 * Style.uiScaleRatio ··· 35 34 mainInstance.refreshView() 36 35 mainInstance.goToToday() 37 36 Qt.callLater(root.scrollToCurrentTime) 38 - if (currentTab === 1) mainInstance.loadTodos() 37 + mainInstance.loadTodos() 39 38 } 40 39 41 40 // Scroll to time indicator position ··· 52 51 scrollAnim.targetY = Math.max(0, Math.min(scrollPos, maxScroll)) 53 52 scrollAnim.start() 54 53 } 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 54 } 82 55 83 56 // Event creation dialog ··· 402 375 anchors.margins: Style.marginM 403 376 anchors.fill: parent 404 377 405 - NIcon { icon: currentTab === 0 ? "calendar-week" : "clipboard-check"; pointSize: Style.fontSizeXXL; color: Color.mPrimary } 378 + NIcon { icon: "calendar-week"; pointSize: Style.fontSizeXXL; color: Color.mPrimary } 406 379 407 380 ColumnLayout { 408 381 Layout.fillHeight: true ··· 414 387 RowLayout { 415 388 spacing: Style.marginS 416 389 NText { 417 - text: currentTab === 0 ? (mainInstance?.monthRangeText || "") : (mainInstance?.todoSyncStatus || "") 390 + text: mainInstance?.monthRangeText || "" 418 391 font.pointSize: Style.fontSizeS; font.weight: Font.Medium; color: Color.mOnSurfaceVariant 419 392 } 420 393 Rectangle { 421 394 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 - } 395 + color: mainInstance?.isLoading ? Color.mError : 396 + mainInstance?.syncStatus?.includes("No") ? Color.mError : Color.mOnSurfaceVariant 430 397 } 431 398 NText { 432 - text: currentTab === 0 ? (mainInstance?.syncStatus || "") : "" 399 + text: mainInstance?.syncStatus || "" 433 400 font.pointSize: Style.fontSizeS; color: Color.mOnSurfaceVariant 434 401 } 435 402 } ··· 439 406 440 407 RowLayout { 441 408 spacing: Style.marginS 442 - // Calendar-specific buttons 443 409 NIconButton { 444 - visible: currentTab === 0 445 410 icon: "plus"; tooltipText: pluginApi.tr("panel.add_event") 446 411 onClicked: { 447 412 createEventSummary.text = "" ··· 456 421 } 457 422 } 458 423 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") 424 + icon: "clipboard-check"; tooltipText: pluginApi.tr("panel.add_task") 477 425 onClicked: { 478 426 createTaskSummary.text = "" 479 427 createTaskDueDate.text = "" ··· 483 431 } 484 432 } 485 433 NIconButton { 486 - visible: currentTab === 1 487 434 icon: mainInstance?.showCompletedTodos ? "eye-off" : "eye" 488 435 tooltipText: pluginApi.tr("panel.show_completed") 489 436 onClicked: { ··· 493 440 } 494 441 } 495 442 } 496 - // Shared buttons 443 + NIconButton { 444 + icon: "chevron-left" 445 + onClicked: mainInstance?.navigateWeek(-7) 446 + } 447 + NIconButton { 448 + icon: "calendar"; tooltipText: pluginApi.tr("panel.today") 449 + onClicked: { mainInstance?.goToToday(); Qt.callLater(root.scrollToCurrentTime) } 450 + } 451 + NIconButton { 452 + icon: "chevron-right" 453 + onClicked: mainInstance?.navigateWeek(7) 454 + } 497 455 NIconButton { 498 456 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) 457 + onClicked: { mainInstance?.loadEvents(); mainInstance?.loadTodos() } 458 + enabled: mainInstance ? !mainInstance.isLoading : false 505 459 } 506 460 NIconButton { 507 461 icon: "close"; tooltipText: I18n.tr("common.close") ··· 511 465 } 512 466 } 513 467 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 468 // Calendar View 565 469 Rectangle { 566 470 Layout.fillWidth: true ··· 568 472 color: Color.mSurfaceVariant 569 473 radius: Style.radiusM 570 474 clip: true 571 - visible: currentTab === 0 572 475 573 476 Column { 574 477 anchors.fill: parent ··· 654 557 model: mainInstance?.allDayEventsWithLayout || [] 655 558 delegate: Item { 656 559 property var eventData: modelData 560 + property bool isTodoItem: eventData.isTodo || false 657 561 x: eventData.startDay * ((mainInstance?.dayColumnWidth) + (root.daySpacing)) 658 562 y: eventData.lane * 25 659 563 width: (eventData.spanDays * ((mainInstance?.dayColumnWidth) + (root.daySpacing))) - (root.daySpacing) ··· 661 565 662 566 Rectangle { 663 567 anchors.fill: parent 664 - color: Color.mTertiary 568 + color: isTodoItem ? Color.mSecondary : Color.mTertiary 665 569 radius: Style.radiusS 570 + opacity: isTodoItem && eventData.todoStatus === "COMPLETED" ? 0.5 : 1.0 666 571 NText { 667 572 anchors.fill: parent; anchors.margins: 4 668 - text: eventData.title 669 - color: Color.mOnTertiary 573 + text: (isTodoItem ? (eventData.todoStatus === "COMPLETED" ? "\u2611 " : "\u2610 ") : "") + eventData.title 574 + color: isTodoItem ? Color.mOnSecondary : Color.mOnTertiary 670 575 font.pointSize: Style.fontSizeXXS; font.weight: Font.Medium 576 + font.strikeout: isTodoItem && eventData.todoStatus === "COMPLETED" 671 577 elide: Text.ElideRight; verticalAlignment: Text.AlignVCenter 672 578 } 673 579 } ··· 679 585 var tip = mainInstance?.getEventTooltip(eventData) || "" 680 586 TooltipService.show(parent, tip, "auto", Style.tooltipDelay, Settings.data.ui.fontFixed) 681 587 } 682 - onClicked: mainInstance?.handleEventClick(eventData) 588 + onClicked: { 589 + if (isTodoItem) { 590 + if (eventData.todoStatus === "COMPLETED") 591 + mainInstance?.uncompleteTodo(eventData.calendarUid, eventData.todoUid) 592 + else 593 + mainInstance?.completeTodo(eventData.calendarUid, eventData.todoUid) 594 + } else { 595 + mainInstance?.handleEventClick(eventData) 596 + } 597 + } 683 598 onExited: TooltipService.hide() 684 599 } 685 600 } ··· 827 742 y: startHour * (mainInstance?.hourHeight || 50) 828 743 z: 100 + overlapInfo.lane 829 744 745 + property bool isTodoItem: model.isTodo || false 746 + property color eventColor: isTodoItem ? Color.mSecondary : Color.mPrimary 747 + property color eventTextColor: isTodoItem ? Color.mOnSecondary : Color.mOnPrimary 748 + 830 749 Rectangle { 831 750 anchors.fill: parent 832 - color: Color.mPrimary 751 + color: eventColor 833 752 radius: Style.radiusS 834 - opacity: 0.9 753 + opacity: isTodoItem && model.todoStatus === "COMPLETED" ? 0.5 : 0.9 835 754 clip: true 836 755 Rectangle { 837 756 visible: exactHeight < 5 && overlapInfo.lane > 0 ··· 839 758 color: "transparent" 840 759 radius: parent.radius 841 760 border.width: 1 842 - border.color: Color.mPrimary 761 + border.color: eventColor 843 762 } 844 763 Loader { 845 764 anchors.fill: parent ··· 856 775 width: parent.width - 3 857 776 NText { 858 777 visible: exactHeight >= 20 859 - text: model.title 860 - color: Color.mOnPrimary 778 + text: (isTodoItem ? (model.todoStatus === "COMPLETED" ? "\u2611 " : "\u2610 ") : "") + model.title 779 + color: eventTextColor 861 780 font.pointSize: Style.fontSizeXS; font.weight: Font.Medium 781 + font.strikeout: isTodoItem && model.todoStatus === "COMPLETED" 862 782 elide: Text.ElideRight; width: parent.width 863 783 } 864 784 NText { 865 - visible: exactHeight >= 30 785 + visible: exactHeight >= 30 && !isTodoItem 866 786 text: mainInstance?.formatTimeRangeForDisplay(model) || "" 867 - color: Color.mOnPrimary 787 + color: eventTextColor 868 788 font.pointSize: Style.fontSizeXXS; opacity: 0.9 869 789 elide: Text.ElideRight; width: parent.width 870 790 } 871 791 NText { 872 792 visible: exactHeight >= 45 && model.location && model.location !== "" 873 793 text: "\u26B2 " + (model.location || "") 874 - color: Color.mOnPrimary 794 + color: eventTextColor 875 795 font.pointSize: Style.fontSizeXXS; opacity: 0.8 876 796 elide: Text.ElideRight; width: parent.width 877 797 } ··· 881 801 Component { 882 802 id: compactLayout 883 803 NText { 884 - text: exactHeight < 15 ? model.title : 885 - model.title + " • " + (mainInstance?.formatTimeRangeForDisplay(model) || "") 886 - color: Color.mOnPrimary 804 + text: { 805 + var prefix = isTodoItem ? (model.todoStatus === "COMPLETED" ? "\u2611 " : "\u2610 ") : "" 806 + if (exactHeight < 15) return prefix + model.title 807 + if (isTodoItem) return prefix + model.title 808 + return model.title + " \u2022 " + (mainInstance?.formatTimeRangeForDisplay(model) || "") 809 + } 810 + color: eventTextColor 887 811 font.pointSize: exactHeight < 15 ? Style.fontSizeXXS : Style.fontSizeXS 888 812 font.weight: Font.Medium 813 + font.strikeout: isTodoItem && model.todoStatus === "COMPLETED" 889 814 elide: Text.ElideRight; verticalAlignment: Text.AlignVCenter 890 815 width: parent.width - 3 891 816 } ··· 899 824 var tip = mainInstance?.getEventTooltip(model) || "" 900 825 TooltipService.show(parent, tip, "auto", Style.tooltipDelay, Settings.data.ui.fontFixed) 901 826 } 902 - onClicked: mainInstance?.handleEventClick(eventData) 827 + onClicked: { 828 + if (isTodoItem) { 829 + if (model.todoStatus === "COMPLETED") 830 + mainInstance?.uncompleteTodo(model.calendarUid, model.todoUid) 831 + else 832 + mainInstance?.completeTodo(model.calendarUid, model.todoUid) 833 + } else { 834 + mainInstance?.handleEventClick(eventData) 835 + } 836 + } 903 837 onExited: TooltipService.hide() 904 838 } 905 839 } ··· 940 874 } 941 875 } 942 876 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 877 } 1097 878 } 1098 879 }
+2 -3
weekly-calendar/i18n/en.json
··· 26 26 "calendar_select": "Calendar", 27 27 "event_created": "Event created", 28 28 "event_error": "Failed to create event", 29 - "calendar_tab": "Calendar", 30 - "tasks_tab": "Tasks", 29 + "task": "task", 30 + "tasks": "tasks", 31 31 "add_task": "Add Task", 32 32 "task_summary": "Task Summary", 33 33 "due_date": "Due Date (optional)", ··· 38 38 "task_list_select": "Task List", 39 39 "task_created": "Task created", 40 40 "task_error": "Failed to create task", 41 - "no_tasks": "No tasks", 42 41 "show_completed": "Show completed", 43 42 "overdue": "Overdue" 44 43 },
+2 -3
weekly-calendar/i18n/zh-CN.json
··· 26 26 "calendar_select": "日历", 27 27 "event_created": "活动已创建", 28 28 "event_error": "创建活动失败", 29 - "calendar_tab": "日历", 30 - "tasks_tab": "任务", 29 + "task": "个任务", 30 + "tasks": "个任务", 31 31 "add_task": "添加任务", 32 32 "task_summary": "任务摘要", 33 33 "due_date": "截止日期(可选)", ··· 38 38 "task_list_select": "任务列表", 39 39 "task_created": "任务已创建", 40 40 "task_error": "创建任务失败", 41 - "no_tasks": "没有任务", 42 41 "show_completed": "显示已完成", 43 42 "overdue": "已逾期" 44 43 },