feat(android): edit saved server entries; bump companion to 0.4.11 (vc15)

Add an edit affordance to each saved server in ServerConnectScreen: a
pencil button loads the entry into the form (Edit Server mode) with
Save Changes / Cancel actions. Persisted via a new
ServerPreferences.updateSavedServer() that replaces by connection
identity (address/port/scheme) and keeps the active record in sync when
the edited server is the active one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian 2026-06-26 12:53:29 +01:00
parent fc64b422e7
commit 5677f9cca1
5 changed files with 126 additions and 15 deletions

View File

@ -11,8 +11,8 @@ android {
applicationId = "com.archipelago.app" applicationId = "com.archipelago.app"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 14 versionCode = 15
versionName = "0.4.10" versionName = "0.4.11"
vectorDrawables { vectorDrawables {
useSupportLibrary = true useSupportLibrary = true

View File

@ -112,6 +112,37 @@ class ServerPreferences(private val context: Context) {
} }
} }
/**
* Replace a saved server in place. Matches the existing entry by connection
* identity (address/port/scheme) so edits that change the name or password
* or that touch a legacy 4-field entry still update the right record. If the
* edited server is also the active one, the active record is kept in sync.
*/
suspend fun updateSavedServer(original: ServerEntry, updated: ServerEntry) {
context.dataStore.edit { prefs ->
val current = prefs[savedServersKey] ?: emptySet()
val filtered = current.filterNot { raw ->
val e = ServerEntry.deserialize(raw)
e != null &&
e.address == original.address &&
e.port == original.port &&
e.useHttps == original.useHttps
}.toSet()
prefs[savedServersKey] = filtered + updated.serialize()
val isActive = prefs[activeAddressKey] == original.address &&
(prefs[activePortKey] ?: "") == original.port &&
(prefs[activeHttpsKey] ?: false) == original.useHttps
if (isActive) {
prefs[activeAddressKey] = updated.address
prefs[activeHttpsKey] = updated.useHttps
prefs[activePortKey] = updated.port
prefs[activePasswordKey] = updated.password
prefs[activeNameKey] = updated.name
}
}
}
suspend fun removeSavedServer(server: ServerEntry) { suspend fun removeSavedServer(server: ServerEntry) {
context.dataStore.edit { prefs -> context.dataStore.edit { prefs ->
val current = prefs[savedServersKey] ?: emptySet() val current = prefs[savedServersKey] ?: emptySet()

View File

@ -30,6 +30,7 @@ import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.LockOpen import androidx.compose.material.icons.filled.LockOpen
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
@ -106,9 +107,50 @@ fun ServerConnectScreen(
var useHttps by remember { mutableStateOf(false) } var useHttps by remember { mutableStateOf(false) }
var isConnecting by remember { mutableStateOf(false) } var isConnecting by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf<String?>(null) } var errorMessage by remember { mutableStateOf<String?>(null) }
// The saved server currently being edited, or null when adding/connecting.
var editingServer by remember { mutableStateOf<ServerEntry?>(null) }
val savedServers by prefs.savedServers.collectAsState(initial = emptyList()) val savedServers by prefs.savedServers.collectAsState(initial = emptyList())
fun clearForm() {
name = ""
address = ""
port = ""
password = ""
useHttps = false
passwordVisible = false
errorMessage = null
}
fun startEdit(server: ServerEntry) {
editingServer = server
name = server.name
address = server.address
port = server.port
password = server.password
useHttps = server.useHttps
passwordVisible = false
errorMessage = null
}
fun cancelEdit() {
editingServer = null
clearForm()
}
fun saveEdit() {
val original = editingServer ?: return
if (address.isBlank()) {
errorMessage = "Enter a server address"
return
}
val updated = ServerEntry(address, useHttps, port, password, name)
scope.launch {
prefs.updateSavedServer(original, updated)
cancelEdit()
}
}
fun connect(server: ServerEntry) { fun connect(server: ServerEntry) {
if (isConnecting) return if (isConnecting) return
if (server.address.isBlank()) { if (server.address.isBlank()) {
@ -178,7 +220,7 @@ fun ServerConnectScreen(
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
Text( Text(
text = "Connect to Server", text = if (editingServer != null) stringResource(R.string.edit_server_title) else "Connect to Server",
style = MaterialTheme.typography.headlineMedium, style = MaterialTheme.typography.headlineMedium,
color = TextPrimary, color = TextPrimary,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
@ -324,7 +366,11 @@ fun ServerConnectScreen(
keyboardActions = KeyboardActions( keyboardActions = KeyboardActions(
onGo = { onGo = {
keyboard?.hide() keyboard?.hide()
connect(ServerEntry(address, useHttps, port, password, name)) if (editingServer != null) {
saveEdit()
} else {
connect(ServerEntry(address, useHttps, port, password, name))
}
}, },
), ),
colors = OutlinedTextFieldDefaults.colors( colors = OutlinedTextFieldDefaults.colors(
@ -389,15 +435,40 @@ fun ServerConnectScreen(
} }
} }
// Connect button — glass style if (editingServer != null) {
GlassButton( // Save / Cancel while editing an existing saved server
text = if (isConnecting) stringResource(R.string.connecting) else stringResource(R.string.connect), Row(
onClick = { modifier = Modifier.fillMaxWidth(),
keyboard?.hide() horizontalArrangement = Arrangement.spacedBy(12.dp),
connect(ServerEntry(address, useHttps, port, password, name)) ) {
}, GlassButton(
modifier = Modifier.fillMaxWidth().height(56.dp), text = stringResource(R.string.cancel),
) onClick = {
keyboard?.hide()
cancelEdit()
},
modifier = Modifier.weight(1f).height(56.dp),
)
GlassButton(
text = stringResource(R.string.save_changes),
onClick = {
keyboard?.hide()
saveEdit()
},
modifier = Modifier.weight(1f).height(56.dp),
)
}
} else {
// Connect button — glass style
GlassButton(
text = if (isConnecting) stringResource(R.string.connecting) else stringResource(R.string.connect),
onClick = {
keyboard?.hide()
connect(ServerEntry(address, useHttps, port, password, name))
},
modifier = Modifier.fillMaxWidth().height(56.dp),
)
}
if (isConnecting) { if (isConnecting) {
CircularProgressIndicator( CircularProgressIndicator(
@ -407,8 +478,8 @@ fun ServerConnectScreen(
) )
} }
// Saved servers // Saved servers (hidden while editing one to keep focus on the form)
if (savedServers.isNotEmpty()) { if (editingServer == null && savedServers.isNotEmpty()) {
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text( Text(
text = stringResource(R.string.saved_servers), text = stringResource(R.string.saved_servers),
@ -422,6 +493,7 @@ fun ServerConnectScreen(
SavedServerItem( SavedServerItem(
server = server, server = server,
onConnect = { connect(it) }, onConnect = { connect(it) },
onEdit = { startEdit(it) },
onRemove = { scope.launch { prefs.removeSavedServer(it) } }, onRemove = { scope.launch { prefs.removeSavedServer(it) } },
) )
} }
@ -434,6 +506,7 @@ fun ServerConnectScreen(
private fun SavedServerItem( private fun SavedServerItem(
server: ServerEntry, server: ServerEntry,
onConnect: (ServerEntry) -> Unit, onConnect: (ServerEntry) -> Unit,
onEdit: (ServerEntry) -> Unit,
onRemove: (ServerEntry) -> Unit, onRemove: (ServerEntry) -> Unit,
) { ) {
Row( Row(
@ -476,6 +549,9 @@ private fun SavedServerItem(
} }
} }
} }
IconButton(onClick = { onEdit(server) }) {
Icon(imageVector = Icons.Default.Edit, contentDescription = stringResource(R.string.edit_server), modifier = Modifier.size(18.dp), tint = TextMuted)
}
IconButton(onClick = { onRemove(server) }) { IconButton(onClick = { onRemove(server) }) {
Icon(imageVector = Icons.Default.Close, contentDescription = stringResource(R.string.remove_server), modifier = Modifier.size(18.dp), tint = TextMuted) Icon(imageVector = Icons.Default.Close, contentDescription = stringResource(R.string.remove_server), modifier = Modifier.size(18.dp), tint = TextMuted)
} }

View File

@ -28,4 +28,8 @@
<string name="refresh">Refresh</string> <string name="refresh">Refresh</string>
<string name="server_name_label">Server Name (optional)</string> <string name="server_name_label">Server Name (optional)</string>
<string name="server_name_placeholder">My Archipelago</string> <string name="server_name_placeholder">My Archipelago</string>
<string name="edit_server">Edit</string>
<string name="edit_server_title">Edit Server</string>
<string name="save_changes">Save Changes</string>
<string name="cancel">Cancel</string>
</resources> </resources>