From a31cdc79e19642e622e5dbe4b01c3a7804031632 Mon Sep 17 00:00:00 2001 From: damontecres <154766448+damontecres@users.noreply.github.com> Date: Sun, 19 Oct 2025 18:50:14 -0400 Subject: [PATCH] Better login page & Quick Connect error handling (#43) Related to #18 Ensures that when trying to auth via Quick Connect, the button to use a username/password instead is always shown. Additionally, errors that occur during the quick connect process are displayed instead of just logging in the background. This PR does not change the Quick Connect behavior or code, it's just UI improvements for when an error occurs with Quick Connect. --- .../wholphin/ui/setup/SwitchUserContent.kt | 94 +++++++++++++------ .../wholphin/ui/setup/SwitchUserViewModel.kt | 83 ++++++++-------- 2 files changed, 107 insertions(+), 70 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserContent.kt index 32db86d0..1edeb7ca 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserContent.kt @@ -8,7 +8,9 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions @@ -26,6 +28,7 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.tv.material3.Button @@ -33,6 +36,7 @@ import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import androidx.tv.material3.surfaceColorAtElevation import com.github.damontecres.wholphin.ui.components.BasicDialog +import com.github.damontecres.wholphin.ui.components.CircularProgress import com.github.damontecres.wholphin.ui.components.EditTextBox import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.nav.Destination @@ -142,26 +146,47 @@ fun SwitchUserContent( .fillMaxWidth(), ) { if (useQuickConnect) { - quickConnect?.let { qc -> + if (quickConnect == null && userState !is LoadingState.Error) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = + Modifier + .height(32.dp) + .align(Alignment.CenterHorizontally), + ) { + CircularProgress(Modifier.size(20.dp)) + Text( + text = "Waiting for Quick Connect code...", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + modifier = Modifier, + ) + } + } else if (quickConnect != null) { Text( text = "Use Quick Connect on your device to authenticate to ${server.name ?: server.url}", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurface, ) Text( - text = qc.code, + text = quickConnect?.code ?: "Failed to get code", style = MaterialTheme.typography.displayMedium, color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.align(Alignment.CenterHorizontally), ) - Button( - onClick = { - useQuickConnect = false - }, - modifier = Modifier.align(Alignment.CenterHorizontally), - ) { - Text(text = "Use username/password") - } + } + UserStateError(userState) + Button( + onClick = { + viewModel.cancelQuickConnect() + viewModel.clearSwitchUserState() + useQuickConnect = false + }, + modifier = Modifier.align(Alignment.CenterHorizontally), + ) { + Text(text = "Use username/password") } } else { var username by remember { mutableStateOf("") } @@ -176,24 +201,7 @@ fun SwitchUserContent( style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurface, ) - when (val s = userState) { - is LoadingState.Error -> { - s.message?.let { - Text( - text = it, - color = MaterialTheme.colorScheme.error, - ) - } - s.exception?.localizedMessage?.let { - Text( - text = it, - color = MaterialTheme.colorScheme.error, - ) - } - } - - else -> {} - } + UserStateError(userState) Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.focusGroup(), @@ -259,3 +267,33 @@ fun SwitchUserContent( } } } + +@Composable +private fun UserStateError( + userState: LoadingState, + modifier: Modifier = Modifier, +) { + when (val s = userState) { + is LoadingState.Error -> { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = modifier, + ) { + s.message?.let { + Text( + text = it, + color = MaterialTheme.colorScheme.error, + ) + } + s.exception?.localizedMessage?.let { + Text( + text = it, + color = MaterialTheme.colorScheme.error, + ) + } + } + } + + else -> {} + } +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserViewModel.kt index 852e4add..cade75f5 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserViewModel.kt @@ -192,53 +192,52 @@ class SwitchUserViewModel fun initiateQuickConnect(server: JellyfinServer) { quickConnectJob?.cancel() - viewModelScope.launchIO { - try { - val api = jellyfin.createApi(server.url) - var state = - api - .quickConnectApi - .initiateQuickConnect() - .content - withContext(Dispatchers.Main) { - quickConnectState.value = state - } + quickConnectJob = + viewModelScope.launchIO { + try { + val api = jellyfin.createApi(server.url) + var state = + api + .quickConnectApi + .initiateQuickConnect() + .content - quickConnectJob = - viewModelScope.launchIO { - while (!state.authenticated) { - delay(5_000L) - state = - api.quickConnectApi - .getQuickConnectState( - secret = state.secret, - ).content - withContext(Dispatchers.Main) { - quickConnectState.value = state - } - } - val authenticationResult by api.userApi.authenticateWithQuickConnect( - QuickConnectDto(secret = state.secret), - ) - serverRepository.changeUser(server.url, authenticationResult) - withContext(Dispatchers.Main) { - navigationManager.goToHome() - } - } - } catch (ex: Exception) { - Timber.e(ex, "Error during quick connect") - if (ex is InvalidStatusException && ex.status == 401) { withContext(Dispatchers.Main) { - quickConnectState.value = null - serverQuickConnect.value = - serverQuickConnect.value!!.toMutableMap().apply { - put(server.id, false) - } + quickConnectState.value = state } + + while (!state.authenticated) { + delay(5_000L) + state = + api.quickConnectApi + .getQuickConnectState( + secret = state.secret, + ).content + withContext(Dispatchers.Main) { + quickConnectState.value = state + } + } + val authenticationResult by api.userApi.authenticateWithQuickConnect( + QuickConnectDto(secret = state.secret), + ) + serverRepository.changeUser(server.url, authenticationResult) + withContext(Dispatchers.Main) { + navigationManager.goToHome() + } + } catch (ex: Exception) { + Timber.e(ex, "Error during quick connect") + if (ex is InvalidStatusException && ex.status == 401) { + withContext(Dispatchers.Main) { + quickConnectState.value = null + serverQuickConnect.value = + serverQuickConnect.value!!.toMutableMap().apply { + put(server.id, false) + } + } + } + setError("Error with Quick Connect", ex) } - setError("Error with Quick Connect", ex) } - } } fun cancelQuickConnect() {