Skip to content

Commit

Permalink
Move sorting & grouping logic in proper layer, outside UI
Browse files Browse the repository at this point in the history
  • Loading branch information
opatry committed Oct 15, 2024
1 parent 4a0e583 commit 84ee009
Show file tree
Hide file tree
Showing 8 changed files with 43 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ 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
Expand All @@ -47,13 +49,28 @@ 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,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ private fun TaskListMenuPreview() {
) {
IconButton(onClick = { showMenu = true }) {
Icon(LucideIcons.EllipsisVertical, null)
TaskListMenu(TaskListUIModel(0L, "My task list", "TODO DATE", tasks = emptyList(), sorting = TaskListSorting.Manual), showMenu) {}
TaskListMenu(TaskListUIModel(0L, "My task list", "TODO DATE"), showMenu) {}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -73,15 +69,13 @@ fun TaskMenu(
expanded: Boolean,
onAction: (TaskAction?) -> 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 } }
val currentTaskList = taskLists.firstOrNull { it.containsTask(task) }

DropdownMenu(
expanded = expanded,
onDismissRequest = { onAction(null) }
) {
if (canMoveToTop) {
if (task.canMoveToTop) {
DropdownMenuItem(
text = {
RowWithIcon(stringResource(Res.string.task_menu_move_to_top))
Expand All @@ -101,7 +95,7 @@ fun TaskMenu(
)
}

if (task.canIndent && taskPosition > 0) {
if (task.canIndent) {
DropdownMenuItem(
text = {
RowWithIcon(stringResource(Res.string.task_menu_indent))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,17 @@ data class TaskListUIModel(
val id: Long,
val title: String,
val lastUpdate: String,
val tasks: List<TaskUIModel>,
val sorting: TaskListSorting,
val remainingTasks: Map<DateRange?, List<TaskUIModel>> = emptyMap(),
val completedTasks: List<TaskUIModel> = 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?
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ data class TaskUIModel(
}
}

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
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ import net.opatry.tasks.app.ui.component.RowWithIcon
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_lists_screen_add_task_list_cta
import org.jetbrains.compose.resources.stringResource
Expand Down Expand Up @@ -136,8 +135,6 @@ private fun TaskListRowScaffold(
id = 0L,
title = title,
"TODO DATE",
tasks = emptyList(),
sorting = TaskListSorting.Manual,
),
isSelected = isSelected,
onClick = {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,6 @@ import net.opatry.tasks.app.ui.component.TaskMenu
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.app.ui.tooling.TaskfolioPreview
import net.opatry.tasks.app.ui.tooling.TaskfolioThemedPreview
import net.opatry.tasks.data.TaskListSorting
Expand Down Expand Up @@ -590,30 +589,11 @@ fun TasksColumn(
) {
var showCompleted by remember(taskList.id) { mutableStateOf(false) }

// FIXME remember computation & derived states
val (completedTasks, remainingTasks) = taskList.tasks.partition(TaskUIModel::isCompleted)

val taskGroups = when (taskList.sorting) {
// no grouping
TaskListSorting.Manual -> mapOf(null to remainingTasks)
TaskListSorting.DueDate -> remainingTasks
.map { it.copy(indent = 0) }
.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
}
}
}

LazyColumn(
contentPadding = PaddingValues(vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
if (completedTasks.isNotEmpty() && remainingTasks.isEmpty()) {
if (taskList.isEmptyRemainingTasksVisible) {
item(key = "all_tasks_complete") {
EmptyState(
icon = LucideIcons.CheckCheck,
Expand All @@ -624,7 +604,7 @@ fun TasksColumn(
}
}

taskGroups.forEach { (dateRange, tasks) ->
taskList.remainingTasks.forEach { (dateRange, tasks) ->
if (dateRange != null) {
stickyHeader(key = dateRange) {
Box(
Expand Down Expand Up @@ -663,7 +643,7 @@ fun TasksColumn(
}
}

if (completedTasks.isNotEmpty()) {
if (taskList.hasCompletedTasks) {
stickyHeader(key = "completed") {
Box(
Modifier
Expand All @@ -682,7 +662,7 @@ fun TasksColumn(
}
) {
Text(
stringResource(Res.string.task_list_pane_completed_section_title_with_count, completedTasks.size),
stringResource(Res.string.task_list_pane_completed_section_title_with_count, taskList.completedTasks.size),
style = MaterialTheme.typography.titleSmall
)
}
Expand All @@ -691,7 +671,7 @@ fun TasksColumn(
}

if (showCompleted) {
items(completedTasks, key = TaskUIModel::id) { task ->
items(taskList.completedTasks, key = TaskUIModel::id) { task ->
CompletedTaskRow(
task,
onAction = { action ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ fun sortTasksManualOrdering(tasks: List<TaskEntity>): List<Pair<TaskEntity, Int>

// 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
Expand Down

0 comments on commit 84ee009

Please sign in to comment.