diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/model/AppDestination.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/model/AppDestination.kt new file mode 100644 index 0000000..ebac09c --- /dev/null +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/model/AppDestination.kt @@ -0,0 +1,18 @@ +package com.wismna.geoffroy.donext.domain.model + +sealed class AppDestination( + val route: String, + val title: String, + val showBackButton: Boolean = false, +) { + data class TaskList(val taskListId: Long, val name: String) : AppDestination( + route = "taskList/$taskListId", + title = name, + ) + + object ManageLists : AppDestination( + route = "manageLists", + title = "Manage Lists", + showBackButton = true, + ) +} diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/MainScreen.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/MainScreen.kt index da1584d..31739e8 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/MainScreen.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/MainScreen.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Menu import androidx.compose.material3.CircularProgressIndicator @@ -39,45 +40,26 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.navigation.NavBackStackEntry import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument -import com.wismna.geoffroy.donext.domain.model.TaskListWithOverdue +import com.wismna.geoffroy.donext.domain.model.AppDestination import com.wismna.geoffroy.donext.presentation.viewmodel.MainViewModel import com.wismna.geoffroy.donext.presentation.viewmodel.TaskListViewModel import com.wismna.geoffroy.donext.presentation.viewmodel.TaskViewModel import kotlinx.coroutines.launch -sealed class AppDestination( - val route: String, - val title: String, - val showBackButton: Boolean = false, - val actions: @Composable (() -> Unit)? = null -) { - data class TaskList(val taskListId: Long, val name: String) : AppDestination( - route = "taskList/$taskListId", - title = name, - ) - - object ManageLists : AppDestination( - route = "manageLists", - title = "Manage Lists", - showBackButton = true, - actions = { ManageListsActions() } - ) -} - @Composable fun MainScreen( modifier: Modifier = Modifier, viewModel: MainViewModel = hiltViewModel() ) { val navController = rememberNavController() - var showBottomSheet by remember { mutableStateOf(false) } + var showTaskSheet by remember { mutableStateOf(false) } + var showAddListSheet by remember { mutableStateOf(false) } val drawerState = rememberDrawerState(DrawerValue.Closed) val scope = rememberCoroutineScope() val taskViewModel: TaskViewModel = hiltViewModel() @@ -89,20 +71,22 @@ fun MainScreen( return } - val firstListId = viewModel.taskLists.firstOrNull()?.id - if (showBottomSheet) { - TaskBottomSheet(taskViewModel) { showBottomSheet = false } + val startDestination = viewModel.destinations.firstOrNull() ?: AppDestination.ManageLists + if (showTaskSheet) { + TaskBottomSheet(taskViewModel) { showTaskSheet = false } + } + if (showAddListSheet) { + AddListBottomSheet { showAddListSheet = false } } val navBackStackEntry by navController.currentBackStackEntryAsState() - val currentDestination = remember(navBackStackEntry, viewModel.taskLists) { - deriveDestination(navBackStackEntry, viewModel.taskLists) - } + val currentDestination = + viewModel.deriveDestination(navBackStackEntry?.destination?.route) + ?: startDestination ModalNavigationDrawer( drawerContent = { MenuScreen ( - taskLists = viewModel.taskLists, currentDestination = currentDestination, onNavigate = { route -> scope.launch { drawerState.close() } @@ -119,7 +103,7 @@ fun MainScreen( containerColor = Color.Transparent, topBar = { TopAppBar( - title = { Text(currentDestination.title) }, + title = { Text(currentDestination!!.title) }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.primaryContainer, titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer, @@ -127,7 +111,7 @@ fun MainScreen( ), navigationIcon = { CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onPrimaryContainer) { - if (currentDestination.showBackButton) { + if (currentDestination!!.showBackButton) { IconButton(onClick = { navController.popBackStack() }) { Icon(Icons.Default.ArrowBack, contentDescription = "Back") } @@ -141,7 +125,13 @@ fun MainScreen( } } }, - actions = { currentDestination.actions?.invoke() } + actions = { + if (currentDestination is AppDestination.ManageLists) { + IconButton(onClick = { showAddListSheet = true }) { + Icon(Icons.Default.Add, contentDescription = "Add List") + } + } + } ) }, floatingActionButton = { @@ -149,7 +139,7 @@ fun MainScreen( is AppDestination.TaskList -> { TaskListFab( taskListId = dest.taskListId, - showBottomSheet = { showBottomSheet = it } + showBottomSheet = { showTaskSheet = it } ) } else -> null @@ -165,8 +155,7 @@ fun MainScreen( ) { NavHost( navController = navController, - startDestination = firstListId?.let { "taskList/$it" } - ?: AppDestination.ManageLists.route, + startDestination = startDestination.route, enterTransition = { slideInHorizontally(initialOffsetX = { fullWidth -> fullWidth }, animationSpec = tween(300)) }, @@ -180,7 +169,7 @@ fun MainScreen( slideOutHorizontally(targetOffsetX = { fullWidth -> fullWidth }, animationSpec = tween(300)) } ) { - viewModel.taskLists.forEach { list -> + viewModel.destinations.forEach { destination -> composable( route = "taskList/{taskListId}", arguments = listOf(navArgument("taskListId") { @@ -192,40 +181,20 @@ fun MainScreen( viewModel = viewModel, onTaskClick = { task -> taskViewModel.startEditTask(task) - showBottomSheet = true + showTaskSheet = true } ) } } composable(AppDestination.ManageLists.route) { - ManageListsScreen(modifier = Modifier) + ManageListsScreen( + modifier = Modifier, + showAddListSheet = {showAddListSheet = true} + ) } } } } } -} - -fun deriveDestination( - navBackStackEntry: NavBackStackEntry?, - taskLists: List -): AppDestination { - val route = navBackStackEntry?.destination?.route - - return when { - route == AppDestination.ManageLists.route -> AppDestination.ManageLists - route?.startsWith("taskList/") == true || route == "taskList/{taskListId}" -> { - val idArg = navBackStackEntry.arguments?.getLong("taskListId") - val taskListId = idArg ?: route.substringAfter("taskList/", "").toLongOrNull() - val matching = taskLists.find { it.id == taskListId } - matching?.let { AppDestination.TaskList(it.id, it.name) } - ?: taskLists.firstOrNull()?.let { AppDestination.TaskList(it.id, it.name) } - ?: AppDestination.ManageLists - } - else -> { - taskLists.firstOrNull()?.let { AppDestination.TaskList(it.id, it.name) } - ?: AppDestination.ManageLists - } - } } \ No newline at end of file diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/ManageListsScreen.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/ManageListsScreen.kt index 0c4b3c8..d729b84 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/ManageListsScreen.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/ManageListsScreen.kt @@ -1,14 +1,20 @@ package com.wismna.geoffroy.donext.presentation.screen +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Edit +import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -16,24 +22,38 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import com.wismna.geoffroy.donext.domain.model.TaskList import com.wismna.geoffroy.donext.presentation.viewmodel.ManageListsViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable fun ManageListsScreen( modifier: Modifier, - viewModel: ManageListsViewModel = hiltViewModel() + viewModel: ManageListsViewModel = hiltViewModel(), + showAddListSheet: () -> Unit ) { val lists = viewModel.taskLists LazyColumn(modifier = modifier.fillMaxWidth().padding()) { itemsIndexed(lists, key = { _, list -> list.id!! }) { index, list -> ListItem( + modifier = Modifier.animateItem(), headlineContent = { Text(list.name) }, trailingContent = { Row { @@ -54,10 +74,83 @@ fun ManageListsScreen( } @Composable -fun ManageListsActions( - viewModel: ManageListsViewModel = hiltViewModel() +fun EditableListRow( + list: TaskList, + onNameChange: (String) -> Unit, + //onTypeChange: (ListType) -> Unit, + onDone: () -> Unit ) { - IconButton(onClick = { viewModel.createTaskList("Test", 1) }) { - Icon(Icons.Default.Add, contentDescription = "Add List") + var name by remember { mutableStateOf(list.name) } + //var type by remember { mutableStateOf(list.type) } + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth().padding(8.dp) + ) { + TextField( + value = name, + onValueChange = { name = it }, + modifier = Modifier.weight(1f), + singleLine = true + ) + // TODO: implement type + //DropdownSelector(selected = type, onSelect = { type = it; onTypeChange(it) }) + IconButton(onClick = onDone) { + Icon(Icons.Default.Check, contentDescription = "Save") + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AddListBottomSheet( + viewModel: ManageListsViewModel = hiltViewModel(), + onDismiss: () -> Unit +) { + val titleFocusRequester = remember { FocusRequester() } + + LaunchedEffect(Unit) { + titleFocusRequester.requestFocus() + } + + ModalBottomSheet(onDismissRequest = onDismiss) { + var name by remember { mutableStateOf("") } + //var type by remember { mutableStateOf(ListType.Default) } + //var description by remember { mutableStateOf("") } + + Column(modifier = Modifier.padding(16.dp)) { + Text("Create New List", style = MaterialTheme.typography.titleMedium) + + Spacer(Modifier.height(8.dp)) + TextField( + value = name, + onValueChange = { name = it }, + label = { Text("List Name") }, + singleLine = true + ) + + Spacer(Modifier.height(8.dp)) + //DropdownSelector(selected = type, onSelect = { type = it }) + + /*Spacer(Modifier.height(8.dp)) + TextField( + value = description, + onValueChange = { description = it }, + label = { Text("Description") }, + maxLines = 3 + )*/ + + Spacer(Modifier.height(16.dp)) + Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth()) { + TextButton(onClick = onDismiss) { Text("Cancel") } + Spacer(Modifier.width(8.dp)) + Button(onClick = { + viewModel.createTaskList(name/*, type, description*/, 1) + onDismiss() + }) { + Text("Add") + } + } + } } } \ No newline at end of file diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/MenuScreen.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/MenuScreen.kt index db7bd29..b432975 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/MenuScreen.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/MenuScreen.kt @@ -18,11 +18,13 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.wismna.geoffroy.donext.domain.model.TaskListWithOverdue +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import com.wismna.geoffroy.donext.domain.model.AppDestination +import com.wismna.geoffroy.donext.presentation.viewmodel.MenuViewModel @Composable fun MenuScreen( - taskLists: List, + viewModel: MenuViewModel = hiltViewModel(), currentDestination: AppDestination, onNavigate: (String) -> Unit ) { @@ -42,7 +44,7 @@ fun MenuScreen( style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(16.dp) ) - taskLists.forEach { list -> + viewModel.taskLists.forEach { list -> NavigationDrawerItem( label = { Text(list.name) }, icon = { Icon(Icons.Default.List, contentDescription = list.name) }, diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/MainViewModel.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/MainViewModel.kt index 2699f1a..48c6ef3 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/MainViewModel.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/MainViewModel.kt @@ -5,8 +5,8 @@ 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.TaskListWithOverdue -import com.wismna.geoffroy.donext.domain.usecase.GetTaskListsWithOverdueUseCase +import com.wismna.geoffroy.donext.domain.model.AppDestination +import com.wismna.geoffroy.donext.domain.usecase.GetTaskListsUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -14,23 +14,33 @@ import javax.inject.Inject @HiltViewModel class MainViewModel @Inject constructor( - getTaskListsWithOverdue: GetTaskListsWithOverdueUseCase + getTaskLists: GetTaskListsUseCase ) : ViewModel() { - var taskLists by mutableStateOf>(emptyList()) - private set - /*val destinations: List - get() = taskLists.map { AppDestination.TaskList(it.id, it.name) } + - AppDestination.ManageLists*/ var isLoading by mutableStateOf(true) private set + var destinations by mutableStateOf>(emptyList()) + private set + init { - getTaskListsWithOverdue() + getTaskLists() .onEach { lists -> - taskLists = lists + destinations = lists.map { taskList -> + AppDestination.TaskList(taskList.id!!, taskList.name) + } + AppDestination.ManageLists isLoading = false } .launchIn(viewModelScope) } + + fun deriveDestination(route: String?): AppDestination? { + if (route == null) return null + return destinations.firstOrNull { dest -> + when (dest) { + is AppDestination.TaskList -> route.startsWith("tasklist/") + else -> dest.route == route + } + } + } } \ No newline at end of file diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/MenuViewModel.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/MenuViewModel.kt new file mode 100644 index 0000000..c2e2097 --- /dev/null +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/MenuViewModel.kt @@ -0,0 +1,30 @@ +package com.wismna.geoffroy.donext.presentation.viewmodel + +import androidx.compose.runtime.getValue +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.TaskListWithOverdue +import com.wismna.geoffroy.donext.domain.usecase.GetTaskListsWithOverdueUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import javax.inject.Inject + +@HiltViewModel +class MenuViewModel @Inject constructor( + getTaskListsWithOverdue: GetTaskListsWithOverdueUseCase +) : ViewModel() { + + var taskLists by mutableStateOf>(emptyList()) + private set + + init { + getTaskListsWithOverdue() + .onEach { lists -> + taskLists = lists + } + .launchIn(viewModelScope) + } +}