Reverted TaskItemVM to a dumb state-like VM

Task Edit events are now carried to the parent composable
This commit is contained in:
Geoffroy Bonneville
2025-10-10 18:23:07 -04:00
parent 53e716a690
commit 30d3efa9de
9 changed files with 78 additions and 70 deletions

View File

@@ -2,15 +2,15 @@
<project version="4"> <project version="4">
<component name="deploymentTargetSelector"> <component name="deploymentTargetSelector">
<selectionStates> <selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
<SelectionState runConfigName="donextv2"> <SelectionState runConfigName="donextv2">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
</SelectionState> </SelectionState>
<SelectionState runConfigName="overdueCount_correctlyCalculated()"> <SelectionState runConfigName="overdueCount_correctlyCalculated()">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
</SelectionState> </SelectionState>
<SelectionState runConfigName="donext">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates> </selectionStates>
</component> </component>
</project> </project>

View File

@@ -37,7 +37,8 @@ fun DueTodayTasksScreen(
modifier = Modifier.animateItem(), modifier = Modifier.animateItem(),
task = task, task = task,
onSwipeLeft = { viewModel.updateTaskDone(task.id!!) }, onSwipeLeft = { viewModel.updateTaskDone(task.id!!) },
onSwipeRight = { viewModel.deleteTask(task.id!!) } onSwipeRight = { viewModel.deleteTask(task.id!!) },
onTaskClick = { viewModel.onTaskClicked(task) }
) )
} }
} }

View File

@@ -80,7 +80,8 @@ fun RecycleBinScreen(
// TODO: add confirmation dialog // TODO: add confirmation dialog
viewModel.deleteForever(item.task.id!!) viewModel.deleteForever(item.task.id!!)
Toast.makeText(context, "Task deleted", Toast.LENGTH_SHORT).show() Toast.makeText(context, "Task deleted", Toast.LENGTH_SHORT).show()
} },
onTaskClick = { viewModel.onTaskClicked(item.task) }
) )
} }
} }

View File

