Compare commits

..

7 Commits

Author SHA1 Message Date
Geoffroy Bonneville
038a97672f Restore previous swiping mechanism 2025-10-10 20:54:25 -04:00
Geoffroy Bonneville
30d3efa9de Reverted TaskItemVM to a dumb state-like VM
Task Edit events are now carried to the parent composable
2025-10-10 18:23:07 -04:00
Geoffroy Bonneville
53e716a690 Light cleanup and refactoring 2025-10-10 16:11:39 -04:00
Geoffroy Bonneville
6c9e5efe38 Added undo snackbar for list deletion
Made all destinations show the menu instead of the back button (for now)
2025-10-10 16:04:18 -04:00
Geoffroy Bonneville
c579a5d252 Restore due date on task list items
Replace toast with snackbar with an undo action
2025-10-10 15:43:36 -04:00
Geoffroy Bonneville
c57210494a Prevent navigation to current destination 2025-10-10 11:24:49 -04:00
Geoffroy Bonneville
c3dd615d15 Use events for task clicks 2025-10-10 10:59:32 -04:00
19 changed files with 255 additions and 146 deletions

View File

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

View File

@@ -13,16 +13,16 @@ sealed class AppDestination(
object DueTodayList : AppDestination(
route = "todayList",
title = "Due Today",
showBackButton = true,
showBackButton = false,
)
object ManageLists : AppDestination(
route = "manageLists",
title = "Manage Lists",
showBackButton = true,
showBackButton = false,
)
object RecycleBin : AppDestination(
route = "recycleBin",
title = "Recycle Bin",
showBackButton = true,
showBackButton = false,
)
}

View File

@@ -6,7 +6,7 @@ import javax.inject.Inject
class DeleteTaskListUseCase@Inject constructor(
private val repository: TaskRepository
) {
suspend operator fun invoke(taskListId: Long) {
repository.deleteTaskList(taskListId, true)
suspend operator fun invoke(taskListId: Long, isDeleted: Boolean) {
repository.deleteTaskList(taskListId, isDeleted)
}
}

View File

@@ -1,6 +1,5 @@
package com.wismna.geoffroy.donext.presentation.screen
import android.widget.Toast
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
@@ -10,17 +9,14 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.wismna.geoffroy.donext.domain.model.Task
import com.wismna.geoffroy.donext.presentation.viewmodel.DueTodayViewModel
@Composable
fun DueTodayTasksScreen(
modifier: Modifier = Modifier,
viewModel: DueTodayViewModel = hiltViewModel(),
onTaskClick: (task: Task) -> Unit
) {
val tasks = viewModel.dueTodayTasks
@@ -33,7 +29,6 @@ fun DueTodayTasksScreen(
Text("Nothing due today !")
}
} else {
val context = LocalContext.current
LazyColumn(
modifier = modifier.padding(8.dp)
) {
@@ -41,16 +36,9 @@ fun DueTodayTasksScreen(
TaskItemScreen(
modifier = Modifier.animateItem(),
task = task,
onTaskClick = { onTaskClick(task) },
onSwipeLeft = {
viewModel.updateTaskDone(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()
}
onSwipeLeft = { viewModel.updateTaskDone(task.id!!) },
onSwipeRight = { viewModel.deleteTask(task.id!!) },
onTaskClick = { viewModel.onTaskClicked(task) }
)
}
}

View File

@@ -25,6 +25,10 @@ import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalNavigationDrawer
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.Text
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.viewmodel.MainViewModel
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskListViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
@@ -63,7 +66,6 @@ fun MainScreen(
) {
val navController = rememberNavController()
val drawerState = rememberDrawerState(DrawerValue.Closed)
val scope = rememberCoroutineScope()
if (viewModel.isLoading) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
@@ -88,7 +90,7 @@ fun MainScreen(
},
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,
viewModel: MainViewModel,
navController: NavHostController,
scope: CoroutineScope,
drawerState: DrawerState
) {
val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
LaunchedEffect(Unit) {
viewModel.uiEventBus.events.collectLatest { event ->
when (event) {
@@ -108,13 +112,25 @@ fun AppContent(
navController.navigate(event.route)
}
is UiEvent.NavigateBack -> navController.popBackStack()
else -> {}
is UiEvent.EditTask -> { viewModel.showTaskSheet = true }
is UiEvent.ShowUndoSnackbar -> {
val result = snackbarHostState.showSnackbar(
message = event.message,
actionLabel = "Undo",
duration = SnackbarDuration.Short
)
if (result == SnackbarResult.ActionPerformed) {
event.undoAction()
}
}
else -> Unit
}
}
}
Scaffold(
modifier = modifier.background(MaterialTheme.colorScheme.primaryContainer),
containerColor = Color.Transparent,
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
topBar = {
TopAppBar(
title = { Text(viewModel.currentDestination.title) },
@@ -207,29 +223,17 @@ fun AppContent(
}
val taskListViewModel: TaskListViewModel = hiltViewModel(navBackStackEntry)
TaskListScreen(
viewModel = taskListViewModel,
onTaskClick = { task -> viewModel.onTaskClicked(task) }
)
TaskListScreen(viewModel = taskListViewModel)
}
composable(AppDestination.ManageLists.route) {
ManageListsScreen(
modifier = Modifier,
showAddListSheet = {viewModel.showAddListSheet = true}
)
ManageListsScreen(modifier = Modifier)
}
composable(AppDestination.DueTodayList.route) {
DueTodayTasksScreen (
modifier = Modifier,
onTaskClick = { task -> viewModel.onTaskClicked(task) }
)
DueTodayTasksScreen (modifier = Modifier)
}
composable(AppDestination.RecycleBin.route) {
RecycleBinScreen(
modifier = Modifier,
onTaskClick = { task -> viewModel.onTaskClicked(task) }
)
RecycleBinScreen(modifier = Modifier)
}
}
}

View File

@@ -6,10 +6,12 @@ import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@@ -58,10 +60,21 @@ import sh.calvin.reorderable.rememberReorderableLazyListState
@Composable
fun ManageListsScreen(
modifier: Modifier,
viewModel: ManageListsViewModel = hiltViewModel(),
showAddListSheet: () -> Unit
viewModel: ManageListsViewModel = hiltViewModel()
) {
var lists = viewModel.taskLists.toMutableList()
if (lists.isEmpty()) {
// Placeholder when no task lists exist
Box(
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text("Tap + to create a new task list.")
}
return
}
val lazyListState = rememberLazyListState()
val reorderState = rememberReorderableLazyListState(
lazyListState = lazyListState,

View File

@@ -57,7 +57,7 @@ fun MenuScreen(
},
icon = { Icon(Icons.Default.Today, contentDescription = "Due Today") },
selected = currentDestination is AppDestination.DueTodayList,
onClick = { viewModel.navigateTo(AppDestination.DueTodayList.route) },
onClick = { viewModel.navigateTo(AppDestination.DueTodayList.route, currentDestination.route) },
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)
HorizontalDivider()
@@ -73,7 +73,7 @@ fun MenuScreen(
icon = { Icon(Icons.Default.LineWeight, contentDescription = list.name) },
selected = currentDestination is AppDestination.TaskList &&
currentDestination.taskListId == list.id,
onClick = { viewModel.navigateTo("taskList/${list.id}") },
onClick = { viewModel.navigateTo("taskList/${list.id}", currentDestination.route) },
badge = {
if (list.overdueCount > 0) {
Badge { Text(list.overdueCount.toString()) }
@@ -90,14 +90,14 @@ fun MenuScreen(
label = { Text("Recycle Bin") },
icon = { Icon(Icons.Default.Delete, contentDescription = "Recycle Bin") },
selected = currentDestination is AppDestination.RecycleBin,
onClick = { viewModel.navigateTo(AppDestination.RecycleBin.route) },
onClick = { viewModel.navigateTo(AppDestination.RecycleBin.route, currentDestination.route) },
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)
NavigationDrawerItem(
label = { Text("Edit Lists") },
icon = { Icon(Icons.Default.EditNote, contentDescription = "Edit Lists") },
selected = currentDestination is AppDestination.ManageLists,
onClick = { viewModel.navigateTo(AppDestination.ManageLists.route) },
onClick = { viewModel.navigateTo(AppDestination.ManageLists.route, currentDestination.route) },
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)
}

View File

@@ -29,14 +29,12 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.wismna.geoffroy.donext.domain.model.Task
import com.wismna.geoffroy.donext.presentation.viewmodel.RecycleBinViewModel
@Composable
fun RecycleBinScreen(
modifier: Modifier = Modifier,
viewModel: RecycleBinViewModel = hiltViewModel(),
onTaskClick: (task: Task) -> Unit
) {
val tasks = viewModel.deletedTasks
@@ -77,15 +75,13 @@ fun RecycleBinScreen(
TaskItemScreen(
modifier = Modifier.animateItem(),
task = item.task,
onTaskClick = { onTaskClick(item.task) },
onSwipeLeft = {
viewModel.restore(item.task.id!!)
Toast.makeText(context, "Task restored", Toast.LENGTH_SHORT).show()
},
onSwipeLeft = { viewModel.restore(item.task.id!!) },
onSwipeRight = {
// TODO: add confirmation dialog
viewModel.deleteForever(item.task.id!!)
Toast.makeText(context, "Task deleted", Toast.LENGTH_SHORT).show()
}
},
onTaskClick = { viewModel.onTaskClicked(item.task) }
)
}
}

View File

@@ -45,12 +45,12 @@ import com.wismna.geoffroy.donext.presentation.viewmodel.TaskItemViewModel
fun TaskItemScreen(
modifier: Modifier = Modifier,
task: Task,
onTaskClick: (taskId: Long) -> Unit,
onSwipeLeft: () -> Unit,
onSwipeRight: () -> Unit
onSwipeRight: () -> Unit,
onTaskClick: (task: Task) -> Unit
) {
val viewModel = TaskItemViewModel(task)
// TODO: change this
val dismissState = rememberSwipeToDismissBoxState(
confirmValueChange = {
when (it) {
@@ -63,6 +63,7 @@ fun TaskItemScreen(
// positional threshold of 25%
positionalThreshold = { it * .25f }
)
val baseStyle = MaterialTheme.typography.bodyLarge.copy(
fontWeight = when (viewModel.priority) {
Priority.HIGH -> FontWeight.Bold
@@ -78,7 +79,7 @@ fun TaskItemScreen(
)
Card(
modifier = modifier,
onClick = { onTaskClick(viewModel.id) },
onClick = { onTaskClick(task) },
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainer,
),

View File

@@ -1,7 +1,7 @@
package com.wismna.geoffroy.donext.presentation.screen
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@@ -9,27 +9,38 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.wismna.geoffroy.donext.domain.model.Task
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskListViewModel
@Composable
fun TaskListScreen(
modifier: Modifier = Modifier,
viewModel: TaskListViewModel = hiltViewModel<TaskListViewModel>(),
onTaskClick: (Task) -> Unit) {
) {
val tasks = viewModel.tasks
if (tasks.isEmpty()) {
// Placeholder when recycle bin is empty
Box(
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text("Tap + to create a new task.")
}
return
}
// Split tasks into active and done
val (active, done) = remember(tasks) {
tasks.partition { !it.isDone }
}
val context = LocalContext.current
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(8.dp),
@@ -43,15 +54,9 @@ fun TaskListScreen(
TaskItemScreen(
modifier = Modifier.animateItem(),
task = task,
onTaskClick = { onTaskClick(task) },
onSwipeLeft = {
viewModel.updateTaskDone(task.id!!, true)
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()
}
onSwipeLeft = { viewModel.updateTaskDone(task.id!!, true) },
onSwipeRight = { viewModel.deleteTask(task.id!!) },
onTaskClick = { viewModel.onTaskClicked(task) }
)
}
@@ -74,17 +79,10 @@ fun TaskListScreen(
TaskItemScreen(
modifier = Modifier.animateItem(),
task = task,
onTaskClick = { onTaskClick(task) },
onSwipeLeft = {
viewModel.updateTaskDone(task.id!!, false)
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()
},
onSwipeLeft = { viewModel.updateTaskDone(task.id!!, false) },
onSwipeRight = { viewModel.deleteTask(task.id!!) },
onTaskClick = { viewModel.onTaskClicked(task) }
)
}
}
}

View File

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

View File

@@ -0,0 +1,16 @@
package com.wismna.geoffroy.donext.presentation.ui.events
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class UiEventBus @Inject constructor() {
private val _events = MutableSharedFlow<UiEvent>(replay = 1)
val events = _events.asSharedFlow()
suspend fun send(event: UiEvent) {
_events.emit(event)
}
}

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.ToggleTaskDeletedUseCase
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 kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -17,30 +19,60 @@ import javax.inject.Inject
@HiltViewModel
class DueTodayViewModel @Inject constructor(
getDueTodayTasks: GetDueTodayTasksUseCase,
private val toggleTaskDeleted: ToggleTaskDeletedUseCase,
private val toggleTaskDone: ToggleTaskDoneUseCase
getDueTodayTasksUseCase: GetDueTodayTasksUseCase,
private val toggleTaskDeletedUseCase: ToggleTaskDeletedUseCase,
private val toggleTaskDoneUseCase: ToggleTaskDoneUseCase,
private val uiEventBus: UiEventBus
) : ViewModel() {
var dueTodayTasks by mutableStateOf<List<Task>>(emptyList())
private set
init {
getDueTodayTasks()
getDueTodayTasksUseCase()
.onEach { tasks ->
dueTodayTasks = tasks
}
.launchIn(viewModelScope)
}
fun updateTaskDone(taskId: Long) {
fun onTaskClicked(task: Task) {
viewModelScope.launch {
toggleTaskDone(taskId, true)
uiEventBus.send(UiEvent.EditTask(task))
}
}
fun updateTaskDone(taskId: Long) {
viewModelScope.launch {
toggleTaskDoneUseCase(taskId, true)
uiEventBus.send(
UiEvent.ShowUndoSnackbar(
message = "Task done",
undoAction = {
viewModelScope.launch {
toggleTaskDoneUseCase(taskId, false)
}
}
)
)
}
}
fun deleteTask(taskId: Long) {
viewModelScope.launch {
toggleTaskDeleted(taskId, true)
toggleTaskDeletedUseCase(taskId, true)
uiEventBus.send(
UiEvent.ShowUndoSnackbar(
message = "Task moved to recycle bin",
undoAction = {
viewModelScope.launch {
toggleTaskDeletedUseCase(taskId, false)
}
}
)
)
}
}
}

View File

@@ -7,7 +7,6 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavBackStackEntry
import com.wismna.geoffroy.donext.domain.model.AppDestination
import com.wismna.geoffroy.donext.domain.model.Task
import com.wismna.geoffroy.donext.domain.usecase.GetTaskListsUseCase
import com.wismna.geoffroy.donext.presentation.ui.events.UiEvent
import com.wismna.geoffroy.donext.presentation.ui.events.UiEventBus
@@ -47,10 +46,10 @@ class MainViewModel @Inject constructor(
AppDestination.ManageLists +
AppDestination.RecycleBin +
AppDestination.DueTodayList
isLoading = false
if (startDestination == AppDestination.ManageLists && destinations.isNotEmpty()) {
startDestination = destinations.first()
}
isLoading = false
}
.launchIn(viewModelScope)
}
@@ -68,13 +67,6 @@ class MainViewModel @Inject constructor(
}
}
fun onTaskClicked(task: Task) {
showTaskSheet = true
viewModelScope.launch {
uiEventBus.send(UiEvent.EditTask(task))
}
}
fun onDismissTaskSheet() {
showTaskSheet = false
viewModelScope.launch {

View File

@@ -11,6 +11,8 @@ import com.wismna.geoffroy.donext.domain.usecase.AddTaskListUseCase
import com.wismna.geoffroy.donext.domain.usecase.DeleteTaskListUseCase
import com.wismna.geoffroy.donext.domain.usecase.GetTaskListsUseCase
import com.wismna.geoffroy.donext.domain.usecase.UpdateTaskListUseCase
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.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -22,7 +24,8 @@ class ManageListsViewModel @Inject constructor(
getTaskListsUseCase: GetTaskListsUseCase,
private val addTaskListUseCase: AddTaskListUseCase,
private val updateTaskListUseCase: UpdateTaskListUseCase,
private val deleteTaskListUseCase: DeleteTaskListUseCase
private val deleteTaskListUseCase: DeleteTaskListUseCase,
private val uiEventBus: UiEventBus
) : ViewModel() {
var taskLists by mutableStateOf<List<TaskList>>(emptyList())
@@ -51,7 +54,18 @@ class ManageListsViewModel @Inject constructor(
}
fun deleteTaskList(taskListId: Long) {
viewModelScope.launch {
deleteTaskListUseCase(taskListId)
deleteTaskListUseCase(taskListId, true)
uiEventBus.send(
UiEvent.ShowUndoSnackbar(
message = "Task list moved to recycle bin",
undoAction = {
viewModelScope.launch {
deleteTaskListUseCase(taskListId, false)
}
}
)
)
}
}

View File

@@ -42,9 +42,11 @@ class MenuViewModel @Inject constructor(
.launchIn(viewModelScope)
}
fun navigateTo(route: String) {
viewModelScope.launch {
uiEventBus.send(UiEvent.Navigate(route))
fun navigateTo(route: String, currentRoute: String) {
if (route != currentRoute) {
viewModelScope.launch {
uiEventBus.send(UiEvent.Navigate(route))
}
}
}
}

View File

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

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.ToggleTaskDeletedUseCase
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 kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -18,10 +20,11 @@ import javax.inject.Inject
@HiltViewModel
class TaskListViewModel @Inject constructor(
getTasks: GetTasksForListUseCase,
savedStateHandle: SavedStateHandle,
private val toggleTaskDone: ToggleTaskDoneUseCase,
private val toggleTaskDeleted: ToggleTaskDeletedUseCase,
getTasksUseCase: GetTasksForListUseCase,
private val toggleTaskDoneUseCase: ToggleTaskDoneUseCase,
private val toggleTaskDeletedUseCase: ToggleTaskDeletedUseCase,
private val uiEventBus: UiEventBus
) : ViewModel() {
var tasks by mutableStateOf<List<Task>>(emptyList())
@@ -32,7 +35,7 @@ class TaskListViewModel @Inject constructor(
private val taskListId: Long = checkNotNull(savedStateHandle["taskListId"])
init {
getTasks(taskListId)
getTasksUseCase(taskListId)
.onEach { list ->
tasks = list
isLoading = false
@@ -40,14 +43,42 @@ 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 {
toggleTaskDone(taskId, isDone)
toggleTaskDoneUseCase(taskId, isDone)
uiEventBus.send(
UiEvent.ShowUndoSnackbar(
message = "Task done",
undoAction = {
viewModelScope.launch {
toggleTaskDoneUseCase(taskId, !isDone)
}
}
)
)
}
}
fun deleteTask(taskId: Long) {
viewModelScope.launch {
toggleTaskDeleted(taskId, true)
toggleTaskDeletedUseCase(taskId, true)
uiEventBus.send(
UiEvent.ShowUndoSnackbar(
message = "Task moved to recycle bin",
undoAction = {
viewModelScope.launch {
toggleTaskDeletedUseCase(taskId, false)
}
}
)
)
}
}
}

View File

@@ -45,8 +45,8 @@ class TaskViewModel @Inject constructor(
viewModelScope.launch {
uiEventBus.events.collect { event ->
when (event) {
is UiEvent.EditTask -> startEditTask(event.task)
is UiEvent.CreateNewTask -> startNewTask(event.taskListId)
is UiEvent.EditTask -> startEditTask(event.task)
is UiEvent.CloseTask -> reset()
else -> {}
}
@@ -56,28 +56,6 @@ class TaskViewModel @Inject constructor(
fun screenTitle(): String = if (isDeleted) "Task details" else if (isEditing()) "Edit Task" else "New Task"
fun isEditing(): Boolean = editingTaskId != null
private fun startNewTask(selectedListId: Long) {
editingTaskId = null
taskListId = selectedListId
title = ""
description = ""
priority = Priority.NORMAL
dueDate = null
isDeleted = false
}
private fun startEditTask(task: Task) {
editingTaskId = task.id
taskListId = task.taskListId
title = task.name
description = task.description ?: ""
priority = task.priority
dueDate = task.dueDate
isDone = task.isDone
isDeleted = task.isDeleted
}
fun onTitleChanged(value: String) { title = value }
fun onDescriptionChanged(value: String) { description = value }
fun onPriorityChanged(value: Priority) { priority = value }
@@ -104,7 +82,28 @@ class TaskViewModel @Inject constructor(
}
}
fun reset() {
private fun startNewTask(selectedListId: Long) {
editingTaskId = null
taskListId = selectedListId
title = ""
description = ""
priority = Priority.NORMAL
dueDate = null
isDeleted = false
}
private fun startEditTask(task: Task) {
editingTaskId = task.id
taskListId = task.taskListId
title = task.name
description = task.description ?: ""
priority = task.priority
dueDate = task.dueDate
isDone = task.isDone
isDeleted = task.isDeleted
}
private fun reset() {
editingTaskId = null
taskListId = null
title = ""