Skip to content
Snippets Groups Projects
Commit fab470b1 authored by Jonas Broeckmann's avatar Jonas Broeckmann
Browse files

Refactored scaffold UI code (part 1)

parent 87c81450
No related branches found
No related tags found
No related merge requests found
package net.novagamestudios.kaffeekasse.ui.login
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Logout
import androidx.compose.material.icons.filled.Key
import androidx.compose.material.icons.filled.ScreenLockLandscape
import androidx.compose.material.icons.filled.SmartScreen
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import cafe.adriel.voyager.navigator.Navigator
......@@ -39,18 +15,13 @@ import kotlinx.coroutines.launch
import net.novagamestudios.common_utils.Logger
import net.novagamestudios.common_utils.compose.components.CircularLoadingBox
import net.novagamestudios.kaffeekasse.app
import net.novagamestudios.kaffeekasse.model.credentials.DeviceCredentials
import net.novagamestudios.kaffeekasse.model.credentials.isValid
import net.novagamestudios.kaffeekasse.model.session.Session
import net.novagamestudios.kaffeekasse.repositories.LoginRepository
import net.novagamestudios.kaffeekasse.repositories.RepositoryProvider
import net.novagamestudios.kaffeekasse.repositories.i11.PortalRepository
import net.novagamestudios.kaffeekasse.ui.AppInfoTopBarAction
import net.novagamestudios.kaffeekasse.ui.FullscreenIconButton
import net.novagamestudios.kaffeekasse.ui.login.UserSelectionScreenModel.Companion.backNavigationHandler
import net.novagamestudios.kaffeekasse.ui.navigation.LoginNavigation
import net.novagamestudios.kaffeekasse.ui.util.navigation.BackNavigationHandler
import net.novagamestudios.kaffeekasse.ui.util.rememberSerializableState
import net.novagamestudios.kaffeekasse.ui.util.screenmodel.ScreenModelFactory
import net.novagamestudios.kaffeekasse.ui.util.screenmodel.collectAsStateHere
......@@ -64,15 +35,7 @@ class LoginScreenModel private constructor(
val isLoading by loginRepository.isPerformingAction.collectAsStateHere()
var showLoginDeviceDialog by mutableStateOf(false)
fun loginDevice(deviceCredentials: DeviceCredentials) {
screenModelScope.launch {
loginRepository.loginDevice(deviceCredentials)
showLoginDeviceDialog = false
}
}
val loginDeviceState = LoginDeviceState(screenModelScope, loginRepository)
companion object : ScreenModelFactory<LoginNavigation.Companion, LoginScreenModel> {
context (RepositoryProvider)
......@@ -99,100 +62,16 @@ fun LoginAdditional(
if (session !is Session.WithDevice) navigator.replace(LoginNavigation.FormScreen)
}
}
if (model.showLoginDeviceDialog) LoginDeviceDialog(model)
}
@Composable
private fun LoginDeviceDialog(
model: LoginScreenModel,
modifier: Modifier = Modifier
) {
val inputState = rememberSerializableState {
mutableStateOf(DeviceCredentials.Empty)
}
val input by inputState
AlertDialog(
onDismissRequest = { model.showLoginDeviceDialog = false },
confirmButton = {
Button(
onClick = { model.loginDevice(input) },
enabled = !model.isLoading && input.isValid
) {
Text("Einloggen")
}
},
modifier,
dismissButton = {
TextButton(
onClick = { model.showLoginDeviceDialog = false },
enabled = !model.isLoading
) {
Text("Abbrechen")
}
},
text = {
CircularLoadingBox(loading = model.isLoading) {
LoginDeviceForm(
inputState = inputState,
onDone = { model.loginDevice(input) }
)
}
}
)
LoginDeviceAdditional(model.loginDeviceState)
}
@Composable
private fun LoginDeviceForm(
inputState: MutableState<DeviceCredentials>,
onDone: () -> Unit,
modifier: Modifier = Modifier
) {
var input by inputState
Column(
modifier.width(IntrinsicSize.Min),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
val focusManager = LocalFocusManager.current
OutlinedTextField(
value = input.deviceId,
onValueChange = { input = input.copy(deviceId = it.uppercase()) },
label = { Text("Geräte-Id") },
placeholder = { Text("XX:XX:XX:XX:XX:XX") },
leadingIcon = { Icon(Icons.Default.SmartScreen, "Geräte-Id") },
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Next
),
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) }),
singleLine = true
)
OutlinedTextField(
value = input.apiKey,
onValueChange = { input = input.copy(apiKey = it) },
label = { Text("API-Schlüssel") },
leadingIcon = { Icon(Icons.Default.Key, "API-Schlüssel") },
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(onDone = { onDone() }),
singleLine = true
)
}
}
@Composable
fun LoginTopBarTitle(
model: LoginScreenModel,
navigator: Navigator
) {
when (val screen = navigator.lastItem) {
is LoginNavigation.UserSelectionScreen -> {
UserSelectionTopBarTitle(screen.model, navigator)
}
is LoginNavigation.UserSelectionScreen -> UserSelectionTopBarTitle(screen.model)
}
}
......@@ -202,20 +81,9 @@ fun LoginTopBarActions(
navigator: Navigator
) {
when (val screen = navigator.lastItem) {
is LoginNavigation.FormScreen -> {
AppInfoTopBarAction()
IconButton(
onClick = { model.showLoginDeviceDialog = true },
enabled = !model.isLoading
) {
Icon(Icons.Default.ScreenLockLandscape, "Gerät einloggen")
}
}
is LoginNavigation.UserSelectionScreen -> {
UserSelectionTopBarActions(screen.model, navigator)
}
is LoginNavigation.FormScreen -> LoginFormTopBarActions(model.loginDeviceState)
is LoginNavigation.UserSelectionScreen -> UserSelectionTopBarActions(screen.model, model.loginDeviceState)
}
FullscreenIconButton()
}
@Composable
......@@ -226,15 +94,12 @@ fun LoginScreenModel.backNavigationHandler(navigator: Navigator): BackNavigation
}
}
@Composable
fun LogoutTopBarAction(session: Session) {
val app = app()
val loginRepository = app.loginRepository
fun LogoutTopBarAction(session: Session) = with(app()) {
val isLoading by loginRepository.isPerformingAction.collectAsState()
CircularLoadingBox(loading = isLoading) {
IconButton(
onClick = { app.launch { loginRepository.logoutUser() } },
onClick = { launch { loginRepository.logoutUser() } },
enabled = session !is Session.Empty
) {
Icon(
......
package net.novagamestudios.kaffeekasse.ui.login
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Key
import androidx.compose.material.icons.filled.PhonelinkErase
import androidx.compose.material.icons.filled.ScreenLockLandscape
import androidx.compose.material.icons.filled.SmartScreen
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
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.FocusDirection
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import net.novagamestudios.common_utils.compose.components.CircularLoadingBox
import net.novagamestudios.common_utils.compose.state.collectAsStateIn
import net.novagamestudios.kaffeekasse.model.credentials.DeviceCredentials
import net.novagamestudios.kaffeekasse.model.credentials.isValid
import net.novagamestudios.kaffeekasse.repositories.LoginRepository
import net.novagamestudios.kaffeekasse.ui.util.rememberSerializableState
class LoginDeviceState(
private val coroutineScope: CoroutineScope,
private val loginRepository: LoginRepository
) {
val isLoading by loginRepository.isPerformingAction.collectAsStateIn(coroutineScope)
var showDialog by mutableStateOf(false)
fun login(deviceCredentials: DeviceCredentials) {
coroutineScope.launch {
loginRepository.loginDevice(deviceCredentials)
showDialog = false
}
}
fun logout() {
coroutineScope.launch {
loginRepository.logoutDevice()
}
}
}
@Composable
fun LoginDeviceAdditional(
state: LoginDeviceState,
modifier: Modifier = Modifier
) {
if (state.showDialog) LoginDeviceDialog(
onDismiss = { state.showDialog = false },
onLogin = { state.login(it) },
isLoading = state.isLoading,
modifier
)
}
@Composable
private fun LoginDeviceDialog(
onDismiss: () -> Unit,
onLogin: (DeviceCredentials) -> Unit,
isLoading: Boolean,
modifier: Modifier = Modifier
) {
val inputState = rememberSerializableState {
mutableStateOf(DeviceCredentials.Empty)
}
val input by inputState
AlertDialog(
onDismissRequest = onDismiss,
confirmButton = {
Button(
onClick = { onLogin(input) },
enabled = !isLoading && input.isValid
) {
Text("Einloggen")
}
},
modifier,
dismissButton = {
TextButton(
onClick = onDismiss,
enabled = !isLoading
) {
Text("Abbrechen")
}
},
text = {
CircularLoadingBox(loading = isLoading) {
LoginDeviceForm(
inputState = inputState,
onDone = { onLogin(input) }
)
}
}
)
}
@Composable
private fun LoginDeviceForm(
inputState: MutableState<DeviceCredentials>,
onDone: () -> Unit,
modifier: Modifier = Modifier
) {
var input by inputState
Column(
modifier.width(IntrinsicSize.Min),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
val focusManager = LocalFocusManager.current
OutlinedTextField(
value = input.deviceId,
onValueChange = { input = input.copy(deviceId = it.uppercase()) },
label = { Text("Geräte-Id") },
placeholder = { Text("XX:XX:XX:XX:XX:XX") },
leadingIcon = { Icon(Icons.Default.SmartScreen, "Geräte-Id") },
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Next
),
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) }),
singleLine = true
)
OutlinedTextField(
value = input.apiKey,
onValueChange = { input = input.copy(apiKey = it) },
label = { Text("API-Schlüssel") },
leadingIcon = { Icon(Icons.Default.Key, "API-Schlüssel") },
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(onDone = { onDone() }),
singleLine = true
)
}
}
@Composable
fun LoginDeviceButton(
state: LoginDeviceState,
modifier: Modifier = Modifier
) = IconButton(
onClick = { state.showDialog = true },
modifier,
enabled = !state.isLoading
) {
Icon(Icons.Default.ScreenLockLandscape, "Gerät einloggen")
}
@Composable
fun LogoutDeviceButton(
state: LoginDeviceState,
modifier: Modifier = Modifier
) {
var confirmLogout by remember { mutableStateOf(false) }
IconButton(
onClick = { confirmLogout = true },
modifier,
enabled = !state.isLoading
) {
Icon(Icons.Default.PhonelinkErase, "Gerät ausloggen")
}
if (confirmLogout) ConfirmLogoutDeviceDialog(
onDismiss = { confirmLogout = false },
onConfirm = { state.logout() }
)
}
@Composable
private fun ConfirmLogoutDeviceDialog(
onDismiss: () -> Unit,
onConfirm: () -> Unit,
modifier: Modifier = Modifier
) = AlertDialog(
onDismissRequest = onDismiss,
confirmButton = {
Button(
onClick = onConfirm
) {
Text("Ausloggen")
}
},
modifier,
dismissButton = {
TextButton(
onClick = onDismiss
) {
Text("Abbrechen")
}
},
text = {
Text("Möchtest du das Gerät wirklich ausloggen?")
}
)
......@@ -63,10 +63,12 @@ import net.novagamestudios.kaffeekasse.repositories.LoginRepository
import net.novagamestudios.kaffeekasse.repositories.RepositoryProvider
import net.novagamestudios.kaffeekasse.repositories.SettingsRepository
import net.novagamestudios.kaffeekasse.repositories.i11.PortalRepository
import net.novagamestudios.kaffeekasse.ui.AppInfoTopBarAction
import net.novagamestudios.kaffeekasse.ui.FullscreenIconButton
import net.novagamestudios.kaffeekasse.ui.kaffeekasse.components.FailureRetryScreen
import net.novagamestudios.kaffeekasse.ui.navigation.LoginNavigation
import net.novagamestudios.kaffeekasse.ui.util.screenmodel.ScreenModelFactory
import net.novagamestudios.kaffeekasse.ui.util.rememberSerializableState
import net.novagamestudios.kaffeekasse.ui.util.screenmodel.ScreenModelFactory
import net.novagamestudios.kaffeekasse.ui.util.screenmodel.collectAsStateHere
......@@ -238,3 +240,13 @@ fun Modifier.autofill(
}
}
@Composable
fun LoginFormTopBarActions(
loginDeviceState: LoginDeviceState
) {
AppInfoTopBarAction()
LoginDeviceButton(loginDeviceState)
FullscreenIconButton()
}
......@@ -26,13 +26,11 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Password
import androidx.compose.material.icons.filled.PhonelinkErase
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.DividerDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.OutlinedTextField
......@@ -86,6 +84,7 @@ import net.novagamestudios.kaffeekasse.repositories.LoginRepository
import net.novagamestudios.kaffeekasse.repositories.RepositoryProvider
import net.novagamestudios.kaffeekasse.repositories.i11.KaffeekasseRepository
import net.novagamestudios.kaffeekasse.ui.AppInfoTopBarAction
import net.novagamestudios.kaffeekasse.ui.FullscreenIconButton
import net.novagamestudios.kaffeekasse.ui.handleSession
import net.novagamestudios.kaffeekasse.ui.navigation.LoginNavigation
import net.novagamestudios.kaffeekasse.ui.util.AlphabetSelectionChar
......@@ -171,12 +170,6 @@ class UserSelectionScreenModel private constructor(
loginUser(dialog.user, dialog.auth)
}
fun logoutDevice() {
screenModelScope.launch {
loginRepository.logoutDevice()
}
}
companion object : ScreenModelFactory<LoginNavigation.UserSelectionScreen, UserSelectionScreenModel> {
context (RepositoryProvider)
override fun create(screen: LoginNavigation.UserSelectionScreen) = UserSelectionScreenModel(
......@@ -538,8 +531,7 @@ private fun UserAuthDialog(
@Composable
fun UserSelectionTopBarTitle(
model: UserSelectionScreenModel,
navigator: Navigator
model: UserSelectionScreenModel
) {
Row(verticalAlignment = Alignment.CenterVertically) {
if (model.searchState.show) {
......@@ -559,21 +551,12 @@ fun UserSelectionTopBarTitle(
@Composable
fun UserSelectionTopBarActions(
model: UserSelectionScreenModel,
navigator: Navigator
loginDeviceState: LoginDeviceState
) {
TopBarSearchAction(model.searchState, alwaysShow = true)
AppInfoTopBarAction()
var confirmLogout by remember { mutableStateOf(false) }
IconButton(
onClick = { confirmLogout = true },
enabled = !model.isPerformingLoginAction
) {
Icon(Icons.Default.PhonelinkErase, "Gerät ausloggen")
}
if (confirmLogout) ConfirmLogoutDeviceDialog(
onDismiss = { confirmLogout = false },
onConfirm = { model.logoutDevice() }
)
LogoutDeviceButton(loginDeviceState)
FullscreenIconButton()
}
......@@ -598,30 +581,3 @@ private fun DeviceInfo(
}
}
@Composable
private fun ConfirmLogoutDeviceDialog(
onDismiss: () -> Unit,
onConfirm: () -> Unit,
modifier: Modifier = Modifier
) = AlertDialog(
onDismissRequest = onDismiss,
confirmButton = {
Button(
onClick = onConfirm
) {
Text("Ausloggen")
}
},
modifier,
dismissButton = {
TextButton(
onClick = onDismiss
) {
Text("Abbrechen")
}
},
text = {
Text("Möchtest du das Gerät wirklich ausloggen?")
}
)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment