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"> <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

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

View File

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

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,17 +9,14 @@ 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.domain.model.Task
import com.wismna.geoffroy.donext.presentation.viewmodel.DueTodayViewModel import com.wismna.geoffroy.donext.presentation.viewmodel.DueTodayViewModel
@Composable @Composable
fun DueTodayTasksScreen( fun DueTodayTasksScreen(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: DueTodayViewModel = hiltViewModel(), viewModel: DueTodayViewModel = hiltViewModel(),
onTaskClick: (task: Task) -> Unit
) { ) {
val tasks = viewModel.dueTodayTasks val tasks = viewModel.dueTodayTasks
@@ -33,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)
) { ) {
@@ -41,16 +36,9 @@ fun DueTodayTasksScreen(
TaskItemScreen( TaskItemScreen(
modifier = Modifier.animateItem(), modifier = Modifier.animateItem(),
task = task, task = task,
onTaskClick = { onTaskClick(task) }, onSwipeLeft = { viewModel.updateTaskDone(task.id!!) },
onSwipeLeft = { onSwipeRight = { viewModel.deleteTask(task.id!!) },
viewModel.updateTaskDone(task.id!!) onTaskClick = { viewModel.onTaskClicked(task) }
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) {
@@ -108,13 +112,25 @@ fun AppContent(
navController.navigate(event.route) navController.navigate(event.route)
} }
is UiEvent.NavigateBack -> navController.popBackStack() 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( 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) },
@@ -207,29 +223,17 @@ fun AppContent(
} }
val taskListViewModel: TaskListViewModel = hiltViewModel(navBackStackEntry) val taskListViewModel: TaskListViewModel = hiltViewModel(navBackStackEntry)
TaskListScreen( TaskListScreen(viewModel = taskListViewModel)
viewModel = taskListViewModel,
onTaskClick = { task -> viewModel.onTaskClicked(task) }
)
} }
composable(AppDestination.ManageLists.route) { composable(AppDestination.ManageLists.route) {
ManageListsScreen( ManageListsScreen(modifier = Modifier)
modifier = Modifier,
showAddListSheet = {viewModel.showAddListSheet = true}
)
} }
composable(AppDestination.DueTodayList.route) { composable(AppDestination.DueTodayList.route) {
DueTodayTasksScreen ( DueTodayTasksScreen (modifier = Modifier)
modifier = Modifier,
onTaskClick = { task -> viewModel.onTaskClicked(task) }
)
} }
composable(AppDestination.RecycleBin.route) { composable(AppDestination.RecycleBin.route) {
RecycleBinScreen( RecycleBinScreen(modifier = Modifier)
modifier = Modifier,
onTaskClick = { task -> viewModel.onTaskClicked(task) }
)
} }
} }
} }

View File

