diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/Login.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/Login.kt index c257b54d3fb93068e5242d924d3525e3e3cd8184..3413a22fb68f59eb79883dd148f81efed09e58be 100644 --- a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/Login.kt +++ b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/Login.kt @@ -1,37 +1,13 @@ 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( diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/LoginDevice.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/LoginDevice.kt new file mode 100644 index 0000000000000000000000000000000000000000..51dc7ec6517719a6dda5e48d64fdb35f17d0c21d --- /dev/null +++ b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/LoginDevice.kt @@ -0,0 +1,221 @@ +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?") + } +) + + + diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/LoginForm.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/LoginForm.kt index 8b91c3152d0736c82ab230ebb025be3fde560197..10e7d5dc297650bc3d71d33e3c227b0243596fd1 100644 --- a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/LoginForm.kt +++ b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/LoginForm.kt @@ -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() +} + diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/UserSelection.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/UserSelection.kt index ad48a2a9a067dbadfe24e8e9b8f6de374a3243d3..70ace73990cb4d27c8a7ccd23e13a4c6345e98b4 100644 --- a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/UserSelection.kt +++ b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/UserSelection.kt @@ -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?") - } -) -