diff --git a/tasks-app-shared/schemas/net.opatry.tasks.data.TasksAppDatabase/3.json b/tasks-app-shared/schemas/net.opatry.tasks.data.TasksAppDatabase/3.json
new file mode 100644
index 00000000..0284e20b
--- /dev/null
+++ b/tasks-app-shared/schemas/net.opatry.tasks.data.TasksAppDatabase/3.json
@@ -0,0 +1,194 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 3,
+ "identityHash": "17de8549f80d34ddcf3b7baff29a9f31",
+ "entities": [
+ {
+ "tableName": "task_list",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `remote_id` TEXT, `etag` TEXT NOT NULL DEFAULT '', `title` TEXT NOT NULL, `update_date` TEXT NOT NULL, `sorting` TEXT NOT NULL DEFAULT 'UserDefined')",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "local_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "remoteId",
+ "columnName": "remote_id",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "etag",
+ "columnName": "etag",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastUpdateDate",
+ "columnName": "update_date",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sorting",
+ "columnName": "sorting",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "'UserDefined'"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "local_id"
+ ]
+ }
+ },
+ {
+ "tableName": "task",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `remote_id` TEXT, `parent_list_local_id` INTEGER NOT NULL, `etag` TEXT NOT NULL DEFAULT '', `title` TEXT NOT NULL, `due_date` TEXT, `update_date` TEXT NOT NULL, `completion_date` TEXT, `notes` TEXT NOT NULL DEFAULT '', `is_completed` INTEGER NOT NULL, `position` TEXT NOT NULL, `parent_local_id` INTEGER, `remote_parent_id` TEXT)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "local_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "remoteId",
+ "columnName": "remote_id",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "parentListLocalId",
+ "columnName": "parent_list_local_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "etag",
+ "columnName": "etag",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "dueDate",
+ "columnName": "due_date",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "lastUpdateDate",
+ "columnName": "update_date",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "completionDate",
+ "columnName": "completion_date",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "notes",
+ "columnName": "notes",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "isCompleted",
+ "columnName": "is_completed",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "parentTaskLocalId",
+ "columnName": "parent_local_id",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "parentTaskRemoteId",
+ "columnName": "remote_parent_id",
+ "affinity": "TEXT"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "local_id"
+ ]
+ }
+ },
+ {
+ "tableName": "user",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `remote_id` TEXT, `name` TEXT NOT NULL, `email` TEXT, `avatar_url` TEXT, `is_signed_in` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "remoteId",
+ "columnName": "remote_id",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "email",
+ "columnName": "email",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "avatarUrl",
+ "columnName": "avatar_url",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "isSignedIn",
+ "columnName": "is_signed_in",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ }
+ }
+ ],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '17de8549f80d34ddcf3b7baff29a9f31')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/tasks-app-shared/src/commonMain/composeResources/values-fr/strings.xml b/tasks-app-shared/src/commonMain/composeResources/values-fr/strings.xml
index 6584584c..afa78089 100644
--- a/tasks-app-shared/src/commonMain/composeResources/values-fr/strings.xml
+++ b/tasks-app-shared/src/commonMain/composeResources/values-fr/strings.xml
@@ -77,6 +77,8 @@
Hier
Aujourd'hui
Demain
+ Passée
+ Sans date
Mettre à jour
Modifier la tâche
diff --git a/tasks-app-shared/src/commonMain/composeResources/values/strings.xml b/tasks-app-shared/src/commonMain/composeResources/values/strings.xml
index 5ccc15c1..69f6d830 100644
--- a/tasks-app-shared/src/commonMain/composeResources/values/strings.xml
+++ b/tasks-app-shared/src/commonMain/composeResources/values/strings.xml
@@ -77,6 +77,8 @@
Yesterday
Today
Tomorrow
+ Past
+ No date
Update
Edit task
diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/TaskListsViewModel.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/TaskListsViewModel.kt
index fc8b4120..0cb54278 100644
--- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/TaskListsViewModel.kt
+++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/TaskListsViewModel.kt
@@ -37,8 +37,11 @@ import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.atStartOfDayIn
import kotlinx.datetime.toLocalDateTime
+import net.opatry.tasks.app.ui.model.DateRange
import net.opatry.tasks.app.ui.model.TaskListUIModel
import net.opatry.tasks.app.ui.model.TaskUIModel
+import net.opatry.tasks.app.ui.model.compareTo
+import net.opatry.tasks.data.TaskListSorting
import net.opatry.tasks.data.TaskRepository
import net.opatry.tasks.data.model.TaskDataModel
import net.opatry.tasks.data.model.TaskListDataModel
@@ -46,13 +49,29 @@ import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
private fun TaskListDataModel.asTaskListUIModel(): TaskListUIModel {
- // TODO children
- // TODO date formatter
+ val (completedTasks, remainingTasks) = tasks.map(TaskDataModel::asTaskUIModel).partition(TaskUIModel::isCompleted)
+
+ val taskGroups = when (sorting) {
+ // no grouping
+ TaskListSorting.Manual -> mapOf(null to remainingTasks)
+ TaskListSorting.DueDate -> remainingTasks
+ .sortedWith { o1, o2 -> o1.dateRange.compareTo(o2.dateRange) }
+ .groupBy { task ->
+ when (task.dateRange) {
+ // merge all overdue tasks to the same range
+ is DateRange.Overdue -> DateRange.Overdue(LocalDate.fromEpochDays(-1), 1)
+ else -> task.dateRange
+ }
+ }
+ }
+
return TaskListUIModel(
id = id,
title = title,
lastUpdate = lastUpdate.toString(),
- tasks = tasks.map(TaskDataModel::asTaskUIModel)
+ remainingTasks = taskGroups.toMap(),
+ completedTasks = completedTasks,
+ sorting = sorting,
)
}
@@ -141,6 +160,17 @@ class TaskListsViewModel(
}
}
+ fun sortBy(taskList: TaskListUIModel, sorting: TaskListSorting) {
+ viewModelScope.launch {
+ try {
+ taskRepository.sortTasksBy(taskList.id, sorting)
+ } catch (e: Exception) {
+ println("Error while sorting task list: $e")
+ // TODO error handling
+ }
+ }
+ }
+
fun createTask(taskList: TaskListUIModel, title: String, notes: String = "", dueDate: LocalDate? = null) {
viewModelScope.launch {
try {
diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskListMenu.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskListMenu.kt
index 7ef77256..fed5793d 100644
--- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskListMenu.kt
+++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskListMenu.kt
@@ -51,6 +51,7 @@ import androidx.compose.ui.unit.dp
import net.opatry.tasks.app.ui.model.TaskListUIModel
import net.opatry.tasks.app.ui.tooling.TaskfolioPreview
import net.opatry.tasks.app.ui.tooling.TaskfolioThemedPreview
+import net.opatry.tasks.data.TaskListSorting
import net.opatry.tasks.resources.Res
import net.opatry.tasks.resources.task_list_menu_clear_all_completed_tasks
import net.opatry.tasks.resources.task_list_menu_default_list_cannot_be_deleted
@@ -71,7 +72,11 @@ enum class TaskListMenuAction {
}
@Composable
-fun TaskListMenu(taskList: TaskListUIModel, expanded: Boolean, onAction: (TaskListMenuAction) -> Unit) {
+fun TaskListMenu(
+ taskList: TaskListUIModel,
+ expanded: Boolean,
+ onAction: (TaskListMenuAction) -> Unit
+) {
val allowDelete by remember(taskList.canDelete) { mutableStateOf(taskList.canDelete) }
DropdownMenu(
@@ -90,9 +95,9 @@ fun TaskListMenu(taskList: TaskListUIModel, expanded: Boolean, onAction: (TaskLi
text = {
RowWithIcon(
stringResource(Res.string.task_list_menu_sort_manual),
- LucideIcons.Check.takeIf { false/*taskList.sorting == TaskListSorting.Manual*/ })
+ LucideIcons.Check.takeIf { taskList.sorting == TaskListSorting.Manual })
},
- enabled = false, // TODO enable when sorting is implemented
+ enabled = taskList.sorting != TaskListSorting.Manual,
onClick = { onAction(TaskListMenuAction.SortManual) }
)
@@ -100,9 +105,9 @@ fun TaskListMenu(taskList: TaskListUIModel, expanded: Boolean, onAction: (TaskLi
text = {
RowWithIcon(
stringResource(Res.string.task_list_menu_sort_due_date),
- LucideIcons.Check.takeIf { false/*taskList.sorting == TaskListSorting.Date*/ })
+ LucideIcons.Check.takeIf { taskList.sorting == TaskListSorting.DueDate })
},
- enabled = false, // TODO enable when sorting is implemented
+ enabled = taskList.sorting != TaskListSorting.DueDate,
onClick = { onAction(TaskListMenuAction.SortDate) }
)
@@ -161,7 +166,7 @@ private fun TaskListMenuPreview() {
) {
IconButton(onClick = { showMenu = true }) {
Icon(LucideIcons.EllipsisVertical, null)
- TaskListMenu(TaskListUIModel(0L, "My task list", "TODO DATE", tasks = emptyList()), showMenu) {}
+ TaskListMenu(TaskListUIModel(0L, "My task list", "TODO DATE"), showMenu) {}
}
}
}
diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskMenu.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskMenu.kt
index e3bdffe3..7491160a 100644
--- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskMenu.kt
+++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskMenu.kt
@@ -36,10 +36,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
import androidx.compose.ui.text.style.TextOverflow
import net.opatry.tasks.app.ui.model.TaskListUIModel
import net.opatry.tasks.app.ui.model.TaskUIModel
@@ -53,33 +49,38 @@ import net.opatry.tasks.resources.task_menu_new_list
import net.opatry.tasks.resources.task_menu_unindent
import org.jetbrains.compose.resources.stringResource
-sealed class TaskMenuAction {
- data object Dismiss : TaskMenuAction()
- data object AddSubTask : TaskMenuAction()
- data object MoveToTop : TaskMenuAction()
- data object Unindent : TaskMenuAction()
- data object Indent : TaskMenuAction()
- data class MoveToList(val targetParentList: TaskListUIModel) : TaskMenuAction()
- data object MoveToNewList : TaskMenuAction()
- data object Delete : TaskMenuAction()
+sealed class TaskAction {
+ data object ToggleCompletion : TaskAction()
+ data object Edit : TaskAction()
+ data object UpdateDueDate : TaskAction()
+ data object AddSubTask : TaskAction()
+ data object MoveToTop : TaskAction()
+ data object Unindent : TaskAction()
+ data object Indent : TaskAction()
+ data class MoveToList(val targetParentList: TaskListUIModel) : TaskAction()
+ data object MoveToNewList : TaskAction()
+ data object Delete : TaskAction()
}
@Composable
-fun TaskMenu(taskLists: List, task: TaskUIModel, expanded: Boolean, onAction: (TaskMenuAction) -> Unit) {
- val currentTaskList = taskLists.firstOrNull { it.tasks.map(TaskUIModel::id).contains(task.id) }
- val taskPosition by remember(currentTaskList) { mutableStateOf(currentTaskList?.tasks?.indexOf(task) ?: -1) }
- val canMoveToTop by remember(task) { derivedStateOf { taskPosition > 0 && task.canIndent } }
+fun TaskMenu(
+ taskLists: List,
+ task: TaskUIModel,
+ expanded: Boolean,
+ onAction: (TaskAction?) -> Unit
+) {
+ val currentTaskList = taskLists.firstOrNull { it.containsTask(task) }
DropdownMenu(
expanded = expanded,
- onDismissRequest = { onAction(TaskMenuAction.Dismiss) }
+ onDismissRequest = { onAction(null) }
) {
- if (canMoveToTop) {
+ if (task.canMoveToTop) {
DropdownMenuItem(
text = {
RowWithIcon(stringResource(Res.string.task_menu_move_to_top))
},
- onClick = { onAction(TaskMenuAction.MoveToTop) },
+ onClick = { onAction(TaskAction.MoveToTop) },
enabled = false
)
}
@@ -89,17 +90,17 @@ fun TaskMenu(taskLists: List, task: TaskUIModel, expanded: Bool
text = {
RowWithIcon(stringResource(Res.string.task_menu_add_subtask), LucideIcons.SquareStack)
},
- onClick = { onAction(TaskMenuAction.AddSubTask) },
+ onClick = { onAction(TaskAction.AddSubTask) },
enabled = false
)
}
- if (task.canIndent && taskPosition > 0) {
+ if (task.canIndent) {
DropdownMenuItem(
text = {
RowWithIcon(stringResource(Res.string.task_menu_indent))
},
- onClick = { onAction(TaskMenuAction.Indent) },
+ onClick = { onAction(TaskAction.Indent) },
enabled = false
)
}
@@ -109,7 +110,7 @@ fun TaskMenu(taskLists: List, task: TaskUIModel, expanded: Bool
text = {
RowWithIcon(stringResource(Res.string.task_menu_unindent))
},
- onClick = { onAction(TaskMenuAction.Unindent) },
+ onClick = { onAction(TaskAction.Unindent) },
enabled = false
)
}
@@ -128,7 +129,7 @@ fun TaskMenu(taskLists: List, task: TaskUIModel, expanded: Bool
text = {
RowWithIcon(stringResource(Res.string.task_menu_new_list), LucideIcons.ListPlus)
},
- onClick = { onAction(TaskMenuAction.MoveToNewList) },
+ onClick = { onAction(TaskAction.MoveToNewList) },
enabled = false,
)
@@ -147,7 +148,7 @@ fun TaskMenu(taskLists: List, task: TaskUIModel, expanded: Bool
}
},
enabled = taskList.id != currentTaskList?.id,
- onClick = { onAction(TaskMenuAction.MoveToList(taskList)) }
+ onClick = { onAction(TaskAction.MoveToList(taskList)) }
)
}
}
@@ -160,7 +161,7 @@ fun TaskMenu(taskLists: List, task: TaskUIModel, expanded: Bool
RowWithIcon(stringResource(Res.string.task_menu_delete), LucideIcons.Trash2)
}
},
- onClick = { onAction(TaskMenuAction.Delete) }
+ onClick = { onAction(TaskAction.Delete) }
)
}
}
\ No newline at end of file
diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/model/TaskListUIModel.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/model/TaskListUIModel.kt
index 18639360..61d317ec 100644
--- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/model/TaskListUIModel.kt
+++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/model/TaskListUIModel.kt
@@ -22,18 +22,24 @@
package net.opatry.tasks.app.ui.model
-enum class TaskListSorting {
- Manual,
- Date,
-}
+import net.opatry.tasks.data.TaskListSorting
+
data class TaskListUIModel(
val id: Long,
val title: String,
val lastUpdate: String,
- val tasks: List,
+ val remainingTasks: Map> = emptyMap(),
+ val completedTasks: List = emptyList(),
+ val sorting: TaskListSorting = TaskListSorting.Manual,
) {
- val isEmpty: Boolean = tasks.isEmpty()
- val hasCompletedTasks: Boolean = tasks.any { it.isCompleted }
+ fun containsTask(task: TaskUIModel, includeCompleted: Boolean = false): Boolean {
+ return remainingTasks.values.flatten().contains(task)
+ || (includeCompleted && completedTasks.contains(task))
+ }
+
+ val isEmpty: Boolean = remainingTasks.isEmpty() && completedTasks.isEmpty()
+ val hasCompletedTasks: Boolean = completedTasks.isNotEmpty()
+ val isEmptyRemainingTasksVisible: Boolean = remainingTasks.isEmpty() && hasCompletedTasks
val canDelete: Boolean = true // FIXME default list can't be deleted, how to know it?
}
diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/model/TaskUIModel.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/model/TaskUIModel.kt
index 0b2a8ee8..d6448613 100644
--- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/model/TaskUIModel.kt
+++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/model/TaskUIModel.kt
@@ -29,18 +29,33 @@ import kotlinx.datetime.daysUntil
import kotlinx.datetime.toLocalDateTime
sealed class DateRange {
+ open val date: LocalDate? = null
+ open val numberOfDays: Int? = null
data object None : DateRange()
- data class Overdue(val date: LocalDate, val numberOfDays: Int) : DateRange()
- data object Yesterday : DateRange()
- data object Today : DateRange()
- data object Tomorrow : DateRange()
- data class Later(val date: LocalDate, val numberOfDays: Int) : DateRange()
+ data class Overdue(override val date: LocalDate, override val numberOfDays: Int) : DateRange()
+ data class Today(override val date: LocalDate) : DateRange() {
+ override val numberOfDays: Int = 0
+ }
+ data class Later(override val date: LocalDate, override val numberOfDays: Int) : DateRange()
+}
+
+operator fun DateRange.compareTo(other: DateRange): Int {
+ // local variable for non null smart cast convenience
+ val lhsNumberOfDays = this.numberOfDays
+ val rhsNumberOfDays = other.numberOfDays
+ // No date should come last
+ return when {
+ lhsNumberOfDays == null -> 1
+ rhsNumberOfDays == null -> -1
+ else -> lhsNumberOfDays.compareTo(rhsNumberOfDays)
+ }
}
data class TaskUIModel(
val id: Long,
val title: String,
val dueDate: LocalDate? = null,
+ val completionDate: LocalDate? = null,
val notes: String = "",
val isCompleted: Boolean = false,
val position: String = "", // FIXME for debug?
@@ -56,16 +71,14 @@ data class TaskUIModel(
val daysUntilDueDate = todayLocalDate.daysUntil(dueLocalDate)
return when {
- daysUntilDueDate < -1 -> DateRange.Overdue(dueLocalDate, -daysUntilDueDate)
- daysUntilDueDate == -1 -> DateRange.Yesterday
- daysUntilDueDate == 0 -> DateRange.Today
- daysUntilDueDate == 1 -> DateRange.Tomorrow
- // daysUntilDueDate > 1
- else -> DateRange.Later(dueLocalDate, daysUntilDueDate)
+ daysUntilDueDate < 0 -> DateRange.Overdue(dueLocalDate, daysUntilDueDate)
+ daysUntilDueDate > 0 -> DateRange.Later(dueLocalDate, daysUntilDueDate)
+ else -> DateRange.Today(dueLocalDate)
}
}
+ val canMoveToTop: Boolean = false // TODO not in first position in list
val canUnindent: Boolean = indent > 0
- val canIndent: Boolean = indent < 1
+ val canIndent: Boolean = indent < 1 // TODO & not first position in list
val canCreateSubTask: Boolean = indent == 0
}
diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/taskListsPane.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/taskListsPane.kt
index 357c5c1c..a125760b 100644
--- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/taskListsPane.kt
+++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/taskListsPane.kt
@@ -135,7 +135,6 @@ private fun TaskListRowScaffold(
id = 0L,
title = title,
"TODO DATE",
- tasks = emptyList(),
),
isSelected = isSelected,
onClick = {}
diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/tasksPane.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/tasksPane.kt
index 37a596e8..70bb570d 100644
--- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/tasksPane.kt
+++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/tasksPane.kt
@@ -118,18 +118,21 @@ import net.opatry.tasks.app.ui.component.EditTextDialog
import net.opatry.tasks.app.ui.component.EmptyState
import net.opatry.tasks.app.ui.component.MissingScreen
import net.opatry.tasks.app.ui.component.RowWithIcon
+import net.opatry.tasks.app.ui.component.TaskAction
import net.opatry.tasks.app.ui.component.TaskListMenu
import net.opatry.tasks.app.ui.component.TaskListMenuAction
import net.opatry.tasks.app.ui.component.TaskMenu
-import net.opatry.tasks.app.ui.component.TaskMenuAction
import net.opatry.tasks.app.ui.model.DateRange
import net.opatry.tasks.app.ui.model.TaskListUIModel
import net.opatry.tasks.app.ui.model.TaskUIModel
import net.opatry.tasks.app.ui.tooling.TaskfolioPreview
import net.opatry.tasks.app.ui.tooling.TaskfolioThemedPreview
+import net.opatry.tasks.data.TaskListSorting
import net.opatry.tasks.resources.Res
import net.opatry.tasks.resources.dialog_cancel
import net.opatry.tasks.resources.task_due_date_label_days_ago
+import net.opatry.tasks.resources.task_due_date_label_no_date
+import net.opatry.tasks.resources.task_due_date_label_past
import net.opatry.tasks.resources.task_due_date_label_today
import net.opatry.tasks.resources.task_due_date_label_tomorrow
import net.opatry.tasks.resources.task_due_date_label_weeks_ago
@@ -164,6 +167,8 @@ import net.opatry.tasks.resources.task_list_pane_task_restored_snackbar
import net.opatry.tasks.resources.task_lists_screen_empty_list_desc
import net.opatry.tasks.resources.task_lists_screen_empty_list_title
import org.jetbrains.compose.resources.stringResource
+import kotlin.math.abs
+
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -231,8 +236,8 @@ fun TaskListDetail(
showTaskListActions = false
when (action) {
TaskListMenuAction.Dismiss -> Unit
- TaskListMenuAction.SortManual -> {}
- TaskListMenuAction.SortDate -> {}
+ TaskListMenuAction.SortManual -> viewModel.sortBy(taskList, TaskListSorting.Manual)
+ TaskListMenuAction.SortDate -> viewModel.sortBy(taskList, TaskListSorting.DueDate)
TaskListMenuAction.Rename -> showRenameTaskListDialog = true
TaskListMenuAction.ClearCompletedTasks -> showClearTaskListCompletedTasksDialog = true
TaskListMenuAction.Delete -> showDeleteTaskListDialog = true
@@ -583,25 +588,12 @@ fun TasksColumn(
onDeleteTask: (TaskUIModel) -> Unit,
) {
var showCompleted by remember(taskList.id) { mutableStateOf(false) }
- val tasks = taskList.tasks
-
- // TODO depending on sorting (manual vs date), sections could be different
- // manual: no section title for not completed tasks, expandable "completed" section
- // date: sections by date cluster (past, today, tomorrow, future), expandable "completed" section
-
- // FIXME remember computation & derived states
- val groupedTasks = tasks.sortedBy { it.isCompleted }.groupBy { it.isCompleted }.toMutableMap()
- val todoCount = groupedTasks[false]?.size ?: 0
- val completedCount = groupedTasks[true]?.size ?: 0
- if (!showCompleted) {
- groupedTasks[true] = emptyList()
- }
LazyColumn(
contentPadding = PaddingValues(vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
- if (completedCount > 0 && todoCount == 0) {
+ if (taskList.isEmptyRemainingTasksVisible) {
item(key = "all_tasks_complete") {
EmptyState(
icon = LucideIcons.CheckCheck,
@@ -611,48 +603,86 @@ fun TasksColumn(
)
}
}
- groupedTasks.forEach { (completed, tasks) ->
- if (completed && completedCount > 0) {
- stickyHeader(key = "completed") {
+
+ taskList.remainingTasks.forEach { (dateRange, tasks) ->
+ if (dateRange != null) {
+ stickyHeader(key = dateRange) {
Box(
Modifier
- .clip(MaterialTheme.shapes.large)
.fillMaxWidth()
- .clickable { showCompleted = !showCompleted }
.background(MaterialTheme.colorScheme.background)
.padding(horizontal = 12.dp, vertical = 8.dp)
) {
- RowWithIcon(
- icon = {
- if (showCompleted) {
- Icon(LucideIcons.ChevronDown, null)
- } else {
- Icon(LucideIcons.ChevronRight, null)
- }
- }
- ) {
- Text(
- stringResource(Res.string.task_list_pane_completed_section_title_with_count, completedCount),
- style = MaterialTheme.typography.titleSmall
- )
- }
+ Text(
+ dateRange.toLabel(sectionLabel = true),
+ style = MaterialTheme.typography.titleSmall,
+ color = dateRange.toColor(),
+ )
}
}
}
items(tasks, key = TaskUIModel::id) { task ->
- TaskRow(
+ RemainingTaskRow(
taskLists,
task,
- onToggleCompletionState = { onToggleCompletionState(task) },
- onEditTask = { onEditTask(task) },
- onUpdateDueDate = { onUpdateDueDate(task) },
- onNewSubTask = { onNewSubTask(task) },
- onUnindent = { onUnindent(task) },
- onIndent = { onIndent(task) },
- onMoveToTop = { onMoveToTop(task) },
- onMoveToList = { onMoveToList(task, it) },
- onMoveToNewList = { onMoveToNewList(task) },
- onDeleteTask = { onDeleteTask(task) },
+ showDate = taskList.sorting == TaskListSorting.Manual || dateRange is DateRange.Overdue
+ ) { action ->
+ when (action) {
+ TaskAction.ToggleCompletion -> onToggleCompletionState(task)
+ TaskAction.Edit -> onEditTask(task)
+ TaskAction.UpdateDueDate -> onUpdateDueDate(task)
+ TaskAction.AddSubTask -> onNewSubTask(task)
+ TaskAction.Unindent -> onUnindent(task)
+ TaskAction.Indent -> onIndent(task)
+ TaskAction.MoveToTop -> onMoveToTop(task)
+ is TaskAction.MoveToList -> onMoveToList(task, action.targetParentList)
+ TaskAction.MoveToNewList -> onMoveToNewList(task)
+ TaskAction.Delete -> onDeleteTask(task)
+ }
+ }
+ }
+ }
+
+ if (taskList.hasCompletedTasks) {
+ stickyHeader(key = "completed") {
+ Box(
+ Modifier
+ .clip(MaterialTheme.shapes.large)
+ .fillMaxWidth()
+ .clickable { showCompleted = !showCompleted }
+ .background(MaterialTheme.colorScheme.background)
+ .padding(horizontal = 12.dp, vertical = 8.dp)
+ ) {
+ RowWithIcon(
+ icon = {
+ when {
+ showCompleted -> Icon(LucideIcons.ChevronDown, null)
+ else -> Icon(LucideIcons.ChevronRight, null)
+ }
+ }
+ ) {
+ Text(
+ stringResource(Res.string.task_list_pane_completed_section_title_with_count, taskList.completedTasks.size),
+ style = MaterialTheme.typography.titleSmall
+ )
+ }
+ }
+ }
+ }
+
+ if (showCompleted) {
+ items(taskList.completedTasks, key = TaskUIModel::id) { task ->
+ CompletedTaskRow(
+ task,
+ onAction = { action ->
+ when (action) {
+ TaskAction.ToggleCompletion -> onToggleCompletionState(task)
+ TaskAction.Edit -> onEditTask(task)
+ TaskAction.UpdateDueDate -> onUpdateDueDate(task)
+ TaskAction.Delete -> onDeleteTask(task)
+ else -> Unit
+ }
+ },
)
}
}
@@ -661,32 +691,30 @@ fun TasksColumn(
@Composable
fun DateRange?.toColor(): Color = when (this) {
- is DateRange.Overdue,
- DateRange.Yesterday -> MaterialTheme.colorScheme.error
-
- DateRange.Today -> MaterialTheme.colorScheme.primary
- DateRange.Tomorrow,
+ is DateRange.Overdue -> MaterialTheme.colorScheme.error
+ is DateRange.Today -> MaterialTheme.colorScheme.primary
is DateRange.Later,
DateRange.None,
null -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
}
@Composable
-fun DateRange.toLabel(): String = when (this) {
+fun DateRange.toLabel(sectionLabel: Boolean = false): String = when (this) {
is DateRange.Overdue -> {
- if (numberOfDays < 7) {
- stringResource(Res.string.task_due_date_label_days_ago, numberOfDays)
- } else {
- stringResource(Res.string.task_due_date_label_weeks_ago, numberOfDays / 7)
+ val numberOfDays = abs(numberOfDays)
+ when {
+ sectionLabel -> stringResource(Res.string.task_due_date_label_past)
+ numberOfDays == 1 -> stringResource(Res.string.task_due_date_label_yesterday)
+ numberOfDays < 7 -> stringResource(Res.string.task_due_date_label_days_ago, numberOfDays)
+ else -> stringResource(Res.string.task_due_date_label_weeks_ago, numberOfDays / 7)
}
}
- DateRange.Yesterday -> stringResource(Res.string.task_due_date_label_yesterday)
- DateRange.Today -> stringResource(Res.string.task_due_date_label_today)
- DateRange.Tomorrow -> stringResource(Res.string.task_due_date_label_tomorrow)
- // TODO localize names & format
- is DateRange.Later -> LocalDate.Format {
- if (date.year == Clock.System.todayIn(TimeZone.currentSystemDefault()).year) {
+ is DateRange.Today -> stringResource(Res.string.task_due_date_label_today)
+ is DateRange.Later -> when {
+ numberOfDays == 1 -> stringResource(Res.string.task_due_date_label_tomorrow)
+ // TODO localize names & format
+ date.year == Clock.System.todayIn(TimeZone.currentSystemDefault()).year -> LocalDate.Format {
// FIXME doesn't work with more than 2 dd or MM
// byUnicodePattern("ddd', 'MMM' 'yyyy")
dayOfWeek(DayOfWeekNames.ENGLISH_ABBREVIATED) // TODO translation
@@ -694,7 +722,9 @@ fun DateRange.toLabel(): String = when (this) {
monthName(MonthNames.ENGLISH_ABBREVIATED) // TODO translation
char(' ')
dayOfMonth(Padding.NONE)
- } else {
+ }.format(date)
+
+ else -> LocalDate.Format {
// FIXME doesn't work with more than 2 MM
// byUnicodePattern("MMMM' 'dd', 'yyyy")
monthName(MonthNames.ENGLISH_FULL) // TODO translation
@@ -702,41 +732,31 @@ fun DateRange.toLabel(): String = when (this) {
dayOfMonth(Padding.NONE)
chars(", ")
year()
- }
- }.format(date)
+ }.format(date)
+ }
- DateRange.None -> ""
+ DateRange.None -> when {
+ sectionLabel -> stringResource(Res.string.task_due_date_label_no_date)
+ else -> ""
+ }
}
@Composable
-fun TaskRow(
+private fun RemainingTaskRow(
taskLists: List,
task: TaskUIModel,
- onToggleCompletionState: () -> Unit,
- onEditTask: () -> Unit,
- onUpdateDueDate: () -> Unit,
- onNewSubTask: () -> Unit,
- onUnindent: () -> Unit,
- onIndent: () -> Unit,
- onMoveToTop: () -> Unit,
- onMoveToList: (TaskListUIModel) -> Unit,
- onMoveToNewList: () -> Unit,
- onDeleteTask: () -> Unit,
+ modifier: Modifier = Modifier,
+ showDate: Boolean = true,
+ onAction: (TaskAction) -> Unit,
) {
var showContextualMenu by remember { mutableStateOf(false) }
- // TODO remember?
- val (taskCheckIcon, taskCheckIconColor) = when {
- task.isCompleted -> LucideIcons.CircleCheckBig to MaterialTheme.colorScheme.primary
- else -> LucideIcons.Circle to MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
- }
-
- Row(Modifier.clickable(onClick = onEditTask)) {
+ Row(modifier.clickable(onClick = { onAction(TaskAction.Edit) })) {
IconButton(
- onClick = onToggleCompletionState,
- Modifier.padding(start = 36.dp * task.indent)
+ onClick = { onAction(TaskAction.ToggleCompletion) },
+ modifier = Modifier.padding(start = 36.dp * task.indent)
) {
- Icon(taskCheckIcon, null, tint = taskCheckIconColor)
+ Icon(LucideIcons.Circle, null, tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f))
}
Column(
Modifier
@@ -745,7 +765,6 @@ fun TaskRow(
) {
Text(
task.title,
- textDecoration = TextDecoration.LineThrough.takeIf { task.isCompleted },
style = MaterialTheme.typography.titleMedium,
overflow = TextOverflow.Ellipsis,
maxLines = 1
@@ -759,46 +778,78 @@ fun TaskRow(
maxLines = 2
)
}
- if (task.dueDate != null) {
+ if (showDate && task.dueDate != null) {
AssistChip(
- onClick = onUpdateDueDate,
+ onClick = { onAction(TaskAction.UpdateDueDate) },
shape = MaterialTheme.shapes.large,
label = {
Text(
task.dateRange.toLabel(),
- color = if (task.isCompleted)
- MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
- else
- task.dateRange.toColor()
+ color = task.dateRange.toColor()
)
},
)
}
}
- if (task.isCompleted) {
- IconButton(onClick = onDeleteTask) {
- Icon(LucideIcons.Trash, stringResource(Res.string.task_list_pane_delete_task_icon_content_desc))
+ Box {
+ IconButton(onClick = { showContextualMenu = true }) {
+ Icon(LucideIcons.EllipsisVertical, stringResource(Res.string.task_list_pane_task_options_icon_content_desc))
}
- } else {
- Box {
- IconButton(onClick = { showContextualMenu = true }) {
- Icon(LucideIcons.EllipsisVertical, stringResource(Res.string.task_list_pane_task_options_icon_content_desc))
- }
- TaskMenu(taskLists, task, showContextualMenu) { action ->
- showContextualMenu = false
- when (action) {
- TaskMenuAction.Dismiss -> Unit
- TaskMenuAction.AddSubTask -> onNewSubTask()
- TaskMenuAction.Indent -> onIndent()
- TaskMenuAction.Unindent -> onUnindent()
- TaskMenuAction.MoveToTop -> onMoveToTop()
- is TaskMenuAction.MoveToList -> onMoveToList(action.targetParentList)
- TaskMenuAction.MoveToNewList -> onMoveToNewList()
- TaskMenuAction.Delete -> onDeleteTask()
- }
- }
+ TaskMenu(taskLists, task, showContextualMenu) { action ->
+ showContextualMenu = false
+ action?.let(onAction)
+ }
+ }
+ }
+}
+
+@Composable
+private fun CompletedTaskRow(
+ task: TaskUIModel,
+ onAction: (TaskAction) -> Unit,
+) {
+ Row(Modifier.clickable(onClick = { onAction(TaskAction.Edit) })) {
+ IconButton(onClick = { onAction(TaskAction.ToggleCompletion) }) {
+ Icon(LucideIcons.CircleCheckBig, null, tint = MaterialTheme.colorScheme.primary)
+ }
+ Column(
+ Modifier
+ .weight(1f)
+ .padding(vertical = 8.dp)
+ ) {
+ Text(
+ task.title,
+ textDecoration = TextDecoration.LineThrough,
+ style = MaterialTheme.typography.titleMedium,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 1
+ )
+
+ if (task.notes.isNotBlank()) {
+ Text(
+ task.notes,
+ style = MaterialTheme.typography.bodySmall,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 2
+ )
+ }
+ if (task.dueDate != null) {
+ AssistChip(
+ onClick = { onAction(TaskAction.UpdateDueDate) },
+ shape = MaterialTheme.shapes.large,
+ label = {
+ Text(
+ task.dateRange.toLabel(),
+ color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
+ )
+ },
+ )
}
}
+
+ IconButton(onClick = { onAction(TaskAction.Delete) }) {
+ Icon(LucideIcons.Trash, stringResource(Res.string.task_list_pane_delete_task_icon_content_desc))
+ }
}
}
@@ -809,7 +860,7 @@ private fun TaskRowScaffold(
dueDate: LocalDate? = Clock.System.todayIn(TimeZone.currentSystemDefault()),
isCompleted: Boolean = false
) {
- TaskRow(
+ RemainingTaskRow(
emptyList(),
TaskUIModel(
id = 0L,
@@ -817,18 +868,8 @@ private fun TaskRowScaffold(
notes = notes,
dueDate = dueDate,
isCompleted = isCompleted
- ),
- {},
- {},
- {},
- {},
- {},
- {},
- {},
- {},
- {},
- {}
- )
+ )
+ ) {}
}
@TaskfolioPreview
diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/data/TasksAppDatabase.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/data/TasksAppDatabase.kt
index 553db297..87f72f55 100644
--- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/data/TasksAppDatabase.kt
+++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/data/TasksAppDatabase.kt
@@ -41,6 +41,12 @@ object Converters {
@TypeConverter
fun instantToString(instant: Instant?): String? = instant?.toString()
+
+ @TypeConverter
+ fun sortingFromString(value: String?): TaskListEntity.Sorting? = value?.let(TaskListEntity.Sorting::valueOf)
+
+ @TypeConverter
+ fun sortingToString(sorting: TaskListEntity.Sorting?): String? = sorting?.name
}
@Database(
@@ -49,9 +55,10 @@ object Converters {
TaskEntity::class,
UserEntity::class,
],
- version = 2,
+ version = 3,
autoMigrations = [
- AutoMigration(from = 1, to = 2)
+ AutoMigration(from = 1, to = 2), // add user table
+ AutoMigration(from = 2, to = 3), // add sorting column in task_list table
],
)
@ConstructedBy(TasksAppDatabaseConstructor::class)
diff --git a/tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/TaskListDao.kt b/tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/TaskListDao.kt
index 78933f83..ad71d1f7 100644
--- a/tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/TaskListDao.kt
+++ b/tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/TaskListDao.kt
@@ -61,4 +61,7 @@ LEFT JOIN task ON task_list.local_id = task.parent_list_local_id ORDER BY task_l
@Query("DELETE FROM task_list WHERE remote_id IS NOT NULL AND remote_id NOT IN (:validRemoteIds)")
suspend fun deleteStaleTaskLists(validRemoteIds: List)
+
+ @Query("UPDATE task_list SET sorting = :sorting WHERE local_id = :taskListId")
+ suspend fun sortTasksBy(taskListId: Long, sorting: TaskListEntity.Sorting)
}
\ No newline at end of file
diff --git a/tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/TaskRepository.kt b/tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/TaskRepository.kt
index aad70b58..effa7e08 100644
--- a/tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/TaskRepository.kt
+++ b/tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/TaskRepository.kt
@@ -42,13 +42,19 @@ import net.opatry.tasks.data.entity.TaskListEntity
import net.opatry.tasks.data.model.TaskDataModel
import net.opatry.tasks.data.model.TaskListDataModel
-private fun TaskList.asTaskListEntity(localId: Long?): TaskListEntity {
+enum class TaskListSorting {
+ Manual,
+ DueDate,
+}
+
+private fun TaskList.asTaskListEntity(localId: Long?, sorting: TaskListEntity.Sorting): TaskListEntity {
return TaskListEntity(
id = localId ?: 0,
remoteId = id,
etag = etag,
title = title,
lastUpdateDate = updatedDate,
+ sorting = sorting,
)
}
@@ -71,14 +77,21 @@ private fun Task.asTaskEntity(parentLocalId: Long, localId: Long?): TaskEntity {
}
private fun TaskListEntity.asTaskListDataModel(tasks: List): TaskListDataModel {
- val sortedTasks = sortTasks(tasks).map { (task, indent) ->
- task.asTaskDataModel(indent)
+ val (sorting, sortedTasks) = when (sorting) {
+ TaskListEntity.Sorting.UserDefined -> TaskListSorting.Manual to sortTasksManualOrdering(tasks).map { (task, indent) ->
+ task.asTaskDataModel(indent)
+ }
+
+ TaskListEntity.Sorting.DueDate -> TaskListSorting.DueDate to sortTasksDateOrdering(tasks).map { task ->
+ task.asTaskDataModel(0)
+ }
}
return TaskListDataModel(
id = id,
title = title,
lastUpdate = lastUpdateDate,
tasks = sortedTasks,
+ sorting = sorting
)
}
@@ -109,7 +122,7 @@ private fun TaskEntity.asTask(): Task {
)
}
-fun sortTasks(tasks: List): List> {
+fun sortTasksManualOrdering(tasks: List): List> {
// Step 1: Create a map of tasks by their IDs for easy lookup
val taskMap = tasks.associateBy { it.remoteId ?: it.id.toString() }.toMutableMap() // FIXME local data only?
@@ -128,7 +141,7 @@ fun sortTasks(tasks: List): List> {
// Step 3: Sort the child tasks by position
tree.forEach { (_, children) ->
- children.sortBy { it.position }
+ children.sortBy(TaskEntity::position)
}
// Step 4: Recursive function to traverse tasks and assign indentation levels
@@ -150,6 +163,10 @@ fun sortTasks(tasks: List): List> {
return sortedTasks
}
+fun sortTasksDateOrdering(tasks: List): List {
+ return tasks.sortedBy(TaskEntity::dueDate)
+}
+
class TaskRepository(
private val taskListDao: TaskListDao,
private val taskDao: TaskDao,
@@ -188,8 +205,8 @@ class TaskRepository(
// - check new ones
// - etc.
val existingEntity = taskListDao.getByRemoteId(remoteTaskList.id)
- val finalLocalId = taskListDao.insert(remoteTaskList.asTaskListEntity(existingEntity?.id))
- println("task list ${remoteTaskList.etag} vs ${existingEntity?.etag}) with final local id $finalLocalId")
+ val updatedEntity = remoteTaskList.asTaskListEntity(existingEntity?.id, existingEntity?.sorting ?: TaskListEntity.Sorting.UserDefined)
+ val finalLocalId = taskListDao.insert(updatedEntity)
taskListIds[finalLocalId] = remoteTaskList.id
}
taskListDao.deleteStaleTaskLists(remoteTaskLists.map(TaskList::id))
@@ -237,7 +254,7 @@ class TaskRepository(
}
}
if (taskList != null) {
- taskListDao.insert(taskList.asTaskListEntity(taskListId))
+ taskListDao.insert(taskList.asTaskListEntity(taskListId, TaskListEntity.Sorting.UserDefined))
}
}
@@ -304,6 +321,15 @@ class TaskRepository(
}
}
+ suspend fun sortTasksBy(taskListId: Long, sorting: TaskListSorting) {
+ val dbSorting = when (sorting) {
+ TaskListSorting.Manual -> TaskListEntity.Sorting.UserDefined
+ TaskListSorting.DueDate -> TaskListEntity.Sorting.DueDate
+ }
+ // no update date change, it's a local only information unrelated to remote tasks
+ taskListDao.sortTasksBy(taskListId, dbSorting)
+ }
+
suspend fun createTask(taskListId: Long, title: String, notes: String = "", dueDate: Instant? = null) {
val now = Clock.System.now()
val taskEntity = TaskEntity(
diff --git a/tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/entity/TaskListEntity.kt b/tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/entity/TaskListEntity.kt
index 25a60fac..1613eeea 100644
--- a/tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/entity/TaskListEntity.kt
+++ b/tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/entity/TaskListEntity.kt
@@ -41,5 +41,12 @@ data class TaskListEntity(
val title: String,
@ColumnInfo(name = "update_date")
val lastUpdateDate: Instant,
-)
+ @ColumnInfo(name = "sorting", defaultValue = "UserDefined") // tightly coupled to converters & enum/string mapping
+ val sorting: Sorting = Sorting.UserDefined,
+) {
+ enum class Sorting {
+ UserDefined,
+ DueDate,
+ }
+}
diff --git a/tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/model/TaskListDataModel.kt b/tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/model/TaskListDataModel.kt
index 5886cdd1..6c2212f6 100644
--- a/tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/model/TaskListDataModel.kt
+++ b/tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/model/TaskListDataModel.kt
@@ -23,10 +23,12 @@
package net.opatry.tasks.data.model
import kotlinx.datetime.Instant
+import net.opatry.tasks.data.TaskListSorting
data class TaskListDataModel(
val id: Long,
val title: String,
val lastUpdate: Instant,
val tasks: List,
+ val sorting: TaskListSorting,
)