Compare commits

...

2 Commits

Author SHA1 Message Date
Lonami Exo ea53d3cb1d Add modifier parameter to composables
For context, see:
https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/docs/compose-api-guidelines.md#elements-accept-and-respect-a-modifier-parameter

> Element functions MUST accept a parameter of type Modifier.
> This parameter MUST be named modifier and MUST appear as the
> first optional parameter in the element function's parameter list
2022-10-24 14:15:20 +02:00
Lonami Exo da136a1990 Add a navigation drawer 2022-10-24 13:58:47 +02:00
4 changed files with 252 additions and 87 deletions

View File

@ -1,13 +1,19 @@
package dev.lonami.talaria package dev.lonami.talaria
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
@ -16,6 +22,7 @@ import dev.lonami.talaria.bindings.Native
import dev.lonami.talaria.ui.screens.ChatScreen import dev.lonami.talaria.ui.screens.ChatScreen
import dev.lonami.talaria.ui.screens.DialogScreen import dev.lonami.talaria.ui.screens.DialogScreen
import dev.lonami.talaria.ui.screens.LoginScreen import dev.lonami.talaria.ui.screens.LoginScreen
import kotlinx.coroutines.launch
enum class TalariaScreen(@StringRes val title: Int) { enum class TalariaScreen(@StringRes val title: Int) {
Login(title = R.string.app_name), Login(title = R.string.app_name),
@ -23,8 +30,94 @@ enum class TalariaScreen(@StringRes val title: Int) {
Chat(title = R.string.chat), Chat(title = R.string.chat),
} }
enum class DrawerAction {
SelectAccount,
AddAccount,
SavedMessages,
Contacts,
NewChat,
Settings,
Help,
}
@Composable @Composable
fun TalariaAppBar(currentScreen: TalariaScreen, canNavigateBack: Boolean, navigateUp: () -> Unit) { fun DrawerAction(
icon: ImageVector,
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.clickable(onClick = onClick)
.fillMaxWidth()
.padding(0.dp, 8.dp)
) {
Icon(imageVector = icon, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text(text)
}
}
@Composable
fun Drawer(onSelect: (DrawerAction) -> Unit, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize()
.padding(24.dp, 48.dp)
) {
Image(
painter = painterResource(R.drawable.ic_launcher_foreground),
contentDescription = "App Icon"
)
DrawerAction(
icon = Icons.Filled.AccountBox,
text = "Main Account",
onClick = { onSelect(DrawerAction.SelectAccount) }
)
DrawerAction(
icon = Icons.Filled.Add,
text = "Add Account",
onClick = { onSelect(DrawerAction.AddAccount) }
)
Divider()
DrawerAction(
icon = Icons.Filled.Star,
text = "Saved Messages",
onClick = { onSelect(DrawerAction.SavedMessages) }
)
DrawerAction(
icon = Icons.Filled.Call,
text = "Contacts",
onClick = { onSelect(DrawerAction.Contacts) }
)
DrawerAction(
icon = Icons.Filled.Create,
text = "New Chat",
onClick = { onSelect(DrawerAction.NewChat) }
)
Divider()
DrawerAction(
icon = Icons.Filled.Settings,
text = "Settings",
onClick = { onSelect(DrawerAction.Settings) }
)
DrawerAction(
icon = Icons.Filled.Info,
text = "Help",
onClick = { onSelect(DrawerAction.Help) }
)
}
}
@Composable
fun TalariaAppBar(
currentScreen: TalariaScreen,
canNavigateBack: Boolean,
navigateUp: () -> Unit,
openDrawer: () -> Unit,
modifier: Modifier = Modifier,
) {
TopAppBar( TopAppBar(
title = { Text(stringResource(currentScreen.title)) }, title = { Text(stringResource(currentScreen.title)) },
navigationIcon = { navigationIcon = {
@ -35,13 +128,20 @@ fun TalariaAppBar(currentScreen: TalariaScreen, canNavigateBack: Boolean, naviga
contentDescription = stringResource(R.string.back_button) contentDescription = stringResource(R.string.back_button)
) )
} }
} else {
IconButton(onClick = openDrawer) {
Icon(
imageVector = Icons.Filled.Menu,
contentDescription = stringResource(androidx.compose.ui.R.string.navigation_menu)
)
}
} }
} }
) )
} }
@Composable @Composable
fun TalariaApp() { fun TalariaApp(modifier: Modifier = Modifier) {
val navController = rememberNavController() val navController = rememberNavController()
val backStackEntry by navController.currentBackStackEntryAsState() val backStackEntry by navController.currentBackStackEntryAsState()
val currentScreen = val currentScreen =
@ -50,38 +150,79 @@ fun TalariaApp() {
val loggedIn by remember { mutableStateOf(!Native.needLogin()) } val loggedIn by remember { mutableStateOf(!Native.needLogin()) }
var selectedDialog by remember { mutableStateOf("") } var selectedDialog by remember { mutableStateOf("") }
val drawerState = rememberDrawerState(DrawerValue.Closed)
val scope = rememberCoroutineScope()
val toggleDrawer = { open: Boolean ->
scope.launch {
if (open) {
drawerState.open()
} else {
drawerState.close()
}
}
}
Scaffold( Scaffold(
modifier = modifier,
topBar = { topBar = {
TalariaAppBar( TalariaAppBar(
currentScreen, currentScreen,
canNavigateBack = navController.previousBackStackEntry != null, canNavigateBack = navController.previousBackStackEntry != null,
navigateUp = { navController.navigateUp() } navigateUp = { navController.navigateUp() },
openDrawer = { toggleDrawer(drawerState.isClosed) }
) )
} }
) { innerPadding -> ) { innerPadding ->
NavHost( ModalDrawer(
navController = navController, drawerState = drawerState,
startDestination = if (loggedIn) { gesturesEnabled = drawerState.isOpen,
TalariaScreen.Login.name drawerContent = {
} else { Drawer(onSelect = {})
TalariaScreen.Dialog.name }) {
},
Modifier.padding(innerPadding) NavHost(
) { navController = navController,
composable(route = TalariaScreen.Dialog.name) { startDestination = if (loggedIn) {
DialogScreen(onDialogSelected = { TalariaScreen.Login.name
selectedDialog = it } else {
navController.navigate(TalariaScreen.Chat.name) TalariaScreen.Dialog.name
}) },
} Modifier.padding(innerPadding)
composable(route = TalariaScreen.Chat.name) { ) {
ChatScreen(selectedDialog) composable(route = TalariaScreen.Dialog.name) {
} DialogScreen(onDialogSelected = {
composable(route = TalariaScreen.Login.name) { selectedDialog = it
LoginScreen(onConfirmOtp = { navController.navigate(TalariaScreen.Chat.name)
navController.navigate(TalariaScreen.Dialog.name) })
}) }
composable(route = TalariaScreen.Chat.name) {
ChatScreen(selectedDialog)
}
composable(route = TalariaScreen.Login.name) {
LoginScreen(onConfirmOtp = {
navController.navigate(TalariaScreen.Dialog.name)
})
}
} }
} }
} }
} }
@Preview
@Composable
fun TopBarPreview() {
TalariaAppBar(
currentScreen = TalariaScreen.Dialog,
canNavigateBack = false,
navigateUp = {},
openDrawer = {},
)
}
@Preview
@Composable
fun NavDrawerPreview() {
Surface {
Drawer(onSelect = {})
}
}

View File

@ -25,10 +25,10 @@ import dev.lonami.talaria.ui.theme.TalariaTheme
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @Composable
fun MessageCard(message: Message) { fun MessageCard(message: Message, modifier: Modifier = Modifier) {
Card( Card(
elevation = 4.dp, elevation = 4.dp,
modifier = Modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.padding(8.dp) .padding(8.dp)
) { ) {
@ -44,8 +44,8 @@ fun MessageCard(message: Message) {
} }
@Composable @Composable
fun MessageList(messages: List<Message>, modifier: Modifier, listState: LazyListState) { fun MessageList(messages: List<Message>, listState: LazyListState, modifier: Modifier = Modifier) {
LazyColumn(modifier, state = listState) { LazyColumn(modifier = modifier, state = listState) {
items(messages.size) { MessageCard(messages[it]) } items(messages.size) { MessageCard(messages[it]) }
} }
} }
@ -54,9 +54,10 @@ fun MessageList(messages: List<Message>, modifier: Modifier, listState: LazyList
fun MessageInputField( fun MessageInputField(
messageText: String, messageText: String,
onMessageChanged: (String) -> Unit, onMessageChanged: (String) -> Unit,
onSendMessage: () -> Unit onSendMessage: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
Row { Row(modifier = modifier) {
TextField( TextField(
messageText, messageText,
placeholder = { Text(stringResource(R.string.write_message)) }, placeholder = { Text(stringResource(R.string.write_message)) },
@ -78,13 +79,17 @@ fun MessageInputField(
} }
@Composable @Composable
fun ChatScreen(selectedDialog: String, chatViewModel: ChatViewModel = viewModel()) { fun ChatScreen(
selectedDialog: String,
modifier: Modifier = Modifier,
chatViewModel: ChatViewModel = viewModel(),
) {
val chatUiState by chatViewModel.uiState.collectAsState() val chatUiState by chatViewModel.uiState.collectAsState()
var messageText by remember { mutableStateOf("") } var messageText by remember { mutableStateOf("") }
val messageListState = rememberLazyListState() val messageListState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
Column(modifier = Modifier.fillMaxSize()) { Column(modifier = modifier.fillMaxSize()) {
MessageList( MessageList(
chatUiState.messages, chatUiState.messages,
modifier = Modifier.weight(1.0f), modifier = Modifier.weight(1.0f),

View File

@ -31,9 +31,9 @@ import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle import java.time.format.FormatStyle
@Composable @Composable
fun Dialog(dialog: Dialog, onDialogSelected: () -> Unit) { fun Dialog(dialog: Dialog, onDialogSelected: () -> Unit, modifier: Modifier = Modifier) {
Row( Row(
modifier = Modifier modifier = modifier
.padding(4.dp) .padding(4.dp)
.clickable(onClick = onDialogSelected) .clickable(onClick = onDialogSelected)
) { ) {
@ -98,8 +98,12 @@ fun Dialog(dialog: Dialog, onDialogSelected: () -> Unit) {
} }
@Composable @Composable
fun DialogList(dialogs: List<Dialog>, onDialogSelected: (String) -> Unit) { fun DialogList(
LazyColumn { dialogs: List<Dialog>,
onDialogSelected: (String) -> Unit,
modifier: Modifier = Modifier,
) {
LazyColumn(modifier = modifier) {
items(dialogs.size) { items(dialogs.size) {
Dialog(dialogs[it], onDialogSelected = { Dialog(dialogs[it], onDialogSelected = {
onDialogSelected(dialogs[it].id) onDialogSelected(dialogs[it].id)
@ -112,10 +116,11 @@ fun DialogList(dialogs: List<Dialog>, onDialogSelected: (String) -> Unit) {
@Composable @Composable
fun DialogScreen( fun DialogScreen(
onDialogSelected: (String) -> Unit, onDialogSelected: (String) -> Unit,
dialogViewModel: DialogViewModel = viewModel() modifier: Modifier = Modifier,
dialogViewModel: DialogViewModel = viewModel(),
) { ) {
val dialogUiState by dialogViewModel.uiState.collectAsState() val dialogUiState by dialogViewModel.uiState.collectAsState()
Surface { Surface(modifier = modifier) {
DialogList(dialogUiState.dialogs, onDialogSelected = onDialogSelected) DialogList(dialogUiState.dialogs, onDialogSelected = onDialogSelected)
} }
} }

View File

@ -30,63 +30,77 @@ fun isPhoneValid(phone: String): Boolean = phone.trim('+', ' ').isNotEmpty()
fun isLoginCodeValid(code: String): Boolean = code.trim().count { it.isDigit() } == 5 fun isLoginCodeValid(code: String): Boolean = code.trim().count { it.isDigit() } == 5
@Composable @Composable
fun PhoneInput(phone: String, onPhoneChanged: (String) -> Unit, onSendCode: () -> Unit) { fun PhoneInput(
phone: String,
onPhoneChanged: (String) -> Unit,
onSendCode: () -> Unit,
modifier: Modifier = Modifier,
) {
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
Text(stringResource(R.string.enter_phone)) Column(modifier = modifier) {
TextField( Text(stringResource(R.string.enter_phone))
phone, TextField(
label = { Text(stringResource(R.string.phone_international)) }, phone,
placeholder = { Text(stringResource(R.string.phone_example)) }, label = { Text(stringResource(R.string.phone_international)) },
singleLine = true, placeholder = { Text(stringResource(R.string.phone_example)) },
keyboardOptions = KeyboardOptions( singleLine = true,
keyboardType = KeyboardType.Phone, keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done keyboardType = KeyboardType.Phone,
), imeAction = ImeAction.Done
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), ),
modifier = Modifier.fillMaxWidth(), keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChange = onPhoneChanged modifier = Modifier.fillMaxWidth(),
) onValueChange = onPhoneChanged
Spacer(Modifier.height(16.dp)) )
Button( Spacer(Modifier.height(16.dp))
enabled = isPhoneValid(phone), Button(
modifier = Modifier.fillMaxWidth(), enabled = isPhoneValid(phone),
onClick = onSendCode modifier = Modifier.fillMaxWidth(),
) { onClick = onSendCode
Text(stringResource(R.string.send_otp)) ) {
Text(stringResource(R.string.send_otp))
}
} }
} }
@Composable @Composable
fun OtpInput(otp: String, onOtpChanged: (String) -> Unit, onConfirmOtp: () -> Unit) { fun OtpInput(
otp: String,
onOtpChanged: (String) -> Unit,
onConfirmOtp: () -> Unit,
modifier: Modifier = Modifier,
) {
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
Text(stringResource(R.string.enter_otp)) Column(modifier = modifier) {
TextField( Text(stringResource(R.string.enter_otp))
otp, TextField(
label = { Text(stringResource(R.string.otp)) }, otp,
placeholder = { Text(stringResource(R.string.otp_example)) }, label = { Text(stringResource(R.string.otp)) },
singleLine = true, placeholder = { Text(stringResource(R.string.otp_example)) },
keyboardOptions = KeyboardOptions( singleLine = true,
keyboardType = KeyboardType.Number, keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done keyboardType = KeyboardType.Number,
), imeAction = ImeAction.Done
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), ),
modifier = Modifier.fillMaxWidth(), keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChange = onOtpChanged modifier = Modifier.fillMaxWidth(),
) onValueChange = onOtpChanged
Spacer(Modifier.height(16.dp)) )
Button( Spacer(Modifier.height(16.dp))
enabled = isLoginCodeValid(otp), Button(
modifier = Modifier.fillMaxWidth(), enabled = isLoginCodeValid(otp),
onClick = onConfirmOtp modifier = Modifier.fillMaxWidth(),
) { onClick = onConfirmOtp
Text(stringResource(R.string.do_login)) ) {
Text(stringResource(R.string.do_login))
}
} }
} }
@Composable @Composable
fun LoginScreen(onConfirmOtp: () -> Unit) { fun LoginScreen(onConfirmOtp: () -> Unit, modifier: Modifier = Modifier) {
var stage by remember { mutableStateOf(LoginStage.ASK_PHONE) } var stage by remember { mutableStateOf(LoginStage.ASK_PHONE) }
var phone by remember { mutableStateOf("") } var phone by remember { mutableStateOf("") }
var otp by remember { mutableStateOf("") } var otp by remember { mutableStateOf("") }
@ -94,7 +108,7 @@ fun LoginScreen(onConfirmOtp: () -> Unit) {
var tokenPtr by remember { mutableStateOf(0L) } var tokenPtr by remember { mutableStateOf(0L) }
Column( Column(
modifier = Modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
.padding(16.dp), .padding(16.dp),
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center