Compare commits

...

3 Commits

Author SHA1 Message Date
Geoffroy Bonneville
e07f389fac Create UI events
Create a event bus singleton
Handle navigation through events
Handle task creation and edition through events
2025-10-09 22:00:27 -04:00
Geoffroy Bonneville
313e514624 Cleanup 2025-10-09 16:44:35 -04:00
Geoffroy Bonneville
8e78f9b464 Fix navigation issue when navigating back to a deleted list
Some refactoring
2025-10-09 16:43:27 -04:00
13 changed files with 183 additions and 81 deletions

View File

@@ -232,6 +232,18 @@
<option name="screenX" value="1080" /> <option name="screenX" value="1080" />
<option name="screenY" value="2640" /> <option name="screenY" value="2640" />
</PersistentDeviceSelectionData> </PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="36" />
<option name="brand" value="google" />
<option name="codename" value="blazer" />
<option name="id" value="blazer" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 10 Pro" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2410" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData> <PersistentDeviceSelectionData>
<option name="api" value="32" /> <option name="api" value="32" />
<option name="brand" value="google" /> <option name="brand" value="google" />
@@ -439,6 +451,18 @@
<option name="screenX" value="720" /> <option name="screenX" value="720" />
<option name="screenY" value="1600" /> <option name="screenY" value="1600" />
</PersistentDeviceSelectionData> </PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="36" />
<option name="brand" value="google" />
<option name="codename" value="frankel" />
<option name="id" value="frankel" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 10" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2424" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData> <PersistentDeviceSelectionData>
<option name="api" value="34" /> <option name="api" value="34" />
<option name="brand" value="samsung" /> <option name="brand" value="samsung" />
@@ -657,6 +681,18 @@
<option name="screenX" value="720" /> <option name="screenX" value="720" />
<option name="screenY" value="1600" /> <option name="screenY" value="1600" />
</PersistentDeviceSelectionData> </PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="36" />
<option name="brand" value="google" />
<option name="codename" value="mustang" />
<option name="id" value="mustang" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 10 Pro XL" />
<option name="screenDensity" value="390" />
<option name="screenX" value="1080" />
<option name="screenY" value="2404" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData> <PersistentDeviceSelectionData>
<option name="api" value="34" /> <option name="api" value="34" />
<option name="brand" value="samsung" /> <option name="brand" value="samsung" />
@@ -755,6 +791,18 @@
<option name="screenX" value="1080" /> <option name="screenX" value="1080" />
<option name="screenY" value="2340" /> <option name="screenY" value="2340" />
</PersistentDeviceSelectionData> </PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="36" />
<option name="brand" value="google" />
<option name="codename" value="rango" />
<option name="id" value="rango" />
<option name="labId" value="google" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 10 Pro Fold" />
<option name="screenDensity" value="390" />
<option name="screenX" value="2076" />
<option name="screenY" value="2152" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData> <PersistentDeviceSelectionData>
<option name="api" value="30" /> <option name="api" value="30" />
<option name="brand" value="google" /> <option name="brand" value="google" />

View File

