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.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue 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.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
@@ -30,6 +33,7 @@ import coil3.util.DebugLogger
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import hu.bbara.purefin.client.JellyfinApiClient import hu.bbara.purefin.client.JellyfinApiClient
import hu.bbara.purefin.client.JellyfinAuthInterceptor import hu.bbara.purefin.client.JellyfinAuthInterceptor
import hu.bbara.purefin.common.ui.PurefinWaitingScreen
import hu.bbara.purefin.login.ui.LoginScreen import hu.bbara.purefin.login.ui.LoginScreen
import hu.bbara.purefin.navigation.LocalNavigationManager import hu.bbara.purefin.navigation.LocalNavigationManager
import hu.bbara.purefin.navigation.NavigationCommand import hu.bbara.purefin.navigation.NavigationCommand
@@ -126,8 +130,20 @@ class PurefinActivity : ComponentActivity() {
entryBuilders: Set<@JvmSuppressWildcards EntryProviderScope<Route>.() -> Unit>, entryBuilders: Set<@JvmSuppressWildcards EntryProviderScope<Route>.() -> Unit>,
navigationManager: NavigationManager navigationManager: NavigationManager
) { ) {
var sessionLoaded by remember { mutableStateOf(false) }
val isLoggedIn by userSessionRepository.isLoggedIn.collectAsState(initial = 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) { if (isLoggedIn) {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
val backStack = rememberNavBackStack(Route.Home) as NavBackStack<Route> val backStack = rememberNavBackStack(Route.Home) as NavBackStack<Route>

View File

@@ -71,16 +71,21 @@ class JellyfinApiClient @Inject constructor(
return false return false
} }
api.update(baseUrl = trimmedUrl) api.update(baseUrl = trimmedUrl)
val response = api.userApi.authenticateUserByName(username = username, password = password) return try {
val authResult = response.content val response = api.userApi.authenticateUserByName(username = username, password = password)
val authResult = response.content
val token = authResult.accessToken ?: return false val token = authResult.accessToken ?: return false
val userId = authResult.user?.id ?: return false val userId = authResult.user?.id ?: return false
userSessionRepository.setAccessToken(accessToken = token) userSessionRepository.setAccessToken(accessToken = token)
userSessionRepository.setUserId(userId) userSessionRepository.setUserId(userId)
userSessionRepository.setLoggedIn(true) userSessionRepository.setLoggedIn(true)
api.update(accessToken = token) api.update(accessToken = token)
return true true
} catch (e: Exception) {
Log.e("JellyfinApiClient", "Login failed", e)
false
}
} }
suspend fun updateApiClient() { suspend fun updateApiClient() {

View File

@@ -16,7 +16,7 @@ object MediaDatabaseModule {
@Provides @Provides
@Singleton @Singleton
fun provideDatabase(@ApplicationContext context: Context): MediaDatabase = fun provideDatabase(@ApplicationContext context: Context): MediaDatabase =
Room.inMemoryDatabaseBuilder(context, MediaDatabase::class.java) Room.databaseBuilder(context, MediaDatabase::class.java, "media_database")
.fallbackToDestructiveMigration() .fallbackToDestructiveMigration()
.build() .build()

View File

@@ -55,6 +55,7 @@ fun LoginScreen(
val serverUrl by viewModel.url.collectAsState() val serverUrl by viewModel.url.collectAsState()
val username by viewModel.username.collectAsState() val username by viewModel.username.collectAsState()
val password by viewModel.password.collectAsState() val password by viewModel.password.collectAsState()
val errorMessage by viewModel.errorMessage.collectAsState()
var isLoggingIn by remember { mutableStateOf(false) } var isLoggingIn by remember { mutableStateOf(false) }
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
@@ -120,10 +121,29 @@ fun LoginScreen(
.padding(bottom = 24.dp) .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( PurefinComplexTextField(
label = "Server URL", label = "Server URL",
value = serverUrl, value = serverUrl,
onValueChange = { viewModel.setUrl(it) }, onValueChange = {
viewModel.clearError()
viewModel.setUrl(it)
},
placeholder = "http://192.168.1.100:8096", placeholder = "http://192.168.1.100:8096",
leadingIcon = Icons.Default.Storage leadingIcon = Icons.Default.Storage
) )
@@ -133,7 +153,10 @@ fun LoginScreen(
PurefinComplexTextField( PurefinComplexTextField(
label = "Username", label = "Username",
value = username, value = username,
onValueChange = { viewModel.setUsername(it) }, onValueChange = {
viewModel.clearError()
viewModel.setUsername(it)
},
placeholder = "Enter your username", placeholder = "Enter your username",
leadingIcon = Icons.Default.Person leadingIcon = Icons.Default.Person
) )
@@ -143,7 +166,10 @@ fun LoginScreen(
PurefinPasswordField( PurefinPasswordField(
label = "Password", label = "Password",
value = password, value = password,
onValueChange = { viewModel.setPassword(it) }, onValueChange = {
viewModel.clearError()
viewModel.setPassword(it)
},
placeholder = "••••••••", placeholder = "••••••••",
leadingIcon = Icons.Default.Lock, leadingIcon = Icons.Default.Lock,
) )

View File

@@ -23,6 +23,8 @@ class LoginViewModel @Inject constructor(
val password: StateFlow<String> = _password.asStateFlow() val password: StateFlow<String> = _password.asStateFlow()
private val _url = MutableStateFlow("") private val _url = MutableStateFlow("")
val url: StateFlow<String> = _url.asStateFlow() val url: StateFlow<String> = _url.asStateFlow()
private val _errorMessage = MutableStateFlow<String?>(null)
val errorMessage: StateFlow<String?> = _errorMessage.asStateFlow()
init { init {
viewModelScope.launch { viewModelScope.launch {
@@ -42,6 +44,10 @@ class LoginViewModel @Inject constructor(
_password.value = password _password.value = password
} }
fun clearError() {
_errorMessage.value = null
}
suspend fun clearFields() { suspend fun clearFields() {
userSessionRepository.setServerUrl(""); userSessionRepository.setServerUrl("");
_username.value = "" _username.value = ""
@@ -49,8 +55,13 @@ class LoginViewModel @Inject constructor(
} }
suspend fun login(): Boolean { suspend fun login(): Boolean {
_errorMessage.value = null
userSessionRepository.setServerUrl(url.value) 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
} }
} }

View File

@@ -7,7 +7,7 @@ espressoCore = "3.6.1"
lifecycleRuntimeKtx = "2.8.7" lifecycleRuntimeKtx = "2.8.7"
activityCompose = "1.9.3" activityCompose = "1.9.3"
kotlin = "2.1.0" kotlin = "2.1.0"
composeBom = "2024.12.01" composeBom = "2025.02.00"
jellyfin-core = "1.8.5" jellyfin-core = "1.8.5"
hilt = "2.54" hilt = "2.54"
hiltNavigationCompose = "1.2.0" hiltNavigationCompose = "1.2.0"