From 1ad5f30ede4a4c81a6ff82cd3af3fb3db9dc26dd Mon Sep 17 00:00:00 2001 From: 2dust <31833384+2dust@users.noreply.github.com> Date: Mon, 21 Sep 2026 19:10:10 +0800 Subject: [PATCH] Show inline validation errors in forms Move several save-time validation failures from toast messages to field error states in the server and subscription editors. Password validation now marks the relevant server form field, and subscription editing highlights invalid URLs and update intervals before save instead of relying on transient toasts. --- .../v2ray/ang/ui/server/BaseServerActivity.kt | 7 ++-- .../ang/ui/server/ServerHysteria2Activity.kt | 11 ++--- .../ui/server/ServerShadowsocksActivity.kt | 7 ++-- .../ang/ui/server/ServerTrojanActivity.kt | 10 ++--- .../com/v2ray/ang/ui/server/ServerUiState.kt | 1 + .../ang/ui/server/ServerVlessActivity.kt | 9 ++--- .../ang/ui/server/ServerVmessActivity.kt | 9 ++--- .../ang/ui/subscription/SubEditActivity.kt | 40 ++++++++++++------- 8 files changed, 50 insertions(+), 44 deletions(-) diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/BaseServerActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/BaseServerActivity.kt index 7e7f7c592..b93181dee 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/BaseServerActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/BaseServerActivity.kt @@ -382,13 +382,13 @@ abstract class BaseServerActivity : BaseComponentActivity() { protected open fun validateProtocolConfig(config: ProfileItem): Boolean = true - protected open fun validateCommonConfig(config: ProfileItem): Boolean { + protected open fun validateCommonConfig(state: ServerUiState, config: ProfileItem): Boolean { if (config.password.isNullOrBlank()) { + state.isPasswordError = true if (config.configType == EConfigType.VMESS || config.configType == EConfigType.VLESS ) { - toast(R.string.server_lab_id) return false } @@ -396,7 +396,6 @@ abstract class BaseServerActivity : BaseComponentActivity() { config.configType == EConfigType.SHADOWSOCKS || config.configType == EConfigType.HYSTERIA2 ) { - toast(R.string.server_lab_id3) return false } } @@ -422,7 +421,7 @@ abstract class BaseServerActivity : BaseComponentActivity() { protected fun saveServer(state: ServerUiState): Boolean { if (!validateBasicConfig(state)) return false val config = state.toProfileItem(initialConfig) - if (!validateCommonConfig(config)) return false + if (!validateCommonConfig(state, config)) return false if (!validateProtocolConfig(config)) return false config.description = AngConfigManager.generateDescription(config) diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerHysteria2Activity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerHysteria2Activity.kt index 85b8886cc..5d5692853 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerHysteria2Activity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerHysteria2Activity.kt @@ -1,14 +1,12 @@ package com.v2ray.ang.ui.server import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.res.stringResource import com.v2ray.ang.AppConfig import com.v2ray.ang.R import com.v2ray.ang.dto.entities.ProfileItem import com.v2ray.ang.enums.EConfigType -import com.v2ray.ang.extension.toast import com.v2ray.ang.ui.compose.FormTextField import com.v2ray.ang.ui.compose.SettingsSwitchItem @@ -18,7 +16,6 @@ class ServerHysteria2Activity : BaseServerActivity() { @Composable override fun ScreenContent() { - val scope = rememberCoroutineScope() val uiState = rememberSaveable(saver = ServerUiState.Saver) { ServerUiState.from( initialConfig = initialConfig @@ -39,7 +36,6 @@ class ServerHysteria2Activity : BaseServerActivity() { override fun validateProtocolConfig(config: ProfileItem): Boolean { if (config.password.isNullOrBlank()) { - toast(R.string.server_lab_id3) return false } if (config.security.isNullOrBlank()) { @@ -51,9 +47,10 @@ class ServerHysteria2Activity : BaseServerActivity() { @Composable private fun Hysteria2ProtocolFields(state: ServerUiState) { FormTextField( - stringResource(R.string.server_lab_id3), - state.password, - { state.password = it } + label = stringResource(R.string.server_lab_id3), + value = state.password, + onValueChange = { state.password = it }, + isError = state.isPasswordError ) FormTextField( stringResource(R.string.server_obfs_password), diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerShadowsocksActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerShadowsocksActivity.kt index ee8127643..a06f225a5 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerShadowsocksActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerShadowsocksActivity.kt @@ -52,9 +52,10 @@ class ServerShadowsocksActivity : BaseServerActivity() { methodOptions: List ) { FormTextField( - stringResource(R.string.server_lab_id3), - state.password, - { state.password = it } + label = stringResource(R.string.server_lab_id3), + value = state.password, + onValueChange = { state.password = it }, + isError = state.isPasswordError ) FormDropdownField( stringResource(R.string.server_lab_security), diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerTrojanActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerTrojanActivity.kt index 9713c3d20..62c376665 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerTrojanActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerTrojanActivity.kt @@ -7,7 +7,6 @@ import androidx.compose.ui.res.stringResource import com.v2ray.ang.R import com.v2ray.ang.dto.entities.ProfileItem import com.v2ray.ang.enums.EConfigType -import com.v2ray.ang.extension.toast import com.v2ray.ang.ui.compose.FormTextField class ServerTrojanActivity : BaseServerActivity() { @@ -44,11 +43,9 @@ class ServerTrojanActivity : BaseServerActivity() { override fun validateProtocolConfig(config: ProfileItem): Boolean { if (config.password.isNullOrBlank()) { - toast(R.string.server_lab_id3) return false } if (config.security.isNullOrBlank()) { - toast(R.string.server_lab_stream_security) return false } return true @@ -57,9 +54,10 @@ class ServerTrojanActivity : BaseServerActivity() { @Composable private fun TrojanProtocolFields(state: ServerUiState) { FormTextField( - stringResource(R.string.server_lab_id3), - state.password, - { state.password = it } + label = stringResource(R.string.server_lab_id3), + value = state.password, + onValueChange = { state.password = it }, + isError = state.isPasswordError ) } } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerUiState.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerUiState.kt index 767399f86..eb9bf1ca7 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerUiState.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerUiState.kt @@ -117,6 +117,7 @@ class ServerUiState( var isRemarksError by mutableStateOf(false) var isAddressError by mutableStateOf(false) var isPortError by mutableStateOf(false) + var isPasswordError by mutableStateOf(false) fun toProfileItem(initialConfig: ProfileItem): ProfileItem { val isVmess = configType == EConfigType.VMESS diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerVlessActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerVlessActivity.kt index 19595c393..8099d28c6 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerVlessActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerVlessActivity.kt @@ -8,7 +8,6 @@ import androidx.compose.ui.res.stringResource import com.v2ray.ang.R import com.v2ray.ang.dto.entities.ProfileItem import com.v2ray.ang.enums.EConfigType -import com.v2ray.ang.extension.toast import com.v2ray.ang.ui.compose.FormDropdownField import com.v2ray.ang.ui.compose.FormTextField @@ -47,7 +46,6 @@ class ServerVlessActivity : BaseServerActivity() { override fun validateProtocolConfig(config: ProfileItem): Boolean { if (config.password.isNullOrBlank()) { - toast(R.string.server_lab_id) return false } return true @@ -59,9 +57,10 @@ class ServerVlessActivity : BaseServerActivity() { flowOptions: List ) { FormTextField( - stringResource(R.string.server_lab_id), - state.password, - { state.password = it } + label = stringResource(R.string.server_lab_id), + value = state.password, + onValueChange = { state.password = it }, + isError = state.isPasswordError ) FormTextField( stringResource(R.string.server_lab_encryption), diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerVmessActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerVmessActivity.kt index 7eec9f46b..9217bcb83 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerVmessActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerVmessActivity.kt @@ -8,7 +8,6 @@ import androidx.compose.ui.res.stringResource import com.v2ray.ang.R import com.v2ray.ang.dto.entities.ProfileItem import com.v2ray.ang.enums.EConfigType -import com.v2ray.ang.extension.toast import com.v2ray.ang.ui.compose.FormDropdownField import com.v2ray.ang.ui.compose.FormTextField @@ -47,7 +46,6 @@ class ServerVmessActivity : BaseServerActivity() { override fun validateProtocolConfig(config: ProfileItem): Boolean { if (config.password.isNullOrBlank()) { - toast(R.string.server_lab_id) return false } return true @@ -59,9 +57,10 @@ class ServerVmessActivity : BaseServerActivity() { methodOptions: List ) { FormTextField( - stringResource(R.string.server_lab_id), - state.password, - { state.password = it } + label = stringResource(R.string.server_lab_id), + value = state.password, + onValueChange = { state.password = it }, + isError = state.isPasswordError ) FormDropdownField( stringResource(R.string.server_lab_security), diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/subscription/SubEditActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/subscription/SubEditActivity.kt index ca0c691da..793f44697 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/subscription/SubEditActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/subscription/SubEditActivity.kt @@ -29,7 +29,6 @@ import com.v2ray.ang.R import com.v2ray.ang.dto.entities.SubscriptionItem import com.v2ray.ang.enums.EConfigType import com.v2ray.ang.extension.toLongEx -import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toastSuccess import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.SettingsChangeManager @@ -78,25 +77,19 @@ class SubEditActivity : BaseComponentActivity() { } private fun saveServer(subItem: SubscriptionItem): Boolean { - if (TextUtils.isEmpty(subItem.remarks)) { return false } if (subItem.url.isNotEmpty()) { if (!Utils.isValidUrl(subItem.url)) { - toast(R.string.toast_invalid_url) return false } - if (!Utils.isValidSubUrl(subItem.url)) { - toast(R.string.toast_insecure_url_protocol) - if (!subItem.allowInsecureUrl) { - return false - } + if (!Utils.isValidSubUrl(subItem.url) && !subItem.allowInsecureUrl) { + return false } } if (subItem.autoUpdate && subItem.updateInterval < AppConfig.SUBSCRIPTION_MIN_INTERVAL_MINUTES) { - toast(R.string.toast_invalid_update_interval) return false } @@ -133,12 +126,14 @@ fun SubEditScreen( var remarks by rememberSaveable { mutableStateOf(initial.remarks.orEmpty()) } var isRemarksError by rememberSaveable { mutableStateOf(false) } var url by rememberSaveable { mutableStateOf(initial.url.orEmpty()) } + var isUrlError by rememberSaveable { mutableStateOf(false) } var userAgent by rememberSaveable { mutableStateOf(initial.userAgent.orEmpty()) } var requestHeaders by rememberSaveable { mutableStateOf(initial.requestHeaders.orEmpty()) } var filter by rememberSaveable { mutableStateOf(initial.filter ?: "") } var enabled by rememberSaveable { mutableStateOf(initial.enabled) } var autoUpdate by rememberSaveable { mutableStateOf(initial.autoUpdate) } var updateInterval by rememberSaveable { mutableStateOf(initial.updateInterval.toString()) } + var isUpdateIntervalError by rememberSaveable { mutableStateOf(false) } var allowInsecureUrl by rememberSaveable { mutableStateOf(initial.allowInsecureUrl) } var prevProfile by rememberSaveable { mutableStateOf(initial.prevProfile ?: "") } var nextProfile by rememberSaveable { mutableStateOf(initial.nextProfile ?: "") } @@ -179,9 +174,16 @@ fun SubEditScreen( } IconButton(onClick = { val remarksErr = remarks.isBlank() - isRemarksError = remarksErr + val urlErr = url.isNotEmpty() && ( + !Utils.isValidUrl(url) || (!Utils.isValidSubUrl(url) && !allowInsecureUrl) + ) + val intervalErr = autoUpdate && updateInterval.toLongEx() < AppConfig.SUBSCRIPTION_MIN_INTERVAL_MINUTES - val hasError = remarksErr + isRemarksError = remarksErr + isUrlError = urlErr + isUpdateIntervalError = intervalErr + + val hasError = remarksErr || urlErr || intervalErr if (!hasError) { onSave(buildSubItem()) } @@ -209,7 +211,13 @@ fun SubEditScreen( onValueChange = { remarks = it }, isError = isRemarksError ) - FormTextField(stringResource(R.string.sub_setting_url), url, { url = it }) + FormTextField( + label = stringResource(R.string.sub_setting_url), + value = url, + onValueChange = { url = it }, + isError = isUrlError, + supportingText = if (isUrlError) stringResource(R.string.toast_invalid_url) else null + ) FormTextField(stringResource(R.string.sub_setting_user_agent), userAgent, { userAgent = it }) FormTextField(stringResource(R.string.sub_setting_request_headers), requestHeaders, { requestHeaders = it }) FormTextField(stringResource(R.string.sub_setting_filter), filter, { filter = it }) @@ -226,8 +234,12 @@ fun SubEditScreen( ) FormTextField( - stringResource(R.string.title_pref_auto_update_interval), - updateInterval, { updateInterval = it }, keyboardType = KeyboardType.Number + label = stringResource(R.string.title_pref_auto_update_interval), + value = updateInterval, + onValueChange = { updateInterval = it }, + keyboardType = KeyboardType.Number, + isError = isUpdateIntervalError, + supportingText = if (isUpdateIntervalError) stringResource(R.string.toast_invalid_update_interval) else null ) SettingsSwitchItem(