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:
M57 authored and GitHub committed 2026-07-08 02:07:11 +03:00
1 parent 04bb7c4325
commit 21092c79f2
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,