From eff2e3a0e935f78837d54618fe162bd373253afa Mon Sep 17 00:00:00 2001 From: Barnabas Balogh Date: Mon, 9 Feb 2026 19:52:04 +0100 Subject: [PATCH] feat: enhance login flow with error handling and user feedback --- .../java/hu/bbara/purefin/PurefinActivity.kt | 18 ++++++++++- .../bbara/purefin/client/JellyfinApiClient.kt | 23 +++++++------ .../data/local/room/MediaDatabaseModule.kt | 2 +- .../hu/bbara/purefin/login/ui/LoginScreen.kt | 32 +++++++++++++++++-- .../purefin/login/viewmodel/LoginViewModel.kt | 13 +++++++- gradle/libs.versions.toml | 2 +- 6 files changed, 74 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/hu/bbara/purefin/PurefinActivity.kt b/app/src/main/java/hu/bbara/purefin/PurefinActivity.kt index d43b6bc..ea50629 100644 --- a/app/src/main/java/hu/bbara/purefin/PurefinActivity.kt +++ b/app/src/main/java/hu/bbara/purefin/PurefinActivity.kt @@ -11,6 +11,9 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.lifecycle.lifecycleScope import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator @@ -30,6 +33,7 @@ import coil3.util.DebugLogger import dagger.hilt.android.AndroidEntryPoint import hu.bbara.purefin.client.JellyfinApiClient import hu.bbara.purefin.client.JellyfinAuthInterceptor +import hu.bbara.purefin.common.ui.PurefinWaitingScreen import hu.bbara.purefin.login.ui.LoginScreen import hu.bbara.purefin.navigation.LocalNavigationManager import hu.bbara.purefin.navigation.NavigationCommand @@ -126,8 +130,20 @@ class PurefinActivity : ComponentActivity() { entryBuilders: Set<@JvmSuppressWildcards EntryProviderScope.() -> Unit>, navigationManager: NavigationManager ) { - + var sessionLoaded by remember { mutableStateOf(false) } val isLoggedIn by userSessionRepository.isLoggedIn.collectAsState(initial = false) + + LaunchedEffect(Unit) { + userSessionRepository.isLoggedIn.collect { + sessionLoaded = true + } + } + + if (!sessionLoaded) { + PurefinWaitingScreen(modifier = Modifier.fillMaxSize()) + return + } + if (isLoggedIn) { @Suppress("UNCHECKED_CAST") val backStack = rememberNavBackStack(Route.Home) as NavBackStack diff --git a/app/src/main/java/hu/bbara/purefin/client/JellyfinApiClient.kt b/app/src/main/java/hu/bbara/purefin/client/JellyfinApiClient.kt index b13ca78..f7f55ae 100644 --- a/app/src/main/java/hu/bbara/purefin/client/JellyfinApiClient.kt +++ b/app/src/main/java/hu/bbara/purefin/client/JellyfinApiClient.kt @@ -71,16 +71,21 @@ class JellyfinApiClient @Inject constructor( return false } api.update(baseUrl = trimmedUrl) - val response = api.userApi.authenticateUserByName(username = username, password = password) - val authResult = response.content + return try { + val response = api.userApi.authenticateUserByName(username = username, password = password) + val authResult = response.content - val token = authResult.accessToken ?: return false - val userId = authResult.user?.id ?: return false - userSessionRepository.setAccessToken(accessToken = token) - userSessionRepository.setUserId(userId) - userSessionRepository.setLoggedIn(true) - api.update(accessToken = token) - return true + val token = authResult.accessToken ?: return false + val userId = authResult.user?.id ?: return false + userSessionRepository.setAccessToken(accessToken = token) + userSessionRepository.setUserId(userId) + userSessionRepository.setLoggedIn(true) + api.update(accessToken = token) + true + } catch (e: Exception) { + Log.e("JellyfinApiClient", "Login failed", e) + false + } } suspend fun updateApiClient() { diff --git a/app/src/main/java/hu/bbara/purefin/data/local/room/MediaDatabaseModule.kt b/app/src/main/java/hu/bbara/purefin/data/local/room/MediaDatabaseModule.kt index a4b1766..0d6824d 100644 --- a/app/src/main/java/hu/bbara/purefin/data/local/room/MediaDatabaseModule.kt +++ b/app/src/main/java/hu/bbara/purefin/data/local/room/MediaDatabaseModule.kt @@ -16,7 +16,7 @@ object MediaDatabaseModule { @Provides @Singleton fun provideDatabase(@ApplicationContext context: Context): MediaDatabase = - Room.inMemoryDatabaseBuilder(context, MediaDatabase::class.java) + Room.databaseBuilder(context, MediaDatabase::class.java, "media_database") .fallbackToDestructiveMigration() .build() diff --git a/app/src/main/java/hu/bbara/purefin/login/ui/LoginScreen.kt b/app/src/main/java/hu/bbara/purefin/login/ui/LoginScreen.kt index d4faf17..52af391 100644 --- a/app/src/main/java/hu/bbara/purefin/login/ui/LoginScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/login/ui/LoginScreen.kt @@ -55,6 +55,7 @@ fun LoginScreen( val serverUrl by viewModel.url.collectAsState() val username by viewModel.username.collectAsState() val password by viewModel.password.collectAsState() + val errorMessage by viewModel.errorMessage.collectAsState() var isLoggingIn by remember { mutableStateOf(false) } val coroutineScope = rememberCoroutineScope() @@ -120,10 +121,29 @@ fun LoginScreen( .padding(bottom = 24.dp) ) + if (errorMessage != null) { + Text( + text = errorMessage!!, + color = scheme.error, + fontSize = 14.sp, + modifier = Modifier + .fillMaxWidth() + .background( + scheme.errorContainer, + RoundedCornerShape(8.dp) + ) + .padding(12.dp) + ) + Spacer(modifier = Modifier.height(16.dp)) + } + PurefinComplexTextField( label = "Server URL", value = serverUrl, - onValueChange = { viewModel.setUrl(it) }, + onValueChange = { + viewModel.clearError() + viewModel.setUrl(it) + }, placeholder = "http://192.168.1.100:8096", leadingIcon = Icons.Default.Storage ) @@ -133,7 +153,10 @@ fun LoginScreen( PurefinComplexTextField( label = "Username", value = username, - onValueChange = { viewModel.setUsername(it) }, + onValueChange = { + viewModel.clearError() + viewModel.setUsername(it) + }, placeholder = "Enter your username", leadingIcon = Icons.Default.Person ) @@ -143,7 +166,10 @@ fun LoginScreen( PurefinPasswordField( label = "Password", value = password, - onValueChange = { viewModel.setPassword(it) }, + onValueChange = { + viewModel.clearError() + viewModel.setPassword(it) + }, placeholder = "••••••••", leadingIcon = Icons.Default.Lock, ) diff --git a/app/src/main/java/hu/bbara/purefin/login/viewmodel/LoginViewModel.kt b/app/src/main/java/hu/bbara/purefin/login/viewmodel/LoginViewModel.kt index dc91cee..07c8eb1 100644 --- a/app/src/main/java/hu/bbara/purefin/login/viewmodel/LoginViewModel.kt +++ b/app/src/main/java/hu/bbara/purefin/login/viewmodel/LoginViewModel.kt @@ -23,6 +23,8 @@ class LoginViewModel @Inject constructor( val password: StateFlow = _password.asStateFlow() private val _url = MutableStateFlow("") val url: StateFlow = _url.asStateFlow() + private val _errorMessage = MutableStateFlow(null) + val errorMessage: StateFlow = _errorMessage.asStateFlow() init { viewModelScope.launch { @@ -42,6 +44,10 @@ class LoginViewModel @Inject constructor( _password.value = password } + fun clearError() { + _errorMessage.value = null + } + suspend fun clearFields() { userSessionRepository.setServerUrl(""); _username.value = "" @@ -49,8 +55,13 @@ class LoginViewModel @Inject constructor( } suspend fun login(): Boolean { + _errorMessage.value = null userSessionRepository.setServerUrl(url.value) - return jellyfinApiClient.login(url.value, username.value, password.value) + val success = jellyfinApiClient.login(url.value, username.value, password.value) + if (!success) { + _errorMessage.value = "Login failed. Check your server URL, username, and password." + } + return success } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2b9f8f2..6c59cb6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,7 +7,7 @@ espressoCore = "3.6.1" lifecycleRuntimeKtx = "2.8.7" activityCompose = "1.9.3" kotlin = "2.1.0" -composeBom = "2024.12.01" +composeBom = "2025.02.00" jellyfin-core = "1.8.5" hilt = "2.54" hiltNavigationCompose = "1.2.0"