@@ -15,7 +15,6 @@ 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.domain.model.Task
import com.wismna.geoffroy.donext.presentation.viewmodel.DueTodayViewModel import com.wismna.geoffroy.donext.presentation.viewmodel.DueTodayViewModel
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskItemViewModel
@Composable @Composable
fun DueTodayTasksScreen( fun DueTodayTasksScreen(
@@ -41,7 +40,7 @@ fun DueTodayTasksScreen(
items(tasks, key = { it.id!! }) { task -> items(tasks, key = { it.id!! }) { task ->
TaskItemScreen( TaskItemScreen(
modifier = Modifier.animateItem(), modifier = Modifier.animateItem(),
viewModel = TaskItemViewModel(task), task = task,
onTaskClick = { onTaskClick(task) }, onTaskClick = { onTaskClick(task) },
onSwipeLeft = { onSwipeLeft = {
viewModel.updateTaskDone(task.id!!) viewModel.updateTaskDone(task.id!!)

View File

@@ -18,6 +18,7 @@ import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DrawerState import androidx.compose.material3.DrawerState
import androidx.compose.material3.DrawerValue import androidx.compose.material3.DrawerValue
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
@@ -31,7 +32,10 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberDrawerState import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -46,22 +50,20 @@ import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument import androidx.navigation.navArgument
import com.wismna.geoffroy.donext.domain.model.AppDestination 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.MainViewModel
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskListViewModel import com.wismna.geoffroy.donext.presentation.viewmodel.TaskListViewModel
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskViewModel
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @Composable
fun MainScreen( fun MainScreen(
modifier: Modifier = Modifier,
viewModel: MainViewModel = hiltViewModel() viewModel: MainViewModel = hiltViewModel()
) { ) {
val navController = rememberNavController() val navController = rememberNavController()
val drawerState = rememberDrawerState(DrawerValue.Closed) val drawerState = rememberDrawerState(DrawerValue.Closed)
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
// TODO: find a way to get rid of this
val taskViewModel: TaskViewModel = hiltViewModel()
if (viewModel.isLoading) { if (viewModel.isLoading) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
@@ -71,7 +73,7 @@ fun MainScreen(
} }
if (viewModel.showTaskSheet) { if (viewModel.showTaskSheet) {
TaskBottomSheet(taskViewModel) { viewModel.showTaskSheet = false } TaskBottomSheet { viewModel.onDismissTaskSheet() }
} }
if (viewModel.showAddListSheet) { if (viewModel.showAddListSheet) {
AddListBottomSheet { viewModel.showAddListSheet = false } AddListBottomSheet { viewModel.showAddListSheet = false }
@@ -82,21 +84,11 @@ fun MainScreen(
ModalNavigationDrawer( ModalNavigationDrawer(
drawerContent = { drawerContent = {
MenuScreen ( MenuScreen (currentDestination = viewModel.currentDestination)
currentDestination = viewModel.currentDestination,
onNavigate = { route ->
scope.launch {
drawerState.close()
navController.navigate(route) {
restoreState = true
}
}
}
)
}, },
drawerState = drawerState drawerState = drawerState
) { ) {
AppContent(viewModel = viewModel, taskViewModel = taskViewModel, navController = navController, scope = scope, drawerState = drawerState) AppContent(viewModel = viewModel, navController = navController, scope = scope, drawerState = drawerState)
} }
} }
@@ -104,11 +96,22 @@ fun MainScreen(
fun AppContent( fun AppContent(
modifier : Modifier = Modifier, modifier : Modifier = Modifier,
viewModel: MainViewModel, viewModel: MainViewModel,
taskViewModel: TaskViewModel,
navController: NavHostController, navController: NavHostController,
scope: CoroutineScope, scope: CoroutineScope,
drawerState: DrawerState drawerState: DrawerState
) { ) {
LaunchedEffect(Unit) {
viewModel.uiEventBus.events.collectLatest { event ->
when (event) {
is UiEvent.Navigate -> {
drawerState.close()
navController.navigate(event.route)
}
is UiEvent.NavigateBack -> navController.popBackStack()
else -> {}
}
}
}
Scaffold( Scaffold(
modifier = modifier.background(MaterialTheme.colorScheme.primaryContainer), modifier = modifier.background(MaterialTheme.colorScheme.primaryContainer),
containerColor = Color.Transparent, containerColor = Color.Transparent,
@@ -123,7 +126,7 @@ fun AppContent(
navigationIcon = { navigationIcon = {
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onPrimaryContainer) { CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onPrimaryContainer) {
if (viewModel.currentDestination.showBackButton) { if (viewModel.currentDestination.showBackButton) {
IconButton(onClick = { navController.popBackStack() }) { IconButton(onClick = { viewModel.navigateBack() }) {
Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back") Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back")
} }
} else { } else {
@@ -154,9 +157,10 @@ fun AppContent(
floatingActionButton = { floatingActionButton = {
when (val dest = viewModel.currentDestination) { when (val dest = viewModel.currentDestination) {
is AppDestination.TaskList -> { is AppDestination.TaskList -> {
TaskListFab( ExtendedFloatingActionButton(
taskListId = dest.taskListId, onClick = { viewModel.onNewTaskButtonClicked(dest.taskListId) },
showBottomSheet = { viewModel.showTaskSheet = it } icon = { Icon(Icons.Filled.Add, "Create a task.") },
text = { Text("Create a task") },
) )
} }
else -> null else -> null
@@ -192,14 +196,20 @@ fun AppContent(
type = NavType.LongType type = NavType.LongType
}) })
) { navBackStackEntry -> ) { navBackStackEntry ->
// TODO: when task list has been deleted, we should not navigate to it event if in the stack val taskListId = navBackStackEntry.arguments?.getLong("taskListId") ?: return@composable
val listExists by remember(taskListId, viewModel.destinations) {
derivedStateOf { viewModel.doesListExist(taskListId) }
}
LaunchedEffect(listExists) {
if (!viewModel.doesListExist(taskListId)) {
viewModel.navigateBack()
}
}
val taskListViewModel: TaskListViewModel = hiltViewModel(navBackStackEntry) val taskListViewModel: TaskListViewModel = hiltViewModel(navBackStackEntry)
TaskListScreen( TaskListScreen(
viewModel = taskListViewModel, viewModel = taskListViewModel,
onTaskClick = { task -> onTaskClick = { task -> viewModel.onTaskClicked(task) }
taskViewModel.startEditTask(task)
viewModel.showTaskSheet = true
}
) )
} }
@@ -212,19 +222,13 @@ fun AppContent(
composable(AppDestination.DueTodayList.route) { composable(AppDestination.DueTodayList.route) {
DueTodayTasksScreen ( DueTodayTasksScreen (
modifier = Modifier, modifier = Modifier,
onTaskClick = { task -> onTaskClick = { task -> viewModel.onTaskClicked(task) }
taskViewModel.startEditTask(task)
viewModel.showTaskSheet = true
}
) )
} }
composable(AppDestination.RecycleBin.route) { composable(AppDestination.RecycleBin.route) {
RecycleBinScreen( RecycleBinScreen(
modifier = Modifier, modifier = Modifier,
onTaskClick = { task -> onTaskClick = { task -> viewModel.onTaskClicked(task) }
taskViewModel.startEditTask(task)
viewModel.showTaskSheet = true
}
) )
} }
} }

View File

@@ -31,7 +31,6 @@ import com.wismna.geoffroy.donext.presentation.viewmodel.MenuViewModel
fun MenuScreen( fun MenuScreen(
viewModel: MenuViewModel = hiltViewModel(), viewModel: MenuViewModel = hiltViewModel(),
currentDestination: AppDestination, currentDestination: AppDestination,
onNavigate: (String) -> Unit
) { ) {
ModalDrawerSheet( ModalDrawerSheet(
drawerContainerColor = MaterialTheme.colorScheme.surfaceVariant, drawerContainerColor = MaterialTheme.colorScheme.surfaceVariant,
@@ -58,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 = { onNavigate(AppDestination.DueTodayList.route) }, onClick = { viewModel.navigateTo(AppDestination.DueTodayList.route) },
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
) )
HorizontalDivider() HorizontalDivider()
@@ -74,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 = { onNavigate("taskList/${list.id}") }, onClick = { viewModel.navigateTo("taskList/${list.id}") },
badge = { badge = {
if (list.overdueCount > 0) { if (list.overdueCount > 0) {
Badge { Text(list.overdueCount.toString()) } Badge { Text(list.overdueCount.toString()) }
@@ -91,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 = { onNavigate(AppDestination.RecycleBin.route) }, onClick = { viewModel.navigateTo(AppDestination.RecycleBin.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 = { onNavigate(AppDestination.ManageLists.route) }, onClick = { viewModel.navigateTo(AppDestination.ManageLists.route) },
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
) )
} }

View File

@@ -31,7 +31,6 @@ 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.domain.model.Task
import com.wismna.geoffroy.donext.presentation.viewmodel.RecycleBinViewModel import com.wismna.geoffroy.donext.presentation.viewmodel.RecycleBinViewModel
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskItemViewModel
@Composable @Composable
fun RecycleBinScreen( fun RecycleBinScreen(
@@ -77,7 +76,7 @@ fun RecycleBinScreen(
items(items, key = { it.task.id!! }) { item -> items(items, key = { it.task.id!! }) { item ->
TaskItemScreen( TaskItemScreen(
modifier = Modifier.animateItem(), modifier = Modifier.animateItem(),
viewModel = TaskItemViewModel(item.task), task = item.task,
onTaskClick = { onTaskClick(item.task) }, onTaskClick = { onTaskClick(item.task) },
onSwipeLeft = { onSwipeLeft = {
viewModel.restore(item.task.id!!) viewModel.restore(item.task.id!!)

View File

@@ -38,16 +38,18 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.wismna.geoffroy.donext.domain.model.Priority import com.wismna.geoffroy.donext.domain.model.Priority
import com.wismna.geoffroy.donext.domain.model.Task
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskItemViewModel import com.wismna.geoffroy.donext.presentation.viewmodel.TaskItemViewModel
@Composable @Composable
fun TaskItemScreen( fun TaskItemScreen(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: TaskItemViewModel, task: Task,
onTaskClick: (taskId: Long) -> Unit, onTaskClick: (taskId: Long) -> Unit,
onSwipeLeft: () -> Unit, onSwipeLeft: () -> Unit,
onSwipeRight: () -> Unit onSwipeRight: () -> Unit
) { ) {
val viewModel = TaskItemViewModel(task)
// TODO: change this // TODO: change this
val dismissState = rememberSwipeToDismissBoxState( val dismissState = rememberSwipeToDismissBoxState(
confirmValueChange = { confirmValueChange = {

View File

@@ -8,12 +8,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding 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.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
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
@@ -21,13 +16,10 @@ 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.domain.model.Task
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskItemViewModel
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskListViewModel import com.wismna.geoffroy.donext.presentation.viewmodel.TaskListViewModel
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskViewModel
@Composable @Composable
fun TaskListScreen( fun TaskListScreen(
modifier: Modifier = Modifier,
viewModel: TaskListViewModel = hiltViewModel<TaskListViewModel>(), viewModel: TaskListViewModel = hiltViewModel<TaskListViewModel>(),
onTaskClick: (Task) -> Unit) { onTaskClick: (Task) -> Unit) {
val tasks = viewModel.tasks val tasks = viewModel.tasks
@@ -50,7 +42,7 @@ fun TaskListScreen(
) { task -> ) { task ->
TaskItemScreen( TaskItemScreen(
modifier = Modifier.animateItem(), modifier = Modifier.animateItem(),
viewModel = TaskItemViewModel(task), task = task,
onTaskClick = { onTaskClick(task) }, onTaskClick = { onTaskClick(task) },
onSwipeLeft = { onSwipeLeft = {
viewModel.updateTaskDone(task.id!!, true) viewModel.updateTaskDone(task.id!!, true)
@@ -81,7 +73,7 @@ fun TaskListScreen(
) { task -> ) { task ->
TaskItemScreen( TaskItemScreen(
modifier = Modifier.animateItem(), modifier = Modifier.animateItem(),
viewModel = TaskItemViewModel(task), task = task,
onTaskClick = { onTaskClick(task) }, onTaskClick = { onTaskClick(task) },
onSwipeLeft = { onSwipeLeft = {
viewModel.updateTaskDone(task.id!!, false) viewModel.updateTaskDone(task.id!!, false)
@@ -95,20 +87,4 @@ fun TaskListScreen(
} }
} }
}
@Composable
fun TaskListFab(
taskListId: Long,
viewModel: TaskViewModel = hiltViewModel(),
showBottomSheet: (Boolean) -> Unit = {}
) {
ExtendedFloatingActionButton(
onClick = {
viewModel.startNewTask(taskListId)
showBottomSheet(true)
},
icon = { Icon(Icons.Filled.Add, "Create a task.") },
text = { Text("Create a task") },
)
} }

View File

@@ -40,6 +40,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.wismna.geoffroy.donext.domain.extension.toLocalDate import com.wismna.geoffroy.donext.domain.extension.toLocalDate
import com.wismna.geoffroy.donext.domain.model.Priority import com.wismna.geoffroy.donext.domain.model.Priority
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskViewModel import com.wismna.geoffroy.donext.presentation.viewmodel.TaskViewModel
@@ -53,7 +54,7 @@ import java.time.format.FormatStyle
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun TaskBottomSheet( fun TaskBottomSheet(
viewModel: TaskViewModel, viewModel: TaskViewModel = hiltViewModel(),
onDismiss: () -> Unit onDismiss: () -> Unit
) { ) {
val titleFocusRequester = remember { FocusRequester() } val titleFocusRequester = remember { FocusRequester() }

View File

@@ -0,0 +1,13 @@
package com.wismna.geoffroy.donext.presentation.ui.events
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()
}

View File

@@ -7,15 +7,20 @@ 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.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
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class MainViewModel @Inject constructor( class MainViewModel @Inject constructor(
getTaskListsUseCase: GetTaskListsUseCase getTaskListsUseCase: GetTaskListsUseCase,
val uiEventBus: UiEventBus
) : ViewModel() { ) : ViewModel() {
var isLoading by mutableStateOf(true) var isLoading by mutableStateOf(true)
@@ -50,6 +55,33 @@ class MainViewModel @Inject constructor(
.launchIn(viewModelScope) .launchIn(viewModelScope)
} }
fun navigateBack() {
viewModelScope.launch {
uiEventBus.send(UiEvent.NavigateBack)
}
}
fun onNewTaskButtonClicked(taskLisId: Long) {
showTaskSheet = true
viewModelScope.launch {
uiEventBus.send(UiEvent.CreateNewTask(taskLisId))
}
}
fun onTaskClicked(task: Task) {
showTaskSheet = true
viewModelScope.launch {
uiEventBus.send(UiEvent.EditTask(task))
}
}
fun onDismissTaskSheet() {
showTaskSheet = false
viewModelScope.launch {
uiEventBus.send(UiEvent.CloseTask)
}
}
fun setCurrentDestination(navBackStackEntry: NavBackStackEntry?) { fun setCurrentDestination(navBackStackEntry: NavBackStackEntry?) {
val route = navBackStackEntry?.destination?.route val route = navBackStackEntry?.destination?.route
val taskListId = navBackStackEntry?.arguments?.getLong("taskListId") val taskListId = navBackStackEntry?.arguments?.getLong("taskListId")
@@ -61,4 +93,10 @@ class MainViewModel @Inject constructor(
} }
} ?: startDestination } ?: startDestination
} }
fun doesListExist(taskListId: Long): Boolean {
return destinations.any { dest ->
dest is AppDestination.TaskList && dest.taskListId == taskListId
}
}
} }

View File

@@ -9,17 +9,20 @@ import androidx.lifecycle.viewModelScope
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
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
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class MenuViewModel @Inject constructor( class MenuViewModel @Inject constructor(
getTaskListsWithOverdue: GetTaskListsWithOverdueUseCase, getTaskListsWithOverdue: GetTaskListsWithOverdueUseCase,
getDueTodayTasks: GetDueTodayTasksUseCase getDueTodayTasks: GetDueTodayTasksUseCase,
private val uiEventBus: UiEventBus
) : ViewModel() { ) : ViewModel() {
var taskLists by mutableStateOf<List<TaskListWithOverdue>>(emptyList()) var taskLists by mutableStateOf<List<TaskListWithOverdue>>(emptyList())
private set private set
@@ -38,4 +41,10 @@ class MenuViewModel @Inject constructor(
} }
.launchIn(viewModelScope) .launchIn(viewModelScope)
} }
fun navigateTo(route: String) {
viewModelScope.launch {
uiEventBus.send(UiEvent.Navigate(route))
}
}
} }

View File

@@ -19,9 +19,9 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class TaskListViewModel @Inject constructor( class TaskListViewModel @Inject constructor(
getTasks: GetTasksForListUseCase, getTasks: GetTasksForListUseCase,
savedStateHandle: SavedStateHandle,
private val toggleTaskDone: ToggleTaskDoneUseCase, private val toggleTaskDone: ToggleTaskDoneUseCase,
private val toggleTaskDeleted: ToggleTaskDeletedUseCase, private val toggleTaskDeleted: ToggleTaskDeletedUseCase,
savedStateHandle: SavedStateHandle
) : ViewModel() { ) : ViewModel() {
var tasks by mutableStateOf<List<Task>>(emptyList()) var tasks by mutableStateOf<List<Task>>(emptyList())

View File

@@ -9,6 +9,8 @@ import com.wismna.geoffroy.donext.domain.model.Priority
import com.wismna.geoffroy.donext.domain.model.Task import com.wismna.geoffroy.donext.domain.model.Task
import com.wismna.geoffroy.donext.domain.usecase.AddTaskUseCase import com.wismna.geoffroy.donext.domain.usecase.AddTaskUseCase
import com.wismna.geoffroy.donext.domain.usecase.UpdateTaskUseCase import com.wismna.geoffroy.donext.domain.usecase.UpdateTaskUseCase
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.launch import kotlinx.coroutines.launch
import java.time.Instant import java.time.Instant
@@ -19,7 +21,8 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class TaskViewModel @Inject constructor( class TaskViewModel @Inject constructor(
private val createTaskUseCase: AddTaskUseCase, private val createTaskUseCase: AddTaskUseCase,
private val updateTaskUseCase: UpdateTaskUseCase private val updateTaskUseCase: UpdateTaskUseCase,
private val uiEventBus: UiEventBus
) : ViewModel() { ) : ViewModel() {
var title by mutableStateOf("") var title by mutableStateOf("")
@@ -38,10 +41,23 @@ class TaskViewModel @Inject constructor(
private var editingTaskId: Long? = null private var editingTaskId: Long? = null
private var taskListId: Long? = null private var taskListId: Long? = null
init {
viewModelScope.launch {
uiEventBus.events.collect { event ->
when (event) {
is UiEvent.EditTask -> startEditTask(event.task)
is UiEvent.CreateNewTask -> startNewTask(event.taskListId)
is UiEvent.CloseTask -> reset()
else -> {}
}
}
}
}
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
fun startNewTask(selectedListId: Long) { private fun startNewTask(selectedListId: Long) {
editingTaskId = null editingTaskId = null
taskListId = selectedListId taskListId = selectedListId
title = "" title = ""
@@ -51,7 +67,7 @@ class TaskViewModel @Inject constructor(
isDeleted = false isDeleted = false
} }
fun startEditTask(task: Task) { private fun startEditTask(task: Task) {
editingTaskId = task.id editingTaskId = task.id
taskListId = task.taskListId taskListId = task.taskListId
title = task.name title = task.name
@@ -84,8 +100,6 @@ class TaskViewModel @Inject constructor(
} else { } else {
createTaskUseCase(taskListId!!, title, description, priority, dueDate) createTaskUseCase(taskListId!!, title, description, priority, dueDate)
} }
// reset state after save
reset()
onDone?.invoke() onDone?.invoke()
} }
} }