diff --git a/android/app/src/main/kotlin/com/flagship/abox/manager/ui/login/LoginScreen.kt b/android/app/src/main/kotlin/com/flagship/abox/manager/ui/login/LoginScreen.kt index daae812..704bdbd 100644 --- a/android/app/src/main/kotlin/com/flagship/abox/manager/ui/login/LoginScreen.kt +++ b/android/app/src/main/kotlin/com/flagship/abox/manager/ui/login/LoginScreen.kt @@ -55,11 +55,13 @@ fun LoginRoute( onDispose(viewModel::clearSensitiveState) } val state = viewModel.uiState.collectAsStateWithLifecycle().value + val errorText = state.errorMessage?.let { stringResource(it.stringResource()) } + ?: actionError + ?: bootstrapError LoginScreen( profile = profile, - state = state.copy( - errorMessage = state.errorMessage ?: actionError ?: bootstrapError, - ), + state = state, + errorText = errorText, onUsernameChange = viewModel::setUsername, onPasswordChange = viewModel::setPassword, onRememberUsernameChange = viewModel::setRememberUsername, @@ -75,6 +77,7 @@ fun LoginRoute( fun LoginScreen( profile: ProviderProfile, state: LoginUiState, + errorText: String? = null, onUsernameChange: (String) -> Unit, onPasswordChange: (String) -> Unit, onRememberUsernameChange: (Boolean) -> Unit, @@ -174,7 +177,7 @@ fun LoginScreen( enabled = formEnabled, onCheckedChange = onAutoLoginChange, ) - state.errorMessage?.let { Text(it, color = MaterialTheme.colorScheme.error) } + errorText?.let { Text(it, color = MaterialTheme.colorScheme.error) } ABoxButton( onClick = onSubmit, enabled = formEnabled && !switchingProvider, @@ -210,6 +213,15 @@ private fun ToggleRow( } } +private fun LoginErrorMessage.stringResource(): Int = when (this) { + LoginErrorMessage.REQUIRED_FIELDS -> R.string.login_error_required + LoginErrorMessage.INVALID_CREDENTIALS -> R.string.login_error_invalid_credentials + LoginErrorMessage.NETWORK_UNAVAILABLE -> R.string.login_error_network + LoginErrorMessage.SESSION_EXPIRED -> R.string.login_error_session_expired + LoginErrorMessage.PREFERENCES_UNAVAILABLE -> R.string.login_error_preferences + LoginErrorMessage.LOGIN_FAILED -> R.string.login_error_failed +} + @Composable private fun providerName(type: ProviderType): String = stringResource( when (type) { diff --git a/android/app/src/main/kotlin/com/flagship/abox/manager/ui/login/LoginViewModel.kt b/android/app/src/main/kotlin/com/flagship/abox/manager/ui/login/LoginViewModel.kt index a96a46b..5d96021 100644 --- a/android/app/src/main/kotlin/com/flagship/abox/manager/ui/login/LoginViewModel.kt +++ b/android/app/src/main/kotlin/com/flagship/abox/manager/ui/login/LoginViewModel.kt @@ -71,7 +71,7 @@ class LoginViewModel @Inject constructor( if (profile?.id == value.id) { _uiState.value = _uiState.value.copy( isLoadingPreferences = false, - errorMessage = "Unable to load saved login settings", + errorMessage = LoginErrorMessage.PREFERENCES_UNAVAILABLE, ) } } finally { @@ -126,7 +126,7 @@ class LoginViewModel @Inject constructor( state.isSubmitting || state.isLoadingPreferences ) { - _uiState.value = state.copy(errorMessage = "Username and password are required") + _uiState.value = state.copy(errorMessage = LoginErrorMessage.REQUIRED_FIELDS) return } submitJob = viewModelScope.launch { @@ -149,14 +149,23 @@ class LoginViewModel @Inject constructor( } } - private fun DomainError.toUiMessage(): String = when (this) { - is DomainError.Authentication -> "Invalid username or password" - is DomainError.Network -> "Network unavailable" - is DomainError.TokenInvalid -> "Session expired" - else -> detail ?: "Login failed" + private fun DomainError.toUiMessage(): LoginErrorMessage = when (this) { + is DomainError.Authentication -> LoginErrorMessage.INVALID_CREDENTIALS + is DomainError.Network -> LoginErrorMessage.NETWORK_UNAVAILABLE + is DomainError.TokenInvalid -> LoginErrorMessage.SESSION_EXPIRED + else -> LoginErrorMessage.LOGIN_FAILED } } +enum class LoginErrorMessage { + REQUIRED_FIELDS, + INVALID_CREDENTIALS, + NETWORK_UNAVAILABLE, + SESSION_EXPIRED, + PREFERENCES_UNAVAILABLE, + LOGIN_FAILED, +} + data class LoginUiState( val username: String = "", val password: String = "", @@ -164,5 +173,5 @@ data class LoginUiState( val autoLogin: Boolean = false, val isLoadingPreferences: Boolean = false, val isSubmitting: Boolean = false, - val errorMessage: String? = null, + val errorMessage: LoginErrorMessage? = null, ) diff --git a/android/app/src/main/kotlin/com/flagship/abox/manager/ui/session/AppSessionViewModel.kt b/android/app/src/main/kotlin/com/flagship/abox/manager/ui/session/AppSessionViewModel.kt index 7525220..34da97a 100644 --- a/android/app/src/main/kotlin/com/flagship/abox/manager/ui/session/AppSessionViewModel.kt +++ b/android/app/src/main/kotlin/com/flagship/abox/manager/ui/session/AppSessionViewModel.kt @@ -16,6 +16,8 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -60,6 +62,33 @@ class AppSessionViewModel @Inject constructor( ) } } + viewModelScope.launch { + coordinator.observeActiveSession() + .map { it?.profileId } + .distinctUntilChanged() + .collect { sessionProfileId -> + if (sessionProfileId != null && bootstrapResult.value?.profileId == sessionProfileId) { + bootstrapResult.value = ProviderBootstrapResult( + profileId = sessionProfileId, + result = DomainResult.Success(null), + ) + } + } + } + viewModelScope.launch { + profileStore.observeActiveProfile() + .map { it?.id } + .distinctUntilChanged() + .collect { activeProfileId -> + val currentBootstrap = bootstrapResult.value + if (currentBootstrap != null && currentBootstrap.profileId != activeProfileId) { + bootstrapResult.value = ProviderBootstrapResult( + profileId = activeProfileId, + result = DomainResult.Success(null), + ) + } + } + } } private fun stateFor( diff --git a/android/app/src/main/res/values-zh-rCN/strings.xml b/android/app/src/main/res/values-zh-rCN/strings.xml index 3a812f4..90e8af8 100644 --- a/android/app/src/main/res/values-zh-rCN/strings.xml +++ b/android/app/src/main/res/values-zh-rCN/strings.xml @@ -19,6 +19,12 @@ 密码 记住用户名 自动登录 + 请输入用户名和密码。 + 用户名或密码不正确。 + 网络不可用,请重试。 + 会话已失效,请重新登录。 + 无法加载已保存的登录设置。 + 登录失败,请重试。 登录 退出登录 切换提供方 diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 0a63752..a2b1d2d 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -19,6 +19,12 @@ Password Remember username Sign in automatically + Username and password are required. + Invalid username or password. + Network unavailable. Try again. + Session expired. Sign in again. + Unable to load saved login settings. + Sign-in failed. Try again. Sign in Sign out Switch provider