@@ -6,10 +6,12 @@ import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith import androidx.compose.animation.togetherWith
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@@ -58,10 +60,21 @@ import sh.calvin.reorderable.rememberReorderableLazyListState
@Composable @Composable
fun ManageListsScreen( fun ManageListsScreen(
modifier: Modifier, modifier: Modifier,
viewModel: ManageListsViewModel = hiltViewModel(), viewModel: ManageListsViewModel = hiltViewModel()
showAddListSheet: () -> Unit
) { ) {
var lists = viewModel.taskLists.toMutableList() 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 lazyListState = rememberLazyListState()
val reorderState = rememberReorderableLazyListState( val reorderState = rememberReorderableLazyListState(
lazyListState = lazyListState, lazyListState = lazyListState,

View File

@@ -57,7 +57,7 @@ fun MenuScreen(
}, },
icon = { Icon(Icons.Default.Today, contentDescription = "Due Today") }, icon = { Icon(Icons.Default.Today, contentDescription = "Due Today") },
selected = currentDestination is AppDestination.DueTodayList, selected = currentDestination is AppDestination.DueTodayList,
onClick = { viewModel.navigateTo(AppDestination.DueTodayList.route) }, onClick = { viewModel.navigateTo(AppDestination.DueTodayList.route, currentDestination.route) },
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
) )
HorizontalDivider() HorizontalDivider()
@@ -73,7 +73,7 @@ fun MenuScreen(
icon = { Icon(Icons.Default.LineWeight, contentDescription = list.name) }, icon = { Icon(Icons.Default.LineWeight, contentDescription = list.name) },
selected = currentDestination is AppDestination.TaskList && selected = currentDestination is AppDestination.TaskList &&
currentDestination.taskListId == list.id, currentDestination.taskListId == list.id,
onClick = { viewModel.navigateTo("taskList/${list.id}") }, onClick = { viewModel.navigateTo("taskList/${list.id}", currentDestination.route) },
badge = { badge = {
if (list.overdueCount > 0) { if (list.overdueCount > 0) {
Badge { Text(list.overdueCount.toString()) } Badge { Text(list.overdueCount.toString()) }
@@ -90,14 +90,14 @@ fun MenuScreen(
label = { Text("Recycle Bin") }, label = { Text("Recycle Bin") },
icon = { Icon(Icons.Default.Delete, contentDescription = "Recycle Bin") }, icon = { Icon(Icons.Default.Delete, contentDescription = "Recycle Bin") },
selected = currentDestination is AppDestination.RecycleBin, selected = currentDestination is AppDestination.RecycleBin,
onClick = { viewModel.navigateTo(AppDestination.RecycleBin.route) }, onClick = { viewModel.navigateTo(AppDestination.RecycleBin.route, currentDestination.route) },
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
) )
NavigationDrawerItem( NavigationDrawerItem(
label = { Text("Edit Lists") }, label = { Text("Edit Lists") },
icon = { Icon(Icons.Default.EditNote, contentDescription = "Edit Lists") }, icon = { Icon(Icons.Default.EditNote, contentDescription = "Edit Lists") },
selected = currentDestination is AppDestination.ManageLists, selected = currentDestination is AppDestination.ManageLists,
onClick = { viewModel.navigateTo(AppDestination.ManageLists.route) }, onClick = { viewModel.navigateTo(AppDestination.ManageLists.route, currentDestination.route) },
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) 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.text.font.FontStyle
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.domain.model.Task
import com.wismna.geoffroy.donext.presentation.viewmodel.RecycleBinViewModel import com.wismna.geoffroy.donext.presentation.viewmodel.RecycleBinViewModel
@Composable @Composable
fun RecycleBinScreen( fun RecycleBinScreen(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: RecycleBinViewModel = hiltViewModel(), viewModel: RecycleBinViewModel = hiltViewModel(),
onTaskClick: (task: Task) -> Unit
) { ) {
val tasks = viewModel.deletedTasks val tasks = viewModel.deletedTasks
@@ -77,15 +75,13 @@ fun RecycleBinScreen(
TaskItemScreen( TaskItemScreen(
modifier = Modifier.animateItem(), modifier = Modifier.animateItem(),
task = item.task, task = item.task,
onTaskClick = { onTaskClick(item.task) }, onSwipeLeft = { viewModel.restore(item.task.id!!) },
onSwipeLeft = {
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()
} },
onTaskClick = { viewModel.onTaskClicked(item.task) }
) )
} }
} }

View File

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

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.Box
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,27 +9,38 @@ 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.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.domain.model.Task
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>(),
onTaskClick: (Task) -> Unit) { ) {
val tasks = viewModel.tasks 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 // 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),
@@ -43,15 +54,9 @@ fun TaskListScreen(
TaskItemScreen( TaskItemScreen(
modifier = Modifier.animateItem(), modifier = Modifier.animateItem(),
task = task, task = task,
onTaskClick = { onTaskClick(task) }, onSwipeLeft = { viewModel.updateTaskDone(task.id!!, true) },
onSwipeLeft = { onSwipeRight = { viewModel.deleteTask(task.id!!) },
viewModel.updateTaskDone(task.id!!, true) onTaskClick = { viewModel.onTaskClicked(task) }
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()
}
) )
} }
@@ -74,17 +79,10 @@ fun TaskListScreen(
TaskItemScreen( TaskItemScreen(
modifier = Modifier.animateItem(), modifier = Modifier.animateItem(),
task = task, task = task,
onTaskClick = { onTaskClick(task) }, onSwipeLeft = { viewModel.updateTaskDone(task.id!!, false) },
onSwipeLeft = { onSwipeRight = { viewModel.deleteTask(task.id!!) },
viewModel.updateTaskDone(task.id!!, false) onTaskClick = { viewModel.onTaskClicked(task) }
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

@@ -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.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,30 +19,60 @@ 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
} }
.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 {
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 recycle bin",
undoAction = {
viewModelScope.launch {
toggleTaskDeletedUseCase(taskId, false)
}
}
)
)
} }
} }
} }

View File

