refactor(settings): edit settings tab (#496)
* refactor(settings): remove current connection editor fields Remove the active host, port, token fields, and clear token button from the main Connection tab, as profiles are now managed exclusively through the ProfileEditorDialog. * feat(settings): migrate to full auto-save design and remove save button Update settings change listeners (auto-reconnect, typing effect delay/toggle) to save state immediately to AuthManager. Remove the manual Save button from the scaffold's top bar and delete the redundant SaveIndicator. * style(settings): clean up unused imports from settings UI cleanup * fix(chat): resolve compose resource lint error and initial state test failure Pre-resolve empty credentials string resource at the composable scope to fix LocalContext resource query lint error. Update testInitialStateAndConnection to use DISCONNECTED state to trigger connection and pass verification.
This commit is contained in:
5 files changed
+185
-167
No files matched your search
@@ -1667,7 +1667,7 @@ private fun ReloginDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onRelogin: (String, String, (Boolean, String?) -> Unit) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val emptyCredentialsError = stringResource(R.string.chat_relogin_error_empty)
|
||||
val statusColors = LocalHermesStatusColors.current
|
||||
var username by rememberSaveable { mutableStateOf("") }
|
||||
var password by rememberSaveable { mutableStateOf("") }
|
||||
@@ -1721,7 +1721,7 @@ private fun ReloginDialog(
|
||||
enabled = !isLoading,
|
||||
onClick = {
|
||||
if (username.isBlank() || password.isBlank()) {
|
||||
errorMessage = context.getString(R.string.chat_relogin_error_empty)
|
||||
errorMessage = emptyCredentialsError
|
||||
return@TextButton
|
||||
}
|
||||
isLoading = true
|
||||
|
||||
@@ -21,7 +21,6 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.PrimaryScrollableTabRow
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -44,7 +43,6 @@ import com.m57.hermescontrol.ui.settings.components.BehaviorSection
|
||||
import com.m57.hermescontrol.ui.settings.components.ChatSection
|
||||
import com.m57.hermescontrol.ui.settings.components.ConnectionSection
|
||||
import com.m57.hermescontrol.ui.settings.components.NavBarSection
|
||||
import com.m57.hermescontrol.ui.settings.components.SaveIndicator
|
||||
import com.m57.hermescontrol.ui.settings.components.TestConnectionButton
|
||||
import com.m57.hermescontrol.ui.settings.components.TestResultCard
|
||||
|
||||
@@ -81,11 +79,6 @@ fun SettingsScreen(
|
||||
HermesScaffold(
|
||||
title = { Text(stringResource(R.string.screen_settings)) },
|
||||
navigationIcon = NavIcon.Back(onBack),
|
||||
actions = {
|
||||
TextButton(onClick = viewModel::save) {
|
||||
Text(stringResource(R.string.action_save))
|
||||
}
|
||||
},
|
||||
) {
|
||||
Column(
|
||||
modifier =
|
||||
@@ -133,7 +126,6 @@ fun SettingsScreen(
|
||||
)
|
||||
|
||||
TestResultCard(testResult = state.testResult)
|
||||
SaveIndicator(isSaved = state.isSaved)
|
||||
TestConnectionButton(
|
||||
isTesting = state.isTesting,
|
||||
onTest = viewModel::testConnection,
|
||||
|
||||
@@ -278,6 +278,7 @@ class SettingsViewModel(
|
||||
|
||||
fun onAutoReconnectChange(enabled: Boolean) {
|
||||
_uiState.update { it.copy(autoReconnect = enabled, isSaved = false) }
|
||||
AuthManager.setAutoReconnect(enabled)
|
||||
}
|
||||
|
||||
fun onThemeChange(theme: ThemePreference) {
|
||||
@@ -302,10 +303,12 @@ class SettingsViewModel(
|
||||
|
||||
fun onTypingEffectEnabledChange(enabled: Boolean) {
|
||||
_uiState.update { it.copy(typingEffectEnabled = enabled, isSaved = false) }
|
||||
AuthManager.setTypingEffectEnabled(enabled)
|
||||
}
|
||||
|
||||
fun onTypingEffectDelayMsChange(delayMs: Int) {
|
||||
_uiState.update { it.copy(typingEffectDelayMs = delayMs, isSaved = false) }
|
||||
AuthManager.setTypingEffectDelayMs(delayMs)
|
||||
}
|
||||
|
||||
/** Clear all auth credentials — logs out and returns to landing screen. */
|
||||
|
||||
@@ -12,23 +12,17 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -38,9 +32,6 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.m57.hermescontrol.R
|
||||
@@ -140,95 +131,6 @@ internal fun ConnectionSection(
|
||||
Spacer(modifier = Modifier.size(4.dp))
|
||||
Text(stringResource(R.string.settings_action_add_profile))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = state.host,
|
||||
onValueChange = viewModel::onHostChange,
|
||||
label = { Text(stringResource(R.string.settings_field_host)) },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors =
|
||||
OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = MaterialTheme.colorScheme.primary,
|
||||
cursorColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = state.port,
|
||||
onValueChange = viewModel::onPortChange,
|
||||
label = { Text(stringResource(R.string.settings_field_port)) },
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors =
|
||||
OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = MaterialTheme.colorScheme.primary,
|
||||
cursorColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = state.token,
|
||||
onValueChange = viewModel::onTokenChange,
|
||||
label = { Text(stringResource(R.string.settings_field_token)) },
|
||||
singleLine = true,
|
||||
visualTransformation =
|
||||
if (passwordVisible) {
|
||||
VisualTransformation.None
|
||||
} else {
|
||||
PasswordVisualTransformation()
|
||||
},
|
||||
trailingIcon = {
|
||||
IconButton(onClick = onPasswordVisibilityToggle) {
|
||||
Icon(
|
||||
imageVector =
|
||||
if (passwordVisible) {
|
||||
Icons.Filled.Visibility
|
||||
} else {
|
||||
Icons.Filled.VisibilityOff
|
||||
},
|
||||
contentDescription =
|
||||
if (passwordVisible) {
|
||||
stringResource(R.string.content_desc_hide_token)
|
||||
} else {
|
||||
stringResource(R.string.content_desc_show_token)
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors =
|
||||
OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = MaterialTheme.colorScheme.primary,
|
||||
cursorColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// B7 (Jun 18 2026): one-tap escape hatch for when the stored
|
||||
// token is corrupted / rejected. Clears the token field and
|
||||
// persists the empty value immediately so a stress-free
|
||||
// re-pair is possible without manually selecting-and-deleting
|
||||
// the masked password field character by character.
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
viewModel.onTokenChange("")
|
||||
viewModel.save()
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(stringResource(R.string.settings_action_clear_token))
|
||||
}
|
||||
}
|
||||
|
||||
// ── Dialogs ──────────────────────────────────────────────────────────
|
||||
@@ -288,23 +190,6 @@ internal fun TestResultCard(testResult: String?) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun SaveIndicator(isSaved: Boolean) {
|
||||
AnimatedVisibility(
|
||||
visible = isSaved,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.settings_save_success),
|
||||
style =
|
||||
MaterialTheme.typography.bodyMedium.copy(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun TestConnectionButton(
|
||||
isTesting: Boolean,
|
||||
|
||||
Reference in new issue
Block a user