Improve large layouts by adding task pane and new list dialog

This commit is contained in:
Geoffroy Bonneville
2025-10-22 22:52:34 -04:00
parent 4e2f3c720c
commit dc46386e0d
5 changed files with 245 additions and 182 deletions

View File

@@ -160,6 +160,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="35" />
<option name="brand" value="samsung" />
<option name="codename" value="a36xq" />
<option name="id" value="a36xq" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="SM-A366E" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData> <PersistentDeviceSelectionData>
<option name="api" value="34" /> <option name="api" value="34" />
<option name="brand" value="google" /> <option name="brand" value="google" />

View File

@@ -8,8 +8,13 @@ import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
@@ -24,6 +29,7 @@ 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
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.PermanentNavigationDrawer import androidx.compose.material3.PermanentNavigationDrawer
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
@@ -35,7 +41,9 @@ 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
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.VerticalDivider
import androidx.compose.material3.rememberDrawerState import androidx.compose.material3.rememberDrawerState
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.material3.windowsizeclass.WindowSizeClass import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -50,6 +58,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.NavType import androidx.navigation.NavType
@@ -79,33 +88,90 @@ fun MainScreen(
return return
} }
if (viewModel.showTaskSheet) {
TaskBottomSheet { viewModel.onDismissTaskSheet() }
}
if (viewModel.showAddListSheet) {
AddListBottomSheet { viewModel.showAddListSheet = false }
}
val navBackStackEntry by navController.currentBackStackEntryAsState() val navBackStackEntry by navController.currentBackStackEntryAsState()
viewModel.setCurrentDestination(navBackStackEntry) viewModel.setCurrentDestination(navBackStackEntry)
val isExpandedScreen = windowSizeClass.widthSizeClass >= WindowWidthSizeClass.Medium val isExpandedScreen = windowSizeClass.widthSizeClass >= WindowWidthSizeClass.Medium
val orientation = LocalConfiguration.current.orientation val orientation = LocalConfiguration.current.orientation
val isLandscape = orientation == Configuration.ORIENTATION_LANDSCAPE val isLandscape = orientation == Configuration.ORIENTATION_LANDSCAPE
val showPermanentDrawer = isExpandedScreen || isLandscape val isLargeLayout = isExpandedScreen || isLandscape
if (showPermanentDrawer) { if (isLargeLayout) {
PermanentNavigationDrawer( PermanentNavigationDrawer(
drawerContent = { drawerContent = {
MenuScreen(currentDestination = viewModel.currentDestination) MenuScreen(
modifier = Modifier.width(240.dp),
currentDestination = viewModel.currentDestination
)
} }
) { ) {
AppContent( Row(Modifier.fillMaxSize()) {
viewModel = viewModel, // Main app content area
navController = navController Box(
) modifier = Modifier
.weight(1f)
.fillMaxHeight()
) {
AppContent(
viewModel = viewModel,
navController = navController
)
}
// Show side "details" pane for the task editor when requested
if (viewModel.showTaskSheet) {
VerticalDivider(
modifier = Modifier
.fillMaxHeight()
.width(1.dp)
)
Box(
modifier = Modifier
.width(380.dp)
.fillMaxHeight()
.background(MaterialTheme.colorScheme.surfaceContainerHigh)
) {
TaskScreen { viewModel.onDismissTaskSheet() }
}
}
if (viewModel.showAddListSheet) {
Dialog(onDismissRequest = { viewModel.showAddListSheet = false }) {
Surface(
shape = RoundedCornerShape(16.dp),
tonalElevation = 6.dp,
modifier = Modifier
.widthIn(max = 400.dp)
.wrapContentHeight()
.padding(16.dp)
) {
AddListScreen { viewModel.showAddListSheet = false }
}
}
}
}
} }
} else { } else {
if (viewModel.showTaskSheet) {
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val scope = rememberCoroutineScope()
ModalBottomSheet(
onDismissRequest = {
scope.launch {
sheetState.hide()
viewModel.onDismissTaskSheet()
}
},
sheetState = sheetState) {
TaskScreen { viewModel.onDismissTaskSheet() }
}
}
if (viewModel.showAddListSheet) {
ModalBottomSheet(onDismissRequest = { viewModel.showAddListSheet = false }) {
AddListScreen { viewModel.showAddListSheet = false }
}
}
val drawerState = rememberDrawerState(DrawerValue.Closed) val drawerState = rememberDrawerState(DrawerValue.Closed)
ModalNavigationDrawer( ModalNavigationDrawer(
drawerContent = { drawerContent = {

View File

@@ -32,7 +32,6 @@ 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
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -217,7 +216,7 @@ fun ManageListsScreen(
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AddListBottomSheet( fun AddListScreen(
viewModel: ManageListsViewModel = hiltViewModel(), viewModel: ManageListsViewModel = hiltViewModel(),
onDismiss: () -> Unit onDismiss: () -> Unit
) { ) {
@@ -227,35 +226,34 @@ fun AddListBottomSheet(
titleFocusRequester.requestFocus() titleFocusRequester.requestFocus()
} }
ModalBottomSheet(onDismissRequest = onDismiss) { var name by remember { mutableStateOf("") }
var name by remember { mutableStateOf("") } //var type by remember { mutableStateOf(ListType.Default) }
//var type by remember { mutableStateOf(ListType.Default) } //var description by remember { mutableStateOf("") }
//var description by remember { mutableStateOf("") }
Column(modifier = Modifier.padding(16.dp)) { Column(modifier = Modifier.padding(16.dp)) {
Text("New List", style = MaterialTheme.typography.titleMedium) Text("New List", style = MaterialTheme.typography.titleMedium)
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
/*TextField( /*TextField(
value = name, value = name,
onValueChange = { name = it }, onValueChange = { name = it },
label = { Text("List Name") }, label = { Text("List Name") },
singleLine = true singleLine = true
)*/ )*/
OutlinedTextField( OutlinedTextField(
value = name, value = name,
singleLine = true, singleLine = true,
onValueChange = { name = it }, onValueChange = { name = it },
label = { Text("Title") }, label = { Text("Title") },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.focusRequester(titleFocusRequester) .focusRequester(titleFocusRequester)
) )
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
//DropdownSelector(selected = type, onSelect = { type = it }) //DropdownSelector(selected = type, onSelect = { type = it })
/*Spacer(Modifier.height(8.dp)) /*Spacer(Modifier.height(8.dp))
TextField( TextField(
value = description, value = description,
onValueChange = { description = it }, onValueChange = { description = it },
@@ -263,19 +261,18 @@ fun AddListBottomSheet(
maxLines = 3 maxLines = 3
)*/ )*/
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth()) { Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth()) {
//TextButton(onClick = onDismiss) { Text("Cancel") } //TextButton(onClick = onDismiss) { Text("Cancel") }
//Spacer(Modifier.width(8.dp)) //Spacer(Modifier.width(8.dp))
Button( Button(
onClick = { onClick = {
viewModel.createTaskList(name/*, type, description*/, viewModel.taskCount + 1) viewModel.createTaskList(name/*, type, description*/, viewModel.taskCount + 1)
onDismiss() onDismiss()
}, },
enabled = name.isNotBlank() enabled = name.isNotBlank()
) { ) {
Text("Create") Text("Create")
}
} }
} }
} }

View File

@@ -29,10 +29,12 @@ import com.wismna.geoffroy.donext.presentation.viewmodel.MenuViewModel
@Composable @Composable
fun MenuScreen( fun MenuScreen(
modifier: Modifier = Modifier,
viewModel: MenuViewModel = hiltViewModel(), viewModel: MenuViewModel = hiltViewModel(),
currentDestination: AppDestination, currentDestination: AppDestination,
) { ) {
ModalDrawerSheet( ModalDrawerSheet(
modifier = modifier,
drawerContainerColor = MaterialTheme.colorScheme.surfaceVariant, drawerContainerColor = MaterialTheme.colorScheme.surfaceVariant,
drawerContentColor = MaterialTheme.colorScheme.onSurfaceVariant drawerContentColor = MaterialTheme.colorScheme.onSurfaceVariant
) { ) {

View File

@@ -18,7 +18,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SegmentedButtonDefaults
@@ -27,13 +26,11 @@ import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberDatePickerState import androidx.compose.material3.rememberDatePickerState
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -44,7 +41,6 @@ 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
import kotlinx.coroutines.launch
import java.time.LocalDate import java.time.LocalDate
import java.time.ZoneId import java.time.ZoneId
import java.time.ZoneOffset import java.time.ZoneOffset
@@ -53,159 +49,149 @@ import java.time.format.FormatStyle
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun TaskBottomSheet( fun TaskScreen(
viewModel: TaskViewModel = hiltViewModel(), viewModel: TaskViewModel = hiltViewModel(),
onDismiss: () -> Unit onDismiss: () -> Unit
) { ) {
val titleFocusRequester = remember { FocusRequester() } val titleFocusRequester = remember { FocusRequester() }
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val scope = rememberCoroutineScope()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
titleFocusRequester.requestFocus() titleFocusRequester.requestFocus()
} }
ModalBottomSheet( Column(Modifier.padding(16.dp)) {
onDismissRequest = onDismiss, Text(
sheetState = sheetState) { viewModel.screenTitle(),
Column(Modifier.padding(16.dp)) { style = MaterialTheme.typography.titleLarge
Text( )
viewModel.screenTitle(), Spacer(Modifier.height(8.dp))
style = MaterialTheme.typography.titleLarge
// --- Title ---
OutlinedTextField(
value = viewModel.title,
singleLine = true,
readOnly = viewModel.isDeleted,
onValueChange = { viewModel.onTitleChanged(it) },
label = { Text("Title") },
modifier = Modifier
.fillMaxWidth()
.focusRequester(titleFocusRequester)
)
Spacer(Modifier.height(12.dp))
// --- Description ---
OutlinedTextField(
value = viewModel.description,
readOnly = viewModel.isDeleted,
onValueChange = { viewModel.onDescriptionChanged(it) },
label = { Text("Description") },
maxLines = 3,
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.height(12.dp))
// --- Priority ---
Row(
modifier = Modifier.fillMaxWidth().padding(start = 17.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text("Priority", style = MaterialTheme.typography.labelLarge)
SingleChoiceSegmentedButton(
value = viewModel.priority,
isEnabled = !viewModel.isDeleted,
onValueChange = { viewModel.onPriorityChanged(it) }
) )
Spacer(Modifier.height(8.dp)) }
Spacer(Modifier.height(12.dp))
// --- Title --- // --- Due Date ---
OutlinedTextField( var showDatePicker by remember { mutableStateOf(false) }
value = viewModel.title, val formattedDate = viewModel.dueDate?.toLocalDate()?.format(
singleLine = true, DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
readOnly = viewModel.isDeleted, )
onValueChange = { viewModel.onTitleChanged(it) }, ?: ""
label = { Text("Title") },
modifier = Modifier
.fillMaxWidth()
.focusRequester(titleFocusRequester)
)
Spacer(Modifier.height(12.dp))
// --- Description --- OutlinedTextField(
OutlinedTextField( value = formattedDate,
value = viewModel.description, onValueChange = {},
readOnly = viewModel.isDeleted, readOnly = true,
onValueChange = { viewModel.onDescriptionChanged(it) }, label = { Text("Due Date") },
label = { Text("Description") }, trailingIcon = {
maxLines = 3, Row {
modifier = Modifier.fillMaxWidth() if (viewModel.dueDate != null) {
)
Spacer(Modifier.height(12.dp))
// --- Priority ---
Row (
modifier = Modifier.fillMaxWidth().padding(start = 17.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically) {
Text("Priority", style = MaterialTheme.typography.labelLarge)
SingleChoiceSegmentedButton(
value = viewModel.priority,
isEnabled = !viewModel.isDeleted,
onValueChange = { viewModel.onPriorityChanged(it) }
)
}
Spacer(Modifier.height(12.dp))
// --- Due Date ---
var showDatePicker by remember { mutableStateOf(false) }
val formattedDate = viewModel.dueDate?.toLocalDate()?.format(
DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM))
?: ""
OutlinedTextField(
value = formattedDate,
onValueChange = {},
readOnly = true,
label = { Text("Due Date") },
trailingIcon = {
Row {
if (viewModel.dueDate != null) {
IconButton(
onClick = { viewModel.onDueDateChanged(null) },
enabled = !viewModel.isDeleted) {
Icon(Icons.Default.Clear, contentDescription = "Clear due date")
}
}
IconButton( IconButton(
onClick = { showDatePicker = true }, onClick = { viewModel.onDueDateChanged(null) },
enabled = !viewModel.isDeleted) { enabled = !viewModel.isDeleted
Icon(Icons.Default.CalendarMonth, contentDescription = "Pick due date") ) {
Icon(Icons.Default.Clear, contentDescription = "Clear due date")
} }
} }
}, IconButton(
modifier = Modifier.fillMaxWidth() onClick = { showDatePicker = true },
enabled = !viewModel.isDeleted
) {
Icon(Icons.Default.CalendarMonth, contentDescription = "Pick due date")
}
}
},
modifier = Modifier.fillMaxWidth()
)
if (showDatePicker) {
val datePickerState = rememberDatePickerState(
initialSelectedDateMillis = viewModel.dueDate,
selectableDates = object : SelectableDates {
override fun isSelectableDate(utcTimeMillis: Long): Boolean {
val todayStartUtcMillis = LocalDate.now(ZoneId.systemDefault())
.atStartOfDay(ZoneOffset.UTC)
.toInstant()
.toEpochMilli()
return utcTimeMillis >= todayStartUtcMillis
}
}
) )
if (showDatePicker) { DatePickerDialog(
val datePickerState = rememberDatePickerState( onDismissRequest = { showDatePicker = false },
initialSelectedDateMillis = viewModel.dueDate, confirmButton = {
selectableDates = object: SelectableDates { TextButton(
override fun isSelectableDate(utcTimeMillis: Long): Boolean { onClick = {
val todayStartUtcMillis = LocalDate.now(ZoneId.systemDefault())
.atStartOfDay(ZoneOffset.UTC)
.toInstant()
.toEpochMilli()
return utcTimeMillis >= todayStartUtcMillis
}
}
)
DatePickerDialog(
onDismissRequest = { showDatePicker = false },
confirmButton = {
TextButton(
onClick = {
datePickerState.selectedDateMillis?.let { viewModel.onDueDateChanged(it) } datePickerState.selectedDateMillis?.let { viewModel.onDueDateChanged(it) }
showDatePicker = false showDatePicker = false
}) { Text("OK") } }) { Text("OK") }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = { showDatePicker = false }) { Text("Cancel") } TextButton(onClick = { showDatePicker = false }) { Text("Cancel") }
}
) {
DatePicker(state = datePickerState)
} }
) {
DatePicker(state = datePickerState)
} }
if (!viewModel.isDeleted) { }
Spacer(Modifier.height(16.dp)) if (!viewModel.isDeleted) {
Spacer(Modifier.height(16.dp))
Row ( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) {
// --- Cancel Button ---
Button(
onClick = { onDismiss() },
colors = ButtonDefaults.textButtonColors(
contentColor = MaterialTheme.colorScheme.primary
)
) { Text("Cancel") }
// --- Save Button ---
Button(
onClick = {
viewModel.save()
onDismiss()
},
enabled = viewModel.title.isNotBlank() && !viewModel.isDeleted,
) { ) {
// --- Cancel Button --- Text(if (viewModel.isEditing()) "Save" else "Create")
Button(
onClick = {
scope.launch {
sheetState.hide()
onDismiss()
}
},
colors = ButtonDefaults.textButtonColors(
contentColor = MaterialTheme.colorScheme.primary
)
) { Text("Cancel") }
// --- Save Button ---
Button(
onClick = {
scope.launch {
viewModel.save()
sheetState.hide()
onDismiss()
}
},
enabled = viewModel.title.isNotBlank() && !viewModel.isDeleted,
) {
Text(if (viewModel.isEditing()) "Save" else "Create")
}
} }
} }
} }