feat: enhance login flow with error handling and user feedback

This commit is contained in:
2026-02-09 19:52:04 +01:00
parent cc9a82a4cf
commit eff2e3a0e9
6 changed files with 74 additions and 16 deletions

View File

@@ -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<Route>.() -> 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<Route>

View File

@@ -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() {

View File

@@ -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()

View File

@@ -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,
)

View File

@@ -23,6 +23,8 @@ class LoginViewModel @Inject constructor(
val password: StateFlow<String> = _password.asStateFlow()
private val _url = MutableStateFlow("")
val url: StateFlow<String> = _url.asStateFlow()
private val _errorMessage = MutableStateFlow<String?>(null)
val errorMessage: StateFlow<String?> = _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
}
}