feat(connect): saved connection profiles support (#107) (#191)

* feat(#107): implement saved connection profiles support

* fix(connect): fallback profiles resolution and stubbing in tests
This commit is contained in:
M57 authored and GitHub committed 2026-06-21 00:18:35 +03:00
1 parent 0eed12d18e
commit 00c6e75f50
8 files changed
+479 -13

No files matched your search

@@ -91,6 +91,86 @@ fun SettingsScreen(
) {
// Connection section
SectionCard(title = "Connection") {
// Connection Profiles management in Settings
if (state.profiles.isNotEmpty()) {
var profilesExpanded by remember { mutableStateOf(false) }
Text(
text = "Connection Profile",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Box(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)) {
OutlinedButton(
onClick = { profilesExpanded = true },
modifier = Modifier.fillMaxWidth(),
) {
val activeProfile = state.profiles.firstOrNull { it.id == state.selectedProfileId }
Text(text = activeProfile?.name ?: "Standalone / Default")
}
DropdownMenu(
expanded = profilesExpanded,
onDismissRequest = { profilesExpanded = false },
modifier = Modifier.fillMaxWidth(0.85f),
) {
DropdownMenuItem(
text = { Text("None (Standalone / Default)") },
onClick = {
viewModel.selectProfile(null)
profilesExpanded = false
},
)
state.profiles.forEach { profile ->
DropdownMenuItem(
text = {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(profile.name)
IconButton(
onClick = {
viewModel.deleteProfile(profile.id)
profilesExpanded = false
},
) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = "Delete Profile",
tint = MaterialTheme.colorScheme.error,
)
}
}
},
onClick = {
viewModel.selectProfile(profile.id)
profilesExpanded = false
},
)
}
}
}
if (state.selectedProfileId != null) {
OutlinedTextField(
value = state.renameProfileName,
onValueChange = viewModel::onRenameProfileNameChange,
label = { Text("Rename Profile") },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
trailingIcon = {
Button(
onClick = viewModel::renameProfile,
modifier = Modifier.padding(end = 4.dp),
) {
Text("Rename")
}
},
)
Spacer(modifier = Modifier.height(12.dp))
}
}
OutlinedTextField(
value = state.host,
onValueChange = viewModel::onHostChange,
@@ -27,6 +27,9 @@ 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 selectedProfileId: String? = null,
val renameProfileName: String = "",
)
class SettingsViewModel : ViewModel() {
@@ -38,6 +41,7 @@ class SettingsViewModel : ViewModel() {
}
private fun loadSettings() {
val selectedId = AuthManager.getSelectedProfileId()
_uiState.update {
it.copy(
host = AuthManager.getHost(),
@@ -51,10 +55,50 @@ class SettingsViewModel : ViewModel() {
selectedNavItems = AuthManager.getBottomNavItems(),
typingEffectEnabled = AuthManager.isTypingEffectEnabled(),
typingEffectDelayMs = AuthManager.getTypingEffectDelayMs(),
profiles = AuthManager.getConnectionProfiles(),
selectedProfileId = selectedId,
renameProfileName =
AuthManager.getConnectionProfiles().firstOrNull {
p ->
p.id == selectedId
}?.name ?: "",
)
}
}
fun selectProfile(profileId: String?) {
AuthManager.setSelectedProfileId(profileId)
loadSettings()
ApiClient.rebuild()
}
fun onRenameProfileNameChange(value: String) {
_uiState.update { it.copy(renameProfileName = value, isSaved = false) }
}
fun renameProfile() {
val currentId = _uiState.value.selectedProfileId ?: return
val newName = _uiState.value.renameProfileName.trim()
if (newName.isBlank()) return
val updatedProfiles =
AuthManager.getConnectionProfiles().map {
if (it.id == currentId) it.copy(name = newName) else it
}
AuthManager.saveConnectionProfiles(updatedProfiles)
loadSettings()
}
fun deleteProfile(profileId: String) {
val updatedProfiles = AuthManager.getConnectionProfiles().filter { it.id != profileId }
AuthManager.saveConnectionProfiles(updatedProfiles)
AuthManager.setProfileToken(profileId, null)
if (AuthManager.getSelectedProfileId() == profileId) {
AuthManager.setSelectedProfileId(null)
}
loadSettings()
ApiClient.rebuild()
}
fun onHostChange(value: String) {
_uiState.update { it.copy(host = value.trim(), isSaved = false) }
}
@@ -145,6 +189,7 @@ class SettingsViewModel : ViewModel() {
AuthManager.setTypingEffectDelayMs(state.typingEffectDelayMs)
ApiClient.rebuild()
loadSettings()
_uiState.update { it.copy(isSaved = true, testResult = null) }
}