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:
parent
fc64b422e7
commit
5677f9cca1
@ -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
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user