* feat(#107): implement saved connection profiles support * fix(connect): fallback profiles resolution and stubbing in tests
This commit is contained in:
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) }
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user