This commit is contained in:
2025-09-28 17:05:51 +08:00
parent 8eb80ab66d
commit d97fa3e0fe
398 changed files with 18737 additions and 0 deletions

View File

@@ -0,0 +1,57 @@
# Qt QML Language Server configuration
qmlls.ini
# Build directories
build/
Desktop_Qt_6_9_0_MinGW_64_bit-Debug/
# Qt Creator user settings
CMakeLists.txt.user
# Compiled Object files
*.o
*.obj
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Compiled Static libraries
*.a
*.lib
# Executables
*.exe
*.out
*.app
# Qt-specific
*.qm
*.prl
*.pro.user
*.pro.user.*
*.qbs.user
*.qbs.user.*
*.moc
moc_*.cpp
moc_*.h
qrc_*.cpp
ui_*.h
Makefile*
*build-*
# QML cache and compiled files
*.qmlc
*.jsc
# Qt temporary files
*~
# macOS
.DS_Store
# Windows
Thumbs.db
ehthumbs.db
Desktop.ini

View File

@@ -0,0 +1,55 @@
cmake_minimum_required(VERSION 3.16)
project(TodoList VERSION 0.1 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt6 REQUIRED COMPONENTS Quick QuickControls2)
qt_standard_project_setup(REQUIRES 6.8)
qt_add_executable(TodoList
src/main.cpp
)
qt_add_qml_module(TodoList
URI TodoList
VERSION 1.0
QML_FILES
# Main View
src/views/MainView.qml
# Components
src/components/AppHeader.qml
src/components/AddTaskBar.qml
src/components/TaskItem.qml
src/components/TaskStats.qml
# Models
src/models/TaskListModel.qml
# Utils
src/utils/TaskStorage.qml
)
# Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1.
# If you are developing for iOS or macOS you should consider setting an
# explicit, fixed bundle identifier manually though.
set_target_properties(TodoList PROPERTIES
# MACOSX_BUNDLE_GUI_IDENTIFIER com.example.app01_todolist_starter
MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
MACOSX_BUNDLE TRUE
WIN32_EXECUTABLE TRUE
)
target_link_libraries(TodoList
PRIVATE Qt6::Quick Qt6::QuickControls2
)
include(GNUInstallDirs)
install(TARGETS TodoList
BUNDLE DESTINATION .
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

View File

@@ -0,0 +1,110 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Item {
id: root
height: 60
signal taskAdded(string taskText)
property color backgroundColor: "#ffffff"
property color textColor: "#333333"
property color primaryColor: "#007aff"
property bool darkMode: false
Rectangle {
id: background
anchors.fill: parent
color: "transparent"
Rectangle {
anchors.fill: parent
color: root.backgroundColor
radius: 12
border.color: root.darkMode ? "#404040" : "#e0e0e0"
border.width: 1
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 12
// Add icon
Rectangle {
Layout.preferredWidth: 36
Layout.preferredHeight: 36
color: root.primaryColor
radius: 18
Text {
anchors.centerIn: parent
text: "+"
color: "#ffffff"
font.pixelSize: 20
font.weight: Font.Bold
}
}
// Text input
TextField {
id: taskInput
Layout.fillWidth: true
Layout.preferredHeight: 36
placeholderText: qsTr("Add a new task...")
placeholderTextColor: root.darkMode ? "#888888" : "#999999"
color: root.textColor
font.pixelSize: 16
selectByMouse: true
background: Rectangle {
color: "transparent"
border.color: "transparent"
}
Keys.onReturnPressed: root.addTask()
Keys.onEnterPressed: root.addTask()
}
// Add button
Button {
id: addButton
Layout.preferredWidth: 80
Layout.preferredHeight: 36
text: qsTr("Add")
enabled: taskInput.text.trim().length > 0
background: Rectangle {
color: addButton.enabled ?
(addButton.pressed ? Qt.darker(root.primaryColor, 1.2) : root.primaryColor) :
(root.darkMode ? "#404040" : "#e0e0e0")
radius: 8
}
contentItem: Text {
text: addButton.text
color: addButton.enabled ? "#ffffff" : (root.darkMode ? "#888888" : "#cccccc")
font.pixelSize: 14
font.weight: Font.Medium
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
onClicked: root.addTask()
}
}
}
}
function addTask() {
if (taskInput.text.trim().length > 0) {
root.taskAdded(taskInput.text.trim())
taskInput.text = ""
taskInput.focus = false
}
}
}

View File

@@ -0,0 +1,86 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Item {
id: root
height: 80
signal toggleDarkMode()
// Properties that will be bound from parent
property color textColor: "#333333"
property color textSecondary: "#999999"
property bool darkMode: false
Rectangle {
anchors.fill: parent
color: "transparent"
RowLayout{
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 8
spacing: 12
// App icon
Text {
text: "📝"
font.pixelSize: 32
}
// App title and subtitle
Column {
Layout.fillWidth: true
spacing: 2
Text {
text: "My Tasks"
font.pixelSize: 28
font.weight: Font.Bold
color: root.textColor
}
Text {
text: qsTr("Stay organized, stay productive")
font.pixelSize: 12
color: root.textSecondary
opacity: 0.8
}
}
// Dark mode toggle
Button {
id: themeToggle
Layout.preferredWidth: 50
Layout.preferredHeight: 50
background: Rectangle {
color: themeToggle.pressed ?
(root.darkMode ? "#404040" : "#e0e0e0") :
(root.darkMode ? "#2d2d2d" : "#f5f5f5")
radius: 25
border.color: root.darkMode ? "#555555" : "#d0d0d0"
border.width: 1
Behavior on color {
ColorAnimation { duration: 150 }
}
}
contentItem: Text{
text: root.darkMode ? "☀️" : "🌙"
font.pixelSize: 20
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
//Emit the signal when the button is clicked
onClicked: root.toggleDarkMode()
}
}
}
}

View File

@@ -0,0 +1,209 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Item {
id: root
height: 56
required property string taskTitle
required property bool taskDone
signal toggleDone()
signal deleteTask()
property color backgroundColor: "#ffffff"
property color textColor: "#333333"
property color completedColor: "#999999"
property color primaryColor: "#007aff"
property bool darkMode: false
// Constants for this component
readonly property int itemHeight: 56
readonly property int margins: 4
readonly property int innerMargins: 12
readonly property int checkboxSize: 24
readonly property int deleteButtonSize: 32
Rectangle {
id: taskItemBackground
anchors.fill: parent
color: "transparent"
Rectangle {
id: itemBackground
anchors.fill: parent
anchors.margins: root.margins
color: root.backgroundColor
radius: 8
border.color: root.taskDone ? "transparent" : (root.darkMode ? "#404040" : "#f0f0f0")
border.width: 1
opacity: root.taskDone ? 0.7 : 1.0
scale: itemBackground.hovered ? 1.02 : 1.0
// Smooth transitions for background properties
Behavior on opacity {
NumberAnimation { duration: 300 }
}
Behavior on border.color {
ColorAnimation { duration: 300 }
}
Behavior on scale {
NumberAnimation { duration: 150; easing.type: Easing.OutQuad }
}
RowLayout {
anchors.fill: parent
anchors.margins: root.innerMargins
spacing: 12
// Checkbox
Rectangle {
id: checkbox
Layout.preferredWidth: root.checkboxSize
Layout.preferredHeight: root.checkboxSize
color: root.taskDone ? root.primaryColor : "transparent"
border.color: root.taskDone ? root.primaryColor : (root.darkMode ? "#666666" : "#cccccc")
border.width: 2
radius: 4
// Smooth transitions for checkbox state changes
Behavior on color {
ColorAnimation { duration: 200 }
}
Behavior on border.color {
ColorAnimation { duration: 200 }
}
Behavior on scale {
NumberAnimation { duration: 100; easing.type: Easing.OutBack }
}
// Checkmark
Text {
anchors.centerIn: parent
text: "✓"
color: "#ffffff"
font.pixelSize: 16
font.weight: Font.Bold
opacity: root.taskDone ? 1.0 : 0.0
scale: root.taskDone ? 1.0 : 0.3
Behavior on opacity {
NumberAnimation { duration: 200 }
}
Behavior on scale {
NumberAnimation { duration: 200; easing.type: Easing.OutBack }
}
}
MouseArea {
anchors.fill: parent
onClicked: {
// Add click animation
checkbox.scale = 0.9
scaleResetTimer.restart()
//Toggle the task
root.toggleDone()
}
cursorShape: Qt.PointingHandCursor
}
// Timer to reset scale after click
Timer {
id: scaleResetTimer
interval: 100
onTriggered: checkbox.scale = 1.0
}
}
// Task text
Text {
id: taskText
Layout.fillWidth: true
text: root.taskTitle
color: root.taskDone ? root.completedColor : root.textColor
font.pixelSize: 16
font.strikeout: root.taskDone
wrapMode: Text.WordWrap
// Smooth color transition when task state changes
Behavior on color {
ColorAnimation { duration: 300 }
}
// Subtle scale animation on completion
scale: root.taskDone ? 0.95 : 1.0
Behavior on scale {
NumberAnimation { duration: 200; easing.type: Easing.OutQuad }
}
MouseArea {
anchors.fill: parent
onClicked: root.toggleDone()
cursorShape: Qt.PointingHandCursor
}
}
// Delete button
Button {
id: deleteButton
Layout.preferredWidth: root.deleteButtonSize
Layout.preferredHeight: root.deleteButtonSize
opacity: itemBackground.hovered ? 1.0 : 0.3
scale: deleteButton.pressed ? 0.9 : 1.0
// Smooth transitions
Behavior on opacity {
NumberAnimation { duration: 200 }
}
Behavior on scale {
NumberAnimation { duration: 100; easing.type: Easing.OutQuad }
}
background: Rectangle {
color: deleteButton.pressed ? "#ff3b30" : "transparent"
radius: 16
Behavior on color {
ColorAnimation { duration: 150 }
}
}
contentItem: Text {
text: "🗑"
font.pixelSize: 16
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
rotation: deleteButton.pressed ? 15 : 0
Behavior on rotation {
NumberAnimation { duration: 100 }
}
}
onClicked: {
root.deleteTask()
}
}
}
// Hover effect
property bool hovered: false
HoverHandler {
onHoveredChanged: itemBackground.hovered = hovered
}
}
}
}

View File

@@ -0,0 +1,95 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Item {
id: root
height: visible ? 60 : 0
visible: totalTasks > 0
property int totalTasks: 0
property int completedTasks: 0
property int remainingTasks: 0
property bool hasCompleted: false
property color backgroundColor: "#ffffff"
property color textColor: "#333333"
property color secondaryTextColor: "#666666"
property color primaryColor: "#007aff"
property color dangerColor: "#ff3b30"
property bool darkMode: false
signal clearCompleted()
Rectangle {
anchors.fill: parent
color: "transparent"
Rectangle {
anchors.fill: parent
color: root.backgroundColor
radius: 12
border.color: root.darkMode ? "#404040" : "#e0e0e0"
border.width: 1
RowLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 16
// Statistics text
Column {
Layout.fillWidth: true
spacing: 2
Text {
text: root.totalTasks === 1 ?
qsTr("%1 task").arg(root.totalTasks) :
qsTr("%1 tasks").arg(root.totalTasks)
color: root.textColor
font.pixelSize: 16
font.weight: Font.Medium
}
Text {
text: root.completedTasks > 0 ?
qsTr("%1 completed, %2 remaining").arg(root.completedTasks).arg(root.remainingTasks) :
qsTr("No tasks completed yet")
color: root.secondaryTextColor
font.pixelSize: 14
visible: root.totalTasks > 0
}
}
// Clear completed button
Button {
id: clearButton
Layout.preferredHeight: 36
text: qsTr("Clear Completed")
visible: root.hasCompleted
enabled: root.hasCompleted
background: Rectangle {
color: clearButton.pressed ?
Qt.darker(root.dangerColor, 1.2) :
(clearButton.hovered ? root.dangerColor : "transparent")
border.color: root.dangerColor
border.width: 1
radius: 8
}
contentItem: Text {
text: clearButton.text
color: clearButton.hovered ? "#ffffff" : root.dangerColor
font.pixelSize: 14
font.weight: Font.Medium
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
onClicked: root.clearCompleted()
}
}
}
}
}

View File

@@ -0,0 +1,33 @@
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQuickStyle>
#include <QSettings>
#include <QDebug>
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
// Set application identifiers for Settings
app.setOrganizationName("TodoApp");
app.setOrganizationDomain("todoapp.local");
app.setApplicationName("TodoList");
// Set Qt Quick Controls style to Basic to enable customization
QQuickStyle::setStyle("Basic");
// Print the settings location
QSettings settings;
qDebug() << "Settings location:" << settings.fileName();
QQmlApplicationEngine engine;
QObject::connect(
&engine,
&QQmlApplicationEngine::objectCreationFailed,
&app,
[]() { QCoreApplication::exit(-1); },
Qt::QueuedConnection);
engine.loadFromModule("TodoList", "MainView");
return app.exec();
}

View File

@@ -0,0 +1,164 @@
import QtQuick
Item {
id: root
// Signal emitted when storage is initialized and ready
signal storageInitialized()
// The actual ListModel for tasks
property alias model: taskListModel
property alias count: taskListModel.count
// Auto-save flag
property bool autoSave: true
ListModel {
id: taskListModel
}
// Storage component for persistence
TaskStorage {
id: storage
onTasksLoaded: function(tasks) {
root.loadTasksFromArray(tasks)
}
Component.onCompleted: {
// Emit signal that storage is ready
root.storageInitialized()
// Load existing tasks
let savedTasks = loadTasks()
if (savedTasks.length === 0) {
// Add some sample tasks only if no saved tasks exist
root.addTask("Learn Qt QML")
root.addTask("Build a todo app")
root.addTask("Practice QML components")
}
}
}
// Auto-save trigger - called after any modification
function saveToStorage() {
if (autoSave) {
storage.saveTasks(taskListModel)
}
}
// Load tasks from an array (used by storage)
function loadTasksFromArray(taskArray) {
taskListModel.clear()
for (let i = 0; i < taskArray.length; i++) {
let task = taskArray[i]
taskListModel.append({
"id": task.id || generateId(),
"title": task.title || "",
"completed": task.completed || false
})
}
}
// Add a new task
function addTask(title) {
if (title && title.trim().length > 0) {
taskListModel.append({
"title": title.trim(),
"completed": false,
"id": generateId()
})
saveToStorage()
}
}
// Toggle task completion status
function toggleTask(index) {
if (index >= 0 && index < taskListModel.count) {
taskListModel.setProperty(index, "completed", !taskListModel.get(index).completed)
saveToStorage()
}
}
// Delete a task
function deleteTask(index) {
if (index >= 0 && index < taskListModel.count) {
taskListModel.remove(index)
saveToStorage()
}
}
// Delete task by ID
function deleteTaskById(taskId) {
for (let i = 0; i < taskListModel.count; i++) {
if (taskListModel.get(i).id === taskId) {
taskListModel.remove(i)
saveToStorage()
break
}
}
}
// Clear all completed tasks
function clearCompletedTasks() {
for (let i = taskListModel.count - 1; i >= 0; i--) {
if (taskListModel.get(i).completed) {
taskListModel.remove(i)
}
}
saveToStorage()
}
// Get statistics
function getStats() {
let total = taskListModel.count
let completed = 0
for (let i = 0; i < taskListModel.count; i++) {
if (taskListModel.get(i).completed) {
completed++
}
}
return {
"total": total,
"completed": completed,
"remaining": total - completed
}
}
// Check if there are any completed tasks
function hasCompletedTasks() {
for (let i = 0; i < taskListModel.count; i++) {
if (taskListModel.get(i).completed) {
return true
}
}
return false
}
// Get item at index
function get(index) {
return taskListModel.get(index)
}
// Generate a unique ID for tasks
function generateId() {
return Date.now() + Math.random().toString(36).substr(2, 9)
}
// Storage utility functions
function clearAllData() {
storage.clearStorage()
taskListModel.clear()
}
// Access to storage for dark mode preference
function saveDarkMode(darkMode) {
storage.saveDarkMode(darkMode)
}
function loadDarkMode() {
return storage.loadDarkMode()
}
}

View File

@@ -0,0 +1,83 @@
import QtQuick
import QtCore
Item {
id: root
// Signal emitted when tasks are loaded from storage
signal tasksLoaded(var tasks)
// Settings component for persistent storage
Settings {
id: settings
category: "Tasks"
// Store tasks as a JSON string
property string tasksData: "[]"
// Store dark mode preference
property bool darkMode: false
}
// Save tasks to storage
function saveTasks(tasks) {
try {
// Convert ListModel data to a proper array
let taskArray = []
for (let i = 0; i < tasks.count; i++) {
let task = tasks.get(i)
taskArray.push({
id: task.id,
title: task.title,
completed: task.completed
})
}
// Save as JSON string
settings.tasksData = JSON.stringify(taskArray)
settings.sync() // Force immediate save
console.log("Tasks saved to storage:", taskArray.length, "tasks")
} catch (error) {
console.error("Error saving tasks:", error)
}
}
// Load tasks from storage
function loadTasks() {
try {
if (settings.tasksData && settings.tasksData !== "[]") {
let taskArray = JSON.parse(settings.tasksData)
console.log("Tasks loaded from storage:", taskArray.length, "tasks")
root.tasksLoaded(taskArray)
return taskArray
} else {
console.log("No saved tasks found, starting with empty list")
root.tasksLoaded([])
return []
}
} catch (error) {
console.error("Error loading tasks:", error)
root.tasksLoaded([])
return []
}
}
// Save dark mode preference
function saveDarkMode(darkMode) {
settings.darkMode = darkMode
settings.sync()
}
// Load dark mode preference
function loadDarkMode() {
return settings.darkMode
}
// Clear all stored data
function clearStorage() {
settings.tasksData = "[]"
settings.darkMode = false
settings.sync()
console.log("Storage cleared")
}
}

View File

@@ -0,0 +1,261 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
ApplicationWindow {
id: root
width: 400
height: 700
visible: true
title: qsTr("My Tasks")
// Theme properties
property bool darkMode: false
property color backgroundColor: darkMode ? "#1e1e1e" : "#f0f2f5"
property color primaryColor: "#007aff"
property color textColor: darkMode ? "#ffffff" : "#333333"
property color cardColor: darkMode ? "#2d2d2d" : "#ffffff"
property color completedColor: darkMode ? "#666666" : "#999999"
//Change 1
// Task model - Using the dedicated TaskListModel
/*
TaskListModel {
id: taskModel
}
*/
// Task model - Using the dedicated TaskListModel
TaskListModel {
id: taskModel
// Connect to storage ready signal to load dark mode
onStorageInitialized: {
root.darkMode = loadDarkMode()
}
}
// Save dark mode preference when changed
onDarkModeChanged: {
taskModel.saveDarkMode(darkMode)
}
// Background
Rectangle {
anchors.fill: parent
color: root.backgroundColor
}
//The components of the ui
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 20
// Header
AppHeader {
id: header
Layout.fillWidth: true
textColor: root.textColor
textSecondary: root.completedColor
darkMode: root.darkMode
onToggleDarkMode: root.darkMode = !root.darkMode
}
// Add task bar
AddTaskBar {
id: addTaskBar
Layout.fillWidth: true
backgroundColor: root.cardColor
textColor: root.textColor
primaryColor: root.primaryColor
darkMode: root.darkMode
onTaskAdded: function(taskText) {
taskModel.addTask(taskText)
}
}
// Tasks list
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: root.cardColor
radius: 12
border.width: 1
border.color: root.darkMode ? "#404040" : "#e0e0e0"
ScrollView {
anchors.fill: parent
anchors.margins: 1
clip: true
ListView {
id: taskListView
//Change 2
//model: taskModel
model: taskModel.model
spacing: 8
anchors.margins: 12
// Add animation transitions
add: Transition {
NumberAnimation {
properties: "x"
from: 100; to: 0
duration: 300
easing.type: Easing.OutCubic
}
NumberAnimation {
property: "opacity"
from: 0; to: 1
duration: 300
}
}
remove: Transition {
NumberAnimation {
properties: "x"
to: 100
duration: 250
easing.type: Easing.InCubic
}
NumberAnimation {
property: "opacity"
to: 0
duration: 250
}
}
displaced: Transition {
NumberAnimation {
properties: "x,y"
duration: 200
easing.type: Easing.OutQuad
}
}
delegate: TaskItem {
required property int index
required property string title
required property bool completed
width: taskListView.width
taskTitle: title
taskDone: completed
backgroundColor: root.cardColor
textColor: root.textColor
completedColor: root.completedColor
primaryColor: root.primaryColor
darkMode: root.darkMode
onToggleDone: {
taskModel.toggleTask(index)
}
onDeleteTask: {
taskModel.deleteTask(index)
}
}
// Empty state
Rectangle {
anchors.centerIn: parent
width: parent.width - 40
height: 120
color: "transparent"
visible: taskListView.count === 0
opacity: taskListView.count === 0 ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation { duration: 400; easing.type: Easing.InOutQuad }
}
Column {
anchors.centerIn: parent
spacing: 16
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "📝"
font.pixelSize: 48
opacity: 0.3
// Subtle floating animation
SequentialAnimation on y {
running: taskListView.count === 0
loops: Animation.Infinite
NumberAnimation { from: 0; to: -5; duration: 1000; easing.type: Easing.InOutSine }
NumberAnimation { from: -5; to: 0; duration: 1000; easing.type: Easing.InOutSine }
}
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("No tasks yet")
color: root.completedColor
font.pixelSize: 18
font.weight: Font.Medium
// Gentle opacity breathing animation
SequentialAnimation on opacity {
running: taskListView.count === 0
loops: Animation.Infinite
NumberAnimation { from: 0.7; to: 1.0; duration: 1500; easing.type: Easing.InOutSine }
NumberAnimation { from: 1.0; to: 0.7; duration: 1500; easing.type: Easing.InOutSine }
}
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Add a task above to get started")
color: root.completedColor
font.pixelSize: 14
horizontalAlignment: Text.AlignHCenter
// Gentle opacity breathing animation with slight delay
SequentialAnimation on opacity {
running: taskListView.count === 0
loops: Animation.Infinite
PauseAnimation { duration: 300 } // Small delay for staggered effect
NumberAnimation { from: 0.6; to: 0.9; duration: 1500; easing.type: Easing.InOutSine }
NumberAnimation { from: 0.9; to: 0.6; duration: 1500; easing.type: Easing.InOutSine }
}
}
}
}
}
}
}
// Task statistics at bottom
TaskStats {
id: taskStats
Layout.fillWidth: true
backgroundColor: root.cardColor
textColor: root.textColor
secondaryTextColor: root.completedColor
primaryColor: root.primaryColor
darkMode: root.darkMode
totalTasks: taskModel.count
completedTasks: taskModel.getStats().completed
remainingTasks: taskModel.getStats().remaining
hasCompleted: taskModel.hasCompletedTasks()
onClearCompleted: {
taskModel.clearCompletedTasks()
}
}
}
}