@@ -28,6 +28,8 @@ import androidx.compose.material3.SwipeToDismissBoxValue
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.rememberSwipeToDismissBoxState import androidx.compose.material3.rememberSwipeToDismissBoxState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
@@ -37,33 +39,42 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.wismna.geoffroy.donext.domain.model.Priority import com.wismna.geoffroy.donext.domain.model.Priority
import com.wismna.geoffroy.donext.domain.model.Task import com.wismna.geoffroy.donext.domain.model.Task
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskItemViewModel import com.wismna.geoffroy.donext.presentation.viewmodel.TaskItemViewModel
import kotlinx.coroutines.flow.filter
@Composable @Composable
fun TaskItemScreen( fun TaskItemScreen(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
task: Task, task: Task,
viewModel: TaskItemViewModel = hiltViewModel<TaskItemViewModel>(),
onSwipeLeft: () -> Unit, onSwipeLeft: () -> Unit,
onSwipeRight: () -> Unit onSwipeRight: () -> Unit,
onTaskClick: (task: Task) -> Unit
) { ) {
viewModel.populateTask(task) val viewModel = TaskItemViewModel(task)
// TODO: change this
val dismissState = rememberSwipeToDismissBoxState( val dismissState = rememberSwipeToDismissBoxState(
confirmValueChange = {
when (it) {
SwipeToDismissBoxValue.StartToEnd -> { onSwipeRight() }
SwipeToDismissBoxValue.EndToStart -> { onSwipeLeft() }
SwipeToDismissBoxValue.Settled -> return@rememberSwipeToDismissBoxState false
}
return@rememberSwipeToDismissBoxState true
},
// positional threshold of 25% // positional threshold of 25%
positionalThreshold = { it * .25f } positionalThreshold = { it * .25f }
) )
LaunchedEffect(dismissState) {
snapshotFlow { dismissState.targetValue }
.filter { it != SwipeToDismissBoxValue.Settled }
.collect { target ->
when (target) {
SwipeToDismissBoxValue.StartToEnd -> {
onSwipeRight()
dismissState.reset()
}
SwipeToDismissBoxValue.EndToStart -> {
onSwipeLeft()
}
else -> Unit
}
}
}
val baseStyle = MaterialTheme.typography.bodyLarge.copy( val baseStyle = MaterialTheme.typography.bodyLarge.copy(
fontWeight = when (viewModel.priority) { fontWeight = when (viewModel.priority) {
Priority.HIGH -> FontWeight.Bold Priority.HIGH -> FontWeight.Bold
@@ -79,7 +90,7 @@ fun TaskItemScreen(
) )
Card( Card(
modifier = modifier, modifier = modifier,
onClick = { viewModel.onTaskClicked(task) }, onClick = { onTaskClick(task) },
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainer, containerColor = MaterialTheme.colorScheme.surfaceContainer,
), ),
@@ -110,7 +121,7 @@ fun TaskItemScreen(
) { ) {
// Title // Title
Text( Text(
text = viewModel.name!!, text = viewModel.name,
fontSize = 18.sp, fontSize = 18.sp,
style = baseStyle, style = baseStyle,
modifier = Modifier modifier = Modifier
@@ -123,7 +134,7 @@ fun TaskItemScreen(
) )
// Due date badge // Due date badge
viewModel.dueDate?.let { dueMillis -> viewModel.dueDateText?.let { dueMillis ->
Badge( Badge(
modifier = Modifier modifier = Modifier
.align( .align(
@@ -134,7 +145,7 @@ fun TaskItemScreen(
) { ) {
Text( Text(
modifier = Modifier.padding(start = 1.dp, end = 1.dp), modifier = Modifier.padding(start = 1.dp, end = 1.dp),
text = viewModel.dueDate!!, text = viewModel.dueDateText,
color = if (viewModel.isOverdue) Color.White else MaterialTheme.colorScheme.onPrimaryContainer, color = if (viewModel.isOverdue) Color.White else MaterialTheme.colorScheme.onPrimaryContainer,
style = MaterialTheme.typography.bodySmall style = MaterialTheme.typography.bodySmall
) )
@@ -151,7 +162,7 @@ fun TaskItemScreen(
) { ) {
if (!viewModel.description.isNullOrBlank()) { if (!viewModel.description.isNullOrBlank()) {
Text( Text(
text = viewModel.description!!, text = viewModel.description,
color = MaterialTheme.colorScheme.tertiary, color = MaterialTheme.colorScheme.tertiary,
style = baseStyle.copy( style = baseStyle.copy(
fontSize = MaterialTheme.typography.bodyMedium.fontSize, fontSize = MaterialTheme.typography.bodyMedium.fontSize,

View File

@@ -55,7 +55,8 @@ fun TaskListScreen(
modifier = Modifier.animateItem(), modifier = Modifier.animateItem(),
task = task, task = task,
onSwipeLeft = { viewModel.updateTaskDone(task.id!!, true) }, onSwipeLeft = { viewModel.updateTaskDone(task.id!!, true) },
onSwipeRight = { viewModel.deleteTask(task.id!!) } onSwipeRight = { viewModel.deleteTask(task.id!!) },
onTaskClick = { viewModel.onTaskClicked(task) }
) )
} }
@@ -80,8 +81,8 @@ fun TaskListScreen(
task = task, task = task,
onSwipeLeft = { viewModel.updateTaskDone(task.id!!, false) }, onSwipeLeft = { viewModel.updateTaskDone(task.id!!, false) },
onSwipeRight = { viewModel.deleteTask(task.id!!) }, onSwipeRight = { viewModel.deleteTask(task.id!!) },
onTaskClick = { viewModel.onTaskClicked(task) }
) )
} }
} }
} }

View File

@@ -36,6 +36,12 @@ class DueTodayViewModel @Inject constructor(
.launchIn(viewModelScope) .launchIn(viewModelScope)
} }
fun onTaskClicked(task: Task) {
viewModelScope.launch {
uiEventBus.send(UiEvent.EditTask(task))
}
}
fun updateTaskDone(taskId: Long) { fun updateTaskDone(taskId: Long) {
viewModelScope.launch { viewModelScope.launch {
toggleTaskDoneUseCase(taskId, true) toggleTaskDoneUseCase(taskId, true)

View File

@@ -5,6 +5,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.wismna.geoffroy.donext.domain.model.Task
import com.wismna.geoffroy.donext.domain.model.TaskWithListName import com.wismna.geoffroy.donext.domain.model.TaskWithListName
import com.wismna.geoffroy.donext.domain.usecase.EmptyRecycleBinUseCase import com.wismna.geoffroy.donext.domain.usecase.EmptyRecycleBinUseCase
import com.wismna.geoffroy.donext.domain.usecase.GetDeletedTasksUseCase import com.wismna.geoffroy.donext.domain.usecase.GetDeletedTasksUseCase
@@ -34,6 +35,12 @@ class RecycleBinViewModel @Inject constructor(
loadDeletedTasks() loadDeletedTasks()
} }
fun onTaskClicked(task: Task) {
viewModelScope.launch {
uiEventBus.send(UiEvent.EditTask(task))
}
}
fun loadDeletedTasks() { fun loadDeletedTasks() {
getDeletedTasksUseCase() getDeletedTasksUseCase()
.onEach { tasks -> .onEach { tasks ->

View File

@@ -1,68 +1,43 @@
package com.wismna.geoffroy.donext.presentation.viewmodel package com.wismna.geoffroy.donext.presentation.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wismna.geoffroy.donext.domain.extension.toLocalDate import com.wismna.geoffroy.donext.domain.extension.toLocalDate
import com.wismna.geoffroy.donext.domain.model.Priority import com.wismna.geoffroy.donext.domain.model.Priority
import com.wismna.geoffroy.donext.domain.model.Task import com.wismna.geoffroy.donext.domain.model.Task
import com.wismna.geoffroy.donext.presentation.ui.events.UiEvent
import com.wismna.geoffroy.donext.presentation.ui.events.UiEventBus
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import java.time.LocalDate import java.time.LocalDate
import java.time.ZoneId import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle import java.time.format.FormatStyle
import java.time.format.TextStyle import java.time.format.TextStyle
import java.util.Locale import java.util.Locale
import javax.inject.Inject
@HiltViewModel class TaskItemViewModel(task: Task) {
class TaskItemViewModel @Inject constructor( val id: Long = task.id!!
private val uiEventBus: UiEventBus val name: String = task.name
): ViewModel() { val description: String? = task.description
var id: Long? = null val isDone: Boolean = task.isDone
var name: String? = null val isDeleted: Boolean = task.isDeleted
var description: String? = null val priority: Priority = task.priority
var dueDate: String? = null
var isDone: Boolean = false
var isDeleted: Boolean = false
var priority: Priority = Priority.NORMAL
var isOverdue: Boolean = false
val today: LocalDate = LocalDate.now(ZoneId.systemDefault()) val today: LocalDate = LocalDate.now(ZoneId.systemDefault())
fun populateTask(task: Task) { val isOverdue: Boolean = task.dueDate?.let { millis ->
id = task.id!! val dueDate = millis.toLocalDate()
name = task.name dueDate.isBefore(today)
description = task.description } ?: false
dueDate = task.dueDate?.let { formatDueDate(it) }
isDone = task.isDone
isDeleted = task.isDeleted
priority = task.priority
isOverdue = task.dueDate?.let { millis ->
val dueDate = millis.toLocalDate()
dueDate.isBefore(today)
} ?: false
}
fun onTaskClicked(task: Task) { val dueDateText: String? = task.dueDate?.let { formatDueDate(it) }
viewModelScope.launch {
uiEventBus.send(UiEvent.EditTask(task))
}
}
private fun formatDueDate(dueMillis: Long): String { private fun formatDueDate(dueMillis: Long): String {
val dueDateLocal = dueMillis.toLocalDate() val dueDate = dueMillis.toLocalDate()
return when { return when {
dueDateLocal.isEqual(today) -> "Today" dueDate.isEqual(today) -> "Today"
dueDateLocal.isEqual(today.plusDays(1)) -> "Tomorrow" dueDate.isEqual(today.plusDays(1)) -> "Tomorrow"
dueDateLocal.isEqual(today.minusDays(1)) -> "Yesterday" dueDate.isEqual(today.minusDays(1)) -> "Yesterday"
dueDateLocal.isAfter(today) && dueDateLocal.isBefore(today.plusDays(7)) -> dueDate.isAfter(today) && dueDate.isBefore(today.plusDays(7)) ->
dueDateLocal.dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.getDefault()) dueDate.dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.getDefault())
else -> else ->
dueDateLocal.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(Locale.getDefault())) dueDate.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(Locale.getDefault()))
} }
} }
} }

View File

@@ -43,6 +43,12 @@ class TaskListViewModel @Inject constructor(
.launchIn(viewModelScope) .launchIn(viewModelScope)
} }
fun onTaskClicked(task: Task) {
viewModelScope.launch {
uiEventBus.send(UiEvent.EditTask(task))
}
}
fun updateTaskDone(taskId: Long, isDone: Boolean) { fun updateTaskDone(taskId: Long, isDone: Boolean) {
viewModelScope.launch { viewModelScope.launch {
toggleTaskDoneUseCase(taskId, isDone) toggleTaskDoneUseCase(taskId, isDone)