@@ -7,7 +7,6 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.navigation.NavBackStackEntry import androidx.navigation.NavBackStackEntry
import com.wismna.geoffroy.donext.domain.model.AppDestination 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.domain.usecase.GetTaskListsUseCase
import com.wismna.geoffroy.donext.presentation.ui.events.UiEvent import com.wismna.geoffroy.donext.presentation.ui.events.UiEvent
import com.wismna.geoffroy.donext.presentation.ui.events.UiEventBus import com.wismna.geoffroy.donext.presentation.ui.events.UiEventBus
@@ -47,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)
} }
@@ -68,13 +67,6 @@ class MainViewModel @Inject constructor(
} }
} }
fun onTaskClicked(task: Task) {
showTaskSheet = true
viewModelScope.launch {
uiEventBus.send(UiEvent.EditTask(task))
}
}
fun onDismissTaskSheet() { fun onDismissTaskSheet() {
showTaskSheet = false showTaskSheet = false
viewModelScope.launch { 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.DeleteTaskListUseCase
import com.wismna.geoffroy.donext.domain.usecase.GetTaskListsUseCase import com.wismna.geoffroy.donext.domain.usecase.GetTaskListsUseCase
import com.wismna.geoffroy.donext.domain.usecase.UpdateTaskListUseCase 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 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
@@ -22,7 +24,8 @@ class ManageListsViewModel @Inject constructor(
getTaskListsUseCase: GetTaskListsUseCase, getTaskListsUseCase: GetTaskListsUseCase,
private val addTaskListUseCase: AddTaskListUseCase, private val addTaskListUseCase: AddTaskListUseCase,
private val updateTaskListUseCase: UpdateTaskListUseCase, private val updateTaskListUseCase: UpdateTaskListUseCase,
private val deleteTaskListUseCase: DeleteTaskListUseCase private val deleteTaskListUseCase: DeleteTaskListUseCase,
private val uiEventBus: UiEventBus
) : ViewModel() { ) : ViewModel() {
var taskLists by mutableStateOf<List<TaskList>>(emptyList()) var taskLists by mutableStateOf<List<TaskList>>(emptyList())
@@ -51,7 +54,18 @@ class ManageListsViewModel @Inject constructor(
} }
fun deleteTaskList(taskListId: Long) { fun deleteTaskList(taskListId: Long) {
viewModelScope.launch { 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) .launchIn(viewModelScope)
} }
fun navigateTo(route: String) { fun navigateTo(route: String, currentRoute: String) {
if (route != currentRoute) {
viewModelScope.launch { viewModelScope.launch {
uiEventBus.send(UiEvent.Navigate(route)) uiEventBus.send(UiEvent.Navigate(route))
} }
} }
} }
}

View File

@@ -5,11 +5,14 @@ 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
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 +21,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())
@@ -31,8 +35,14 @@ class RecycleBinViewModel @Inject constructor(
loadDeletedTasks() loadDeletedTasks()
} }
fun onTaskClicked(task: Task) {
viewModelScope.launch {
uiEventBus.send(UiEvent.EditTask(task))
}
}
fun loadDeletedTasks() { fun loadDeletedTasks() {
getDeletedTasks() getDeletedTasksUseCase()
.onEach { tasks -> .onEach { tasks ->
deletedTasks = tasks deletedTasks = tasks
} }
@@ -41,14 +51,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

@@ -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
@@ -40,14 +43,42 @@ 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 {
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 recycle bin",
undoAction = {
viewModelScope.launch {
toggleTaskDeletedUseCase(taskId, false)
}
}
)
)
} }
} }
} }

View File

@@ -45,8 +45,8 @@ class TaskViewModel @Inject constructor(
viewModelScope.launch { viewModelScope.launch {
uiEventBus.events.collect { event -> uiEventBus.events.collect { event ->
when (event) { when (event) {
is UiEvent.EditTask -> startEditTask(event.task)
is UiEvent.CreateNewTask -> startNewTask(event.taskListId) is UiEvent.CreateNewTask -> startNewTask(event.taskListId)
is UiEvent.EditTask -> startEditTask(event.task)
is UiEvent.CloseTask -> reset() is UiEvent.CloseTask -> reset()
else -> {} 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 screenTitle(): String = if (isDeleted) "Task details" else if (isEditing()) "Edit Task" else "New Task"
fun isEditing(): Boolean = editingTaskId != null 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 onTitleChanged(value: String) { title = value }
fun onDescriptionChanged(value: String) { description = value } fun onDescriptionChanged(value: String) { description = value }
fun onPriorityChanged(value: Priority) { priority = 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 editingTaskId = null
taskListId = null taskListId = null
title = "" title = ""