mirror of
https://github.com/bbara04/Purefin.git
synced 2026-03-31 17:10:08 +02:00
feat: enhance login flow with error handling and user feedback
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user