Restore due date on task list items

Replace toast with snackbar with an undo action
This commit is contained in:
Geoffroy Bonneville
2025-10-10 15:43:36 -04:00
parent c57210494a
commit c579a5d252
12 changed files with 157 additions and 88 deletions

View File

@@ -1,6 +1,5 @@
package com.wismna.geoffroy.donext.presentation.screen package com.wismna.geoffroy.donext.presentation.screen
import android.widget.Toast
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@@ -10,7 +9,6 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.wismna.geoffroy.donext.presentation.viewmodel.DueTodayViewModel import com.wismna.geoffroy.donext.presentation.viewmodel.DueTodayViewModel
@@ -31,7 +29,6 @@ fun DueTodayTasksScreen(
Text("Nothing due today !") Text("Nothing due today !")
} }
} else { } else {
val context = LocalContext.current
LazyColumn( LazyColumn(
modifier = modifier.padding(8.dp) modifier = modifier.padding(8.dp)
) { ) {
@@ -39,15 +36,8 @@ fun DueTodayTasksScreen(
TaskItemScreen( TaskItemScreen(
modifier = Modifier.animateItem(), modifier = Modifier.animateItem(),
task = task, task = task,
onSwipeLeft = { onSwipeLeft = { viewModel.updateTaskDone(task.id!!) },
viewModel.updateTaskDone(task.id!!) onSwipeRight = { viewModel.deleteTask(task.id!!) }
Toast.makeText(context, "Task done", Toast.LENGTH_SHORT).show()
},
onSwipeRight = {
viewModel.deleteTask(task.id!!)
Toast.makeText(context, "Task moved to recycle bin", Toast.LENGTH_SHORT)
.show()
}
) )
} }
} }

View File

