diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt index e5abf80f..b7cf7775 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt @@ -203,6 +203,8 @@ class AppsViewModel( _state.update { it.copy(lastCheckedTimestamp = System.currentTimeMillis()) } + } catch (e: CancellationException) { + throw e } catch (e: Exception) { logger.error("Check all for updates failed: ${e.message}") } finally { @@ -220,6 +222,8 @@ class AppsViewModel( val now = System.currentTimeMillis() lastAutoCheckTimestamp = now _state.update { it.copy(lastCheckedTimestamp = now) } + } catch (e: CancellationException) { + throw e } catch (e: Exception) { logger.error("Refresh failed: ${e.message}") } finally { diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt index 1e1d42f9..3a33905c 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt @@ -37,6 +37,7 @@ import zed.rainxch.tweaks.presentation.model.ProxyScopeFormState import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.failed_to_save_proxy_settings import zed.rainxch.githubstore.core.presentation.res.invalid_proxy_port +import zed.rainxch.githubstore.core.presentation.res.proxy_host_invalid import zed.rainxch.githubstore.core.presentation.res.proxy_host_required import zed.rainxch.githubstore.core.presentation.res.proxy_test_error_auth_required import zed.rainxch.githubstore.core.presentation.res.proxy_test_error_dns @@ -635,13 +636,14 @@ class TweaksViewModel( val host = form.host.trim().takeIf { isValidProxyHost(it) } ?: run { - val msg = - if (form.host.isBlank()) { - getString(Res.string.proxy_host_required) - } else { - getString(Res.string.proxy_host_invalid) - } + val isBlank = form.host.isBlank() viewModelScope.launch { + val msg = + if (isBlank) { + getString(Res.string.proxy_host_required) + } else { + getString(Res.string.proxy_host_invalid) + } _events.send(TweaksEvent.OnProxySaveError(msg)) } return @@ -1021,13 +1023,14 @@ class TweaksViewModel( val host = form.host.trim().takeIf { isValidProxyHost(it) } ?: run { - val msg = - if (form.host.isBlank()) { - getString(Res.string.proxy_host_required) - } else { - getString(Res.string.proxy_host_invalid) - } + val isBlank = form.host.isBlank() viewModelScope.launch { + val msg = + if (isBlank) { + getString(Res.string.proxy_host_required) + } else { + getString(Res.string.proxy_host_invalid) + } _events.send(TweaksEvent.OnProxyTestError(msg)) } return null diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Network.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Network.kt index c42197b0..73117be4 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Network.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Network.kt @@ -41,6 +41,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation @@ -244,6 +245,7 @@ private fun ProxyDetailsFields( onAction: (TweaksAction) -> Unit, ) { val focusManager = LocalFocusManager.current + val keyboardController = LocalSoftwareKeyboardController.current val portValue = form.port val isPortInvalid = portValue.isNotEmpty() && @@ -356,24 +358,33 @@ private fun ProxyDetailsFields( ) { ProxyTestButton( isInProgress = form.isTestInProgress, - enabled = isFormValid && !form.isTestInProgress, + // Keep enabled regardless of form validity so the user + // never taps a disabled button by accident on the first + // press (the previous `isFormValid` gate raced with + // the IME-composition-commit step on some Android + // keyboards and silently swallowed taps). VM still + // validates and surfaces a clear error event. + enabled = !form.isTestInProgress, onClick = { + keyboardController?.hide() focusManager.clearFocus() onAction(TweaksAction.OnProxyTest(scope)) }, ) FilledTonalButton( - // `clearFocus()` first so the IME commits any pending - // keystroke before the save runs — without this the - // user sometimes has to tap Save twice on Android, - // because the first tap goes to focus dismissal and the - // second one actually fires the click. onClick = { + // Hide IME first so any pending composition commits + // synchronously before the VM reads form state. + // `clearFocus()` alone isn't enough on Gboard / + // SwiftKey, which keep characters in a composition + // buffer until focus actually transfers — that's + // what made the user have to tap Save twice. + keyboardController?.hide() focusManager.clearFocus() onAction(TweaksAction.OnProxySave(scope)) }, - enabled = isFormValid && !form.isTestInProgress, + enabled = !form.isTestInProgress, ) { Icon( imageVector = Icons.Default.Save,