From 30d3efa9defe1a499fe23d7e9aa94d49129625a6 Mon Sep 17 00:00:00 2001 From: Geoffroy Bonneville <24917789+wismna@users.noreply.github.com> Date: Fri, 10 Oct 2025 18:23:07 -0400 Subject: [PATCH] Reverted TaskItemVM to a dumb state-like VM Task Edit events are now carried to the parent composable --- .idea/deploymentTargetSelector.xml | 6 +- .../screen/DueTodayTasksScreen.kt | 3 +- .../presentation/screen/RecycleBinScreen.kt | 3 +- .../presentation/screen/TaskItemScreen.kt | 47 +++++++++----- .../presentation/screen/TaskListScreen.kt | 5 +- .../viewmodel/DueTodayViewModel.kt | 6 ++ .../viewmodel/RecycleBinViewModel.kt | 7 ++ .../viewmodel/TaskItemViewModel.kt | 65 ++++++------------- .../viewmodel/TaskListViewModel.kt | 6 ++ 9 files changed, 78 insertions(+), 70 deletions(-) diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index 98cd5b2..586219f 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -2,15 +2,15 @@ - - + + \ No newline at end of file diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/DueTodayTasksScreen.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/DueTodayTasksScreen.kt index 8ff489f..32df149 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/DueTodayTasksScreen.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/DueTodayTasksScreen.kt @@ -37,7 +37,8 @@ fun DueTodayTasksScreen( modifier = Modifier.animateItem(), task = task, onSwipeLeft = { viewModel.updateTaskDone(task.id!!) }, - onSwipeRight = { viewModel.deleteTask(task.id!!) } + onSwipeRight = { viewModel.deleteTask(task.id!!) }, + onTaskClick = { viewModel.onTaskClicked(task) } ) } } diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/RecycleBinScreen.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/RecycleBinScreen.kt index 1a45618..c5717cd 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/RecycleBinScreen.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/RecycleBinScreen.kt @@ -80,7 +80,8 @@ fun RecycleBinScreen( // TODO: add confirmation dialog viewModel.deleteForever(item.task.id!!) Toast.makeText(context, "Task deleted", Toast.LENGTH_SHORT).show() - } + }, + onTaskClick = { viewModel.onTaskClicked(item.task) } ) } } diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/TaskItemScreen.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/TaskItemScreen.kt index 2000be4..5ae4240 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/TaskItemScreen.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/TaskItemScreen.kt @@ -28,6 +28,8 @@ import androidx.compose.material3.SwipeToDismissBoxValue import androidx.compose.material3.Text import androidx.compose.material3.rememberSwipeToDismissBoxState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier 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.unit.dp 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.Task import com.wismna.geoffroy.donext.presentation.viewmodel.TaskItemViewModel +import kotlinx.coroutines.flow.filter @Composable fun TaskItemScreen( modifier: Modifier = Modifier, task: Task, - viewModel: TaskItemViewModel = hiltViewModel(), onSwipeLeft: () -> Unit, - onSwipeRight: () -> Unit + onSwipeRight: () -> Unit, + onTaskClick: (task: Task) -> Unit ) { - viewModel.populateTask(task) - // TODO: change this + val viewModel = TaskItemViewModel(task) + val dismissState = rememberSwipeToDismissBoxState( - confirmValueChange = { - when (it) { - SwipeToDismissBoxValue.StartToEnd -> { onSwipeRight() } - SwipeToDismissBoxValue.EndToStart -> { onSwipeLeft() } - SwipeToDismissBoxValue.Settled -> return@rememberSwipeToDismissBoxState false - } - return@rememberSwipeToDismissBoxState true - }, // positional threshold of 25% 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( fontWeight = when (viewModel.priority) { Priority.HIGH -> FontWeight.Bold @@ -79,7 +90,7 @@ fun TaskItemScreen( ) Card( modifier = modifier, - onClick = { viewModel.onTaskClicked(task) }, + onClick = { onTaskClick(task) }, colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surfaceContainer, ), @@ -110,7 +121,7 @@ fun TaskItemScreen( ) { // Title Text( - text = viewModel.name!!, + text = viewModel.name, fontSize = 18.sp, style = baseStyle, modifier = Modifier @@ -123,7 +134,7 @@ fun TaskItemScreen( ) // Due date badge - viewModel.dueDate?.let { dueMillis -> + viewModel.dueDateText?.let { dueMillis -> Badge( modifier = Modifier .align( @@ -134,7 +145,7 @@ fun TaskItemScreen( ) { Text( 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, style = MaterialTheme.typography.bodySmall ) @@ -151,7 +162,7 @@ fun TaskItemScreen( ) { if (!viewModel.description.isNullOrBlank()) { Text( - text = viewModel.description!!, + text = viewModel.description, color = MaterialTheme.colorScheme.tertiary, style = baseStyle.copy( fontSize = MaterialTheme.typography.bodyMedium.fontSize, diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/TaskListScreen.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/TaskListScreen.kt index 7b39dc5..029ad27 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/TaskListScreen.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/TaskListScreen.kt @@ -55,7 +55,8 @@ fun TaskListScreen( modifier = Modifier.animateItem(), task = task, 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, onSwipeLeft = { viewModel.updateTaskDone(task.id!!, false) }, onSwipeRight = { viewModel.deleteTask(task.id!!) }, + onTaskClick = { viewModel.onTaskClicked(task) } ) } - } } \ No newline at end of file diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/DueTodayViewModel.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/DueTodayViewModel.kt index 948e332..6868ffa 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/DueTodayViewModel.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/DueTodayViewModel.kt @@ -36,6 +36,12 @@ class DueTodayViewModel @Inject constructor( .launchIn(viewModelScope) } + fun onTaskClicked(task: Task) { + viewModelScope.launch { + uiEventBus.send(UiEvent.EditTask(task)) + } + } + fun updateTaskDone(taskId: Long) { viewModelScope.launch { toggleTaskDoneUseCase(taskId, true) diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/RecycleBinViewModel.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/RecycleBinViewModel.kt index 6a71ab4..b79cb02 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/RecycleBinViewModel.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/RecycleBinViewModel.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel 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.usecase.EmptyRecycleBinUseCase import com.wismna.geoffroy.donext.domain.usecase.GetDeletedTasksUseCase @@ -34,6 +35,12 @@ class RecycleBinViewModel @Inject constructor( loadDeletedTasks() } + fun onTaskClicked(task: Task) { + viewModelScope.launch { + uiEventBus.send(UiEvent.EditTask(task)) + } + } + fun loadDeletedTasks() { getDeletedTasksUseCase() .onEach { tasks -> diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/TaskItemViewModel.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/TaskItemViewModel.kt index 5be6666..491905d 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/TaskItemViewModel.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/TaskItemViewModel.kt @@ -1,68 +1,43 @@ 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.model.Priority 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.ZoneId import java.time.format.DateTimeFormatter import java.time.format.FormatStyle import java.time.format.TextStyle import java.util.Locale -import javax.inject.Inject -@HiltViewModel -class TaskItemViewModel @Inject constructor( - private val uiEventBus: UiEventBus -): ViewModel() { - var id: Long? = null - var name: String? = null - var description: String? = null - var dueDate: String? = null - var isDone: Boolean = false - var isDeleted: Boolean = false - var priority: Priority = Priority.NORMAL - var isOverdue: Boolean = false +class TaskItemViewModel(task: Task) { + val id: Long = task.id!! + val name: String = task.name + val description: String? = task.description + val isDone: Boolean = task.isDone + val isDeleted: Boolean = task.isDeleted + val priority: Priority = task.priority + val today: LocalDate = LocalDate.now(ZoneId.systemDefault()) - fun populateTask(task: Task) { - id = task.id!! - name = task.name - description = task.description - 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 - } + val isOverdue: Boolean = task.dueDate?.let { millis -> + val dueDate = millis.toLocalDate() + dueDate.isBefore(today) + } ?: false - fun onTaskClicked(task: Task) { - viewModelScope.launch { - uiEventBus.send(UiEvent.EditTask(task)) - } - } + val dueDateText: String? = task.dueDate?.let { formatDueDate(it) } private fun formatDueDate(dueMillis: Long): String { - val dueDateLocal = dueMillis.toLocalDate() + val dueDate = dueMillis.toLocalDate() return when { - dueDateLocal.isEqual(today) -> "Today" - dueDateLocal.isEqual(today.plusDays(1)) -> "Tomorrow" - dueDateLocal.isEqual(today.minusDays(1)) -> "Yesterday" - dueDateLocal.isAfter(today) && dueDateLocal.isBefore(today.plusDays(7)) -> - dueDateLocal.dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.getDefault()) + dueDate.isEqual(today) -> "Today" + dueDate.isEqual(today.plusDays(1)) -> "Tomorrow" + dueDate.isEqual(today.minusDays(1)) -> "Yesterday" + dueDate.isAfter(today) && dueDate.isBefore(today.plusDays(7)) -> + dueDate.dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.getDefault()) else -> - dueDateLocal.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(Locale.getDefault())) + dueDate.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(Locale.getDefault())) } } - } \ No newline at end of file diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/TaskListViewModel.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/TaskListViewModel.kt index 733fae0..f69ea4a 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/TaskListViewModel.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/TaskListViewModel.kt @@ -43,6 +43,12 @@ class TaskListViewModel @Inject constructor( .launchIn(viewModelScope) } + fun onTaskClicked(task: Task) { + viewModelScope.launch { + uiEventBus.send(UiEvent.EditTask(task)) + } + } + fun updateTaskDone(taskId: Long, isDone: Boolean) { viewModelScope.launch { toggleTaskDoneUseCase(taskId, isDone)