@@ -25,6 +25,10 @@ import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
@@ -53,7 +57,6 @@ import com.wismna.geoffroy.donext.domain.model.AppDestination
import com.wismna.geoffroy.donext.presentation.ui.events.UiEvent import com.wismna.geoffroy.donext.presentation.ui.events.UiEvent
import com.wismna.geoffroy.donext.presentation.viewmodel.MainViewModel import com.wismna.geoffroy.donext.presentation.viewmodel.MainViewModel
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskListViewModel import com.wismna.geoffroy.donext.presentation.viewmodel.TaskListViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -63,7 +66,6 @@ fun MainScreen(
) { ) {
val navController = rememberNavController() val navController = rememberNavController()
val drawerState = rememberDrawerState(DrawerValue.Closed) val drawerState = rememberDrawerState(DrawerValue.Closed)
val scope = rememberCoroutineScope()
if (viewModel.isLoading) { if (viewModel.isLoading) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
@@ -88,7 +90,7 @@ fun MainScreen(
}, },
drawerState = drawerState drawerState = drawerState
) { ) {
AppContent(viewModel = viewModel, navController = navController, scope = scope, drawerState = drawerState) AppContent(viewModel = viewModel, navController = navController, drawerState = drawerState)
} }
} }
@@ -97,9 +99,11 @@ fun AppContent(
modifier : Modifier = Modifier, modifier : Modifier = Modifier,
viewModel: MainViewModel, viewModel: MainViewModel,
navController: NavHostController, navController: NavHostController,
scope: CoroutineScope,
drawerState: DrawerState drawerState: DrawerState
) { ) {
val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.uiEventBus.events.collectLatest { event -> viewModel.uiEventBus.events.collectLatest { event ->
when (event) { when (event) {
@@ -109,13 +113,24 @@ fun AppContent(
} }
is UiEvent.NavigateBack -> navController.popBackStack() is UiEvent.NavigateBack -> navController.popBackStack()
is UiEvent.EditTask -> { viewModel.showTaskSheet = true } is UiEvent.EditTask -> { viewModel.showTaskSheet = true }
else -> {} is UiEvent.ShowUndoSnackbar -> {
val result = snackbarHostState.showSnackbar(
message = event.message,
actionLabel = "Undo",
duration = SnackbarDuration.Short
)
if (result == SnackbarResult.ActionPerformed) {
event.undoAction()
}
}
else -> Unit
} }
} }
} }
Scaffold( Scaffold(
modifier = modifier.background(MaterialTheme.colorScheme.primaryContainer), modifier = modifier.background(MaterialTheme.colorScheme.primaryContainer),
containerColor = Color.Transparent, containerColor = Color.Transparent,
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
topBar = { topBar = {
TopAppBar( TopAppBar(
title = { Text(viewModel.currentDestination.title) }, title = { Text(viewModel.currentDestination.title) },

View File

@@ -75,11 +75,9 @@ fun RecycleBinScreen(
TaskItemScreen( TaskItemScreen(
modifier = Modifier.animateItem(), modifier = Modifier.animateItem(),
task = item.task, task = item.task,
onSwipeLeft = { onSwipeLeft = { viewModel.restore(item.task.id!!) },
viewModel.restore(item.task.id!!)
Toast.makeText(context, "Task restored", Toast.LENGTH_SHORT).show()
},
onSwipeRight = { onSwipeRight = {
// 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()
} }

View File

@@ -123,7 +123,7 @@ fun TaskItemScreen(
) )
// Due date badge // Due date badge
viewModel.dueDateText?.let { dueMillis -> viewModel.dueDate?.let { dueMillis ->
Badge( Badge(
modifier = Modifier modifier = Modifier
.align( .align(
@@ -134,7 +134,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.dueDateText, text = viewModel.dueDate!!,
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
) )

View File

@@ -1,7 +1,7 @@
package com.wismna.geoffroy.donext.presentation.screen package com.wismna.geoffroy.donext.presentation.screen
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@@ -9,26 +9,39 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskListViewModel import com.wismna.geoffroy.donext.presentation.viewmodel.TaskListViewModel
@Composable @Composable
fun TaskListScreen( fun TaskListScreen(
modifier: Modifier = Modifier,
viewModel: TaskListViewModel = hiltViewModel<TaskListViewModel>(), viewModel: TaskListViewModel = hiltViewModel<TaskListViewModel>(),
) { ) {
val tasks = viewModel.tasks val tasks = viewModel.tasks
if (tasks.isEmpty()) {
// Placeholder when recycle bin is empty
Column (
modifier = modifier.fillMaxSize().padding(10.dp),
Arrangement.Center
) {
Text("Nothing here!", modifier.fillMaxWidth(), textAlign = TextAlign.Center)
Text("Add tasks with the Create Task button", modifier.fillMaxWidth(), textAlign = TextAlign.Center)
}
return
}
// Split tasks into active and done // Split tasks into active and done
val (active, done) = remember(tasks) { val (active, done) = remember(tasks) {
tasks.partition { !it.isDone } tasks.partition { !it.isDone }
} }
val context = LocalContext.current
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(8.dp), contentPadding = PaddingValues(8.dp),
@@ -42,14 +55,8 @@ fun TaskListScreen(
TaskItemScreen( TaskItemScreen(
modifier = Modifier.animateItem(), modifier = Modifier.animateItem(),
task = task, task = task,
onSwipeLeft = { onSwipeLeft = { viewModel.updateTaskDone(task.id!!, true) },
viewModel.updateTaskDone(task.id!!, true) onSwipeRight = { viewModel.deleteTask(task.id!!) }
Toast.makeText(context, "Task done", Toast.LENGTH_SHORT).show()
},
onSwipeRight = {
viewModel.deleteTask(task.id!!)
Toast.makeText(context, "Task moved to recycle bin", Toast.LENGTH_SHORT).show()
}
) )
} }
@@ -72,14 +79,8 @@ fun TaskListScreen(
TaskItemScreen( TaskItemScreen(
modifier = Modifier.animateItem(), modifier = Modifier.animateItem(),
task = task, task = task,
onSwipeLeft = { onSwipeLeft = { viewModel.updateTaskDone(task.id!!, false) },
viewModel.updateTaskDone(task.id!!, false) onSwipeRight = { viewModel.deleteTask(task.id!!) },
Toast.makeText(context, "Task in progress", Toast.LENGTH_SHORT).show()
},
onSwipeRight = {
viewModel.deleteTask(task.id!!)
Toast.makeText(context, "Task moved to recycle bin", Toast.LENGTH_SHORT).show()
},
) )
} }

View File

@@ -5,9 +5,11 @@ import com.wismna.geoffroy.donext.domain.model.Task
sealed class UiEvent { sealed class UiEvent {
data class Navigate(val route: String) : UiEvent() data class Navigate(val route: String) : UiEvent()
data object NavigateBack : UiEvent() data object NavigateBack : UiEvent()
data class ShowSnackbar(val message: String) : UiEvent()
data class EditTask(val task: Task) : UiEvent() data class EditTask(val task: Task) : UiEvent()
data class CreateNewTask(val taskListId: Long) : UiEvent() data class CreateNewTask(val taskListId: Long) : UiEvent()
data object CloseTask : UiEvent() data object CloseTask : UiEvent()
data class ShowUndoSnackbar(
val message: String,
val undoAction: () -> Unit
) : UiEvent()
} }

View File

@@ -9,6 +9,8 @@ import com.wismna.geoffroy.donext.domain.model.Task
import com.wismna.geoffroy.donext.domain.usecase.GetDueTodayTasksUseCase import com.wismna.geoffroy.donext.domain.usecase.GetDueTodayTasksUseCase
import com.wismna.geoffroy.donext.domain.usecase.ToggleTaskDeletedUseCase import com.wismna.geoffroy.donext.domain.usecase.ToggleTaskDeletedUseCase
import com.wismna.geoffroy.donext.domain.usecase.ToggleTaskDoneUseCase import com.wismna.geoffroy.donext.domain.usecase.ToggleTaskDoneUseCase
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 dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@@ -17,16 +19,17 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class DueTodayViewModel @Inject constructor( class DueTodayViewModel @Inject constructor(
getDueTodayTasks: GetDueTodayTasksUseCase, getDueTodayTasksUseCase: GetDueTodayTasksUseCase,
private val toggleTaskDeleted: ToggleTaskDeletedUseCase, private val toggleTaskDeletedUseCase: ToggleTaskDeletedUseCase,
private val toggleTaskDone: ToggleTaskDoneUseCase private val toggleTaskDoneUseCase: ToggleTaskDoneUseCase,
private val uiEventBus: UiEventBus
) : ViewModel() { ) : ViewModel() {
var dueTodayTasks by mutableStateOf<List<Task>>(emptyList()) var dueTodayTasks by mutableStateOf<List<Task>>(emptyList())
private set private set
init { init {
getDueTodayTasks() getDueTodayTasksUseCase()
.onEach { tasks -> .onEach { tasks ->
dueTodayTasks = tasks dueTodayTasks = tasks
} }
@@ -35,12 +38,35 @@ class DueTodayViewModel @Inject constructor(
fun updateTaskDone(taskId: Long) { fun updateTaskDone(taskId: Long) {
viewModelScope.launch { viewModelScope.launch {
toggleTaskDone(taskId, true) toggleTaskDoneUseCase(taskId, true)
uiEventBus.send(
UiEvent.ShowUndoSnackbar(
message = "Task done",
undoAction = {
viewModelScope.launch {
toggleTaskDoneUseCase(taskId, false)
}
}
)
)
} }
} }
fun deleteTask(taskId: Long) { fun deleteTask(taskId: Long) {
viewModelScope.launch { viewModelScope.launch {
toggleTaskDeleted(taskId, true) toggleTaskDeletedUseCase(taskId, true)
uiEventBus.send(
UiEvent.ShowUndoSnackbar(
message = "Task moved to trash",
undoAction = {
viewModelScope.launch {
toggleTaskDeletedUseCase(taskId, false)
}
}
)
)
} }
} }
} }

View File

@@ -46,10 +46,10 @@ class MainViewModel @Inject constructor(
AppDestination.ManageLists + AppDestination.ManageLists +
AppDestination.RecycleBin + AppDestination.RecycleBin +
AppDestination.DueTodayList AppDestination.DueTodayList
isLoading = false
if (startDestination == AppDestination.ManageLists && destinations.isNotEmpty()) { if (startDestination == AppDestination.ManageLists && destinations.isNotEmpty()) {
startDestination = destinations.first() startDestination = destinations.first()
} }
isLoading = false
} }
.launchIn(viewModelScope) .launchIn(viewModelScope)
} }

View File

@@ -6,7 +6,6 @@ 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.AppDestination
import com.wismna.geoffroy.donext.domain.model.TaskListWithOverdue import com.wismna.geoffroy.donext.domain.model.TaskListWithOverdue
import com.wismna.geoffroy.donext.domain.usecase.GetDueTodayTasksUseCase import com.wismna.geoffroy.donext.domain.usecase.GetDueTodayTasksUseCase
import com.wismna.geoffroy.donext.domain.usecase.GetTaskListsWithOverdueUseCase import com.wismna.geoffroy.donext.domain.usecase.GetTaskListsWithOverdueUseCase

View File

@@ -10,6 +10,8 @@ import com.wismna.geoffroy.donext.domain.usecase.EmptyRecycleBinUseCase
import com.wismna.geoffroy.donext.domain.usecase.GetDeletedTasksUseCase import com.wismna.geoffroy.donext.domain.usecase.GetDeletedTasksUseCase
import com.wismna.geoffroy.donext.domain.usecase.PermanentlyDeleteTaskUseCase import com.wismna.geoffroy.donext.domain.usecase.PermanentlyDeleteTaskUseCase
import com.wismna.geoffroy.donext.domain.usecase.ToggleTaskDeletedUseCase import com.wismna.geoffroy.donext.domain.usecase.ToggleTaskDeletedUseCase
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 dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@@ -18,10 +20,11 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class RecycleBinViewModel @Inject constructor( class RecycleBinViewModel @Inject constructor(
private val getDeletedTasks: GetDeletedTasksUseCase, private val getDeletedTasksUseCase: GetDeletedTasksUseCase,
private val restoreTask: ToggleTaskDeletedUseCase, private val toggleTaskDeletedUseCase: ToggleTaskDeletedUseCase,
private val permanentlyDeleteTask: PermanentlyDeleteTaskUseCase, private val permanentlyDeleteTaskUseCase: PermanentlyDeleteTaskUseCase,
private val emptyRecycleBinUseCase: EmptyRecycleBinUseCase private val emptyRecycleBinUseCase: EmptyRecycleBinUseCase,
private val uiEventBus: UiEventBus
) : ViewModel() { ) : ViewModel() {
var deletedTasks by mutableStateOf<List<TaskWithListName>>(emptyList()) var deletedTasks by mutableStateOf<List<TaskWithListName>>(emptyList())
@@ -32,7 +35,7 @@ class RecycleBinViewModel @Inject constructor(
} }
fun loadDeletedTasks() { fun loadDeletedTasks() {
getDeletedTasks() getDeletedTasksUseCase()
.onEach { tasks -> .onEach { tasks ->
deletedTasks = tasks deletedTasks = tasks
} }
@@ -41,14 +44,25 @@ class RecycleBinViewModel @Inject constructor(
fun restore(taskId: Long) { fun restore(taskId: Long) {
viewModelScope.launch { viewModelScope.launch {
restoreTask(taskId, false) toggleTaskDeletedUseCase(taskId, false)
loadDeletedTasks() loadDeletedTasks()
uiEventBus.send(
UiEvent.ShowUndoSnackbar(
message = "Task restored",
undoAction = {
viewModelScope.launch {
toggleTaskDeletedUseCase(taskId, true)
}
}
)
)
} }
} }
fun deleteForever(taskId: Long) { fun deleteForever(taskId: Long) {
viewModelScope.launch { viewModelScope.launch {
permanentlyDeleteTask(taskId) permanentlyDeleteTaskUseCase(taskId)
loadDeletedTasks() loadDeletedTasks()
} }
} }

View File

@@ -24,41 +24,25 @@ class TaskItemViewModel @Inject constructor(
var id: Long? = null var id: Long? = null
var name: String? = null var name: String? = null
var description: String? = null var description: String? = null
var dueDate: Long? = null var dueDate: String? = null
var isDone: Boolean = false var isDone: Boolean = false
var isDeleted: Boolean = false var isDeleted: Boolean = false
var priority: Priority = Priority.NORMAL var priority: Priority = Priority.NORMAL
var isOverdue: Boolean = false
val today: LocalDate = LocalDate.now(ZoneId.systemDefault()) val today: LocalDate = LocalDate.now(ZoneId.systemDefault())
val isOverdue: Boolean = dueDate?.let { millis ->
val dueDate = millis.toLocalDate()
dueDate.isBefore(today)
} ?: false
val dueDateText: String? = dueDate?.let { formatDueDate(it) }
private fun formatDueDate(dueMillis: Long): String {
val dueDate = dueMillis.toLocalDate()
return when {
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 ->
dueDate.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(Locale.getDefault()))
}
}
fun populateTask(task: Task) { fun populateTask(task: Task) {
id = task.id!! id = task.id!!
name = task.name name = task.name
description = task.description description = task.description
dueDate = task.dueDate?.let { formatDueDate(it) }
isDone = task.isDone isDone = task.isDone
isDeleted = task.isDeleted isDeleted = task.isDeleted
priority = task.priority priority = task.priority
isOverdue = task.dueDate?.let { millis ->
val dueDate = millis.toLocalDate()
dueDate.isBefore(today)
} ?: false
} }
fun onTaskClicked(task: Task) { fun onTaskClicked(task: Task) {
@@ -66,4 +50,19 @@ class TaskItemViewModel @Inject constructor(
uiEventBus.send(UiEvent.EditTask(task)) uiEventBus.send(UiEvent.EditTask(task))
} }
} }
private fun formatDueDate(dueMillis: Long): String {
val dueDateLocal = 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())
else ->
dueDateLocal.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(Locale.getDefault()))
}
}
} }

View File

@@ -10,6 +10,8 @@ import com.wismna.geoffroy.donext.domain.model.Task
import com.wismna.geoffroy.donext.domain.usecase.GetTasksForListUseCase import com.wismna.geoffroy.donext.domain.usecase.GetTasksForListUseCase
import com.wismna.geoffroy.donext.domain.usecase.ToggleTaskDeletedUseCase import com.wismna.geoffroy.donext.domain.usecase.ToggleTaskDeletedUseCase
import com.wismna.geoffroy.donext.domain.usecase.ToggleTaskDoneUseCase import com.wismna.geoffroy.donext.domain.usecase.ToggleTaskDoneUseCase
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 dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@@ -18,10 +20,11 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class TaskListViewModel @Inject constructor( class TaskListViewModel @Inject constructor(
getTasks: GetTasksForListUseCase,
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
private val toggleTaskDone: ToggleTaskDoneUseCase, getTasksUseCase: GetTasksForListUseCase,
private val toggleTaskDeleted: ToggleTaskDeletedUseCase, private val toggleTaskDoneUseCase: ToggleTaskDoneUseCase,
private val toggleTaskDeletedUseCase: ToggleTaskDeletedUseCase,
private val uiEventBus: UiEventBus
) : ViewModel() { ) : ViewModel() {
var tasks by mutableStateOf<List<Task>>(emptyList()) var tasks by mutableStateOf<List<Task>>(emptyList())
@@ -32,7 +35,7 @@ class TaskListViewModel @Inject constructor(
private val taskListId: Long = checkNotNull(savedStateHandle["taskListId"]) private val taskListId: Long = checkNotNull(savedStateHandle["taskListId"])
init { init {
getTasks(taskListId) getTasksUseCase(taskListId)
.onEach { list -> .onEach { list ->
tasks = list tasks = list
isLoading = false isLoading = false
@@ -42,12 +45,34 @@ class TaskListViewModel @Inject constructor(
fun updateTaskDone(taskId: Long, isDone: Boolean) { fun updateTaskDone(taskId: Long, isDone: Boolean) {
viewModelScope.launch { viewModelScope.launch {
toggleTaskDone(taskId, isDone) toggleTaskDoneUseCase(taskId, isDone)
uiEventBus.send(
UiEvent.ShowUndoSnackbar(
message = "Task done",
undoAction = {
viewModelScope.launch {
toggleTaskDoneUseCase(taskId, !isDone)
}
}
)
)
} }
} }
fun deleteTask(taskId: Long) { fun deleteTask(taskId: Long) {
viewModelScope.launch { viewModelScope.launch {
toggleTaskDeleted(taskId, true) toggleTaskDeletedUseCase(taskId, true)
uiEventBus.send(
UiEvent.ShowUndoSnackbar(
message = "Task moved to trash",
undoAction = {
viewModelScope.launch {
toggleTaskDeletedUseCase(taskId, false)
}
}
)
)
} }
} }
} }