feat(#471): implement DataStore-based server configuration and self-h… (#477)

* feat(#471): implement DataStore-based server configuration and self-healing

* refactor(#471): address Sourcery feedback on ANR risk and reset method splitting

* refactor(#471): implement synchronous ServerStore initialization to fully remove ANR risk

* feat(#471): show profile name, host and port on add profile, redirect to login on save, and support shortcut login buttons for existing profiles
This commit is contained in:
M57 authored and GitHub committed 2026-07-06 22:40:44 +03:00
1 parent 5697405b19
commit 430a083647
20 files changed
+774 -288

No files matched your search

@@ -51,6 +51,7 @@ 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
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -84,11 +85,20 @@ enum class SettingsTab {
@Composable
fun SettingsScreen(
onBack: () -> Unit,
onNavigateToLogin: () -> Unit = {},
onLogout: () -> Unit = {},
modifier: Modifier = Modifier,
viewModel: SettingsViewModel = viewModel { SettingsViewModel() },
) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
LaunchedEffect(state.navigateToLogin) {
if (state.navigateToLogin) {
onNavigateToLogin()
viewModel.clearNavigateToLogin()
}
}
var passwordVisible by remember { mutableStateOf(false) }
var selectedTab by remember { mutableStateOf(SettingsTab.CONNECTION) }
@@ -615,19 +625,21 @@ private fun ProfileEditorDialog(
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth(),
)
OutlinedTextField(
value = token,
onValueChange = onTokenChange,
label = { Text(stringResource(R.string.settings_field_token)) },
singleLine = true,
visualTransformation =
if (token.isNotEmpty()) {
PasswordVisualTransformation()
} else {
VisualTransformation.None
},
modifier = Modifier.fillMaxWidth(),
)
if (isEditing) {
OutlinedTextField(
value = token,
onValueChange = onTokenChange,
label = { Text(stringResource(R.string.settings_field_token)) },
singleLine = true,
visualTransformation =
if (token.isNotEmpty()) {
PasswordVisualTransformation()
} else {
VisualTransformation.None
},
modifier = Modifier.fillMaxWidth(),
)
}
}
},
confirmButton = {
@@ -3,6 +3,7 @@ package com.m57.hermescontrol.ui.settings
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.m57.hermescontrol.ScreenRegistry
import com.m57.hermescontrol.data.config.ConnectionProfile
import com.m57.hermescontrol.data.local.AuthManager
import com.m57.hermescontrol.data.remote.ApiClient
import com.m57.hermescontrol.data.remote.NetworkResult
@@ -34,7 +35,7 @@ data class SettingsUiState(
val selectedNavItems: List<String> = emptyList(),
val typingEffectEnabled: Boolean = false,
val typingEffectDelayMs: Int = 30,
val profiles: List<com.m57.hermescontrol.data.model.ConnectionProfile> = emptyList(),
val profiles: List<ConnectionProfile> = emptyList(),
val selectedProfileId: String? = null,
val renameProfileName: String = "",
val bottomNavDisplayMode: BottomNavDisplayMode = BottomNavDisplayMode.ICON_AND_TEXT,
@@ -49,6 +50,7 @@ data class SettingsUiState(
val showDeleteConfirm: Boolean = false,
val profileToDeleteId: String? = null,
val profileToDeleteName: String = "",
val navigateToLogin: Boolean = false,
)
class SettingsViewModel(
@@ -209,14 +211,16 @@ class SettingsViewModel(
} else {
// Add new profile
val newProfile =
com.m57.hermescontrol.data.model.ConnectionProfile(
ConnectionProfile(
name = name,
host = host,
port = port,
)
profiles.add(newProfile)
AuthManager.saveConnectionProfiles(profiles)
AuthManager.setProfileToken(newProfile.id, token)
AuthManager.setProfileToken(newProfile.id, "")
AuthManager.setSelectedProfileId(newProfile.id)
_uiState.update { it.copy(navigateToLogin = true) }
}
closeProfileDialog()
@@ -224,6 +228,10 @@ class SettingsViewModel(
ApiClient.rebuild()
}
fun clearNavigateToLogin() {
_uiState.update { it.copy(navigateToLogin = false) }
}
// ── Delete confirmation ──────────────────────────────────────────────
fun requestDeleteProfile(profileId: String) {