mirror of
https://github.com/wismna/DoNext.git
synced 2025-10-03 15:40:14 -04:00
Refactor MainScreen and MainViewModel
Add list button now open bottom sheet Add list bottom sheet works but design is WIP WIP on inline list editing
This commit is contained in:
@@ -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,
|
||||
)
|
||||
}
|
@@ -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<TaskListWithOverdue>
|
||||
): 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
|
||||
}
|
||||
}
|
||||
}
|
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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<TaskListWithOverdue>,
|
||||
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) },
|
||||
|
@@ -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<List<TaskListWithOverdue>>(emptyList())
|
||||
private set
|
||||
/*val destinations: List<AppDestination>
|
||||
get() = taskLists.map { AppDestination.TaskList(it.id, it.name) } +
|
||||
AppDestination.ManageLists*/
|
||||
var isLoading by mutableStateOf(true)
|
||||
private set
|
||||
|
||||
var destinations by mutableStateOf<List<AppDestination>>(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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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<List<TaskListWithOverdue>>(emptyList())
|
||||
private set
|
||||
|
||||
init {
|
||||
getTaskListsWithOverdue()
|
||||
.onEach { lists ->
|
||||
taskLists = lists
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user