feat(media-servers): auto-save on every change; polish cache card & add flow

Addresses review feedback on the redesign:

- Auto-save: drop the Save button and replace the cancel "X" with a
  standard back arrow (TopBarWithBackButton). Every add/remove persists
  immediately; a reorder persists once the drag gesture ends, so dragging
  no longer publishes a kind-10063 event on every intermediate swap.
- On-device cache card now uses the app's filled surfaceContainer card
  style (no outline), matching the main Settings screens.
- Drop the redundant intro sentence at the top of the screen.
- Rework the "Add a server" block: recommended servers come first as
  one-tap chips, followed by the "or paste a server address" field.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GJ77Hm5L7fXEbPWbUds1iA
This commit is contained in:
Claude
2026-07-17 23:21:02 +00:00
parent 4d199ab28a
commit a99fc59de8
4 changed files with 52 additions and 52 deletions
@@ -45,6 +45,7 @@ import androidx.compose.material3.MaterialTheme
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.ui.Alignment
import androidx.compose.ui.Modifier
@@ -101,24 +102,21 @@ fun AllMediaBody(
itemCount = { blossomServersState.size },
)
// Auto-save the reordering once the drag finishes, rather than on every intermediate swap.
LaunchedEffect(dragState.isDragging) {
if (!dragState.isDragging) blossomServersViewModel.persistPending()
}
LazyColumn(
modifier = modifier,
contentPadding = FeedPadding,
userScrollEnabled = !dragState.isDragging,
) {
item {
Text(
text = stringRes(id = R.string.set_preferred_media_servers),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.grayText,
modifier = Modifier.padding(top = 12.dp, bottom = 4.dp),
)
}
item {
SectionLabel(
title = stringRes(id = R.string.media_servers_priority_section),
caption = stringRes(id = R.string.media_servers_reorder_hint),
modifier = Modifier.padding(top = 8.dp),
)
}
@@ -289,10 +287,9 @@ private fun AddServerSection(
) {
SectionLabel(title = stringRes(id = R.string.media_servers_add_section))
MediaServerEditField(R.string.add_a_blossom_server) { onAddServer(it) }
// Recommended servers first — one tap adds a known-good host.
Row(
modifier = Modifier.fillMaxWidth().padding(top = 14.dp),
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
@@ -323,6 +320,15 @@ private fun AddServerSection(
)
}
}
// ...or paste any server address.
Text(
text = stringRes(id = R.string.media_servers_add_url_label),
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.grayText,
modifier = Modifier.padding(top = 16.dp, bottom = 8.dp),
)
MediaServerEditField(R.string.add_a_blossom_server) { onAddServer(it) }
}
/** A recommended server as a tappable pill. Once added it reads as done and stops responding. */
@@ -21,7 +21,6 @@
package com.vitorpamplona.amethyst.ui.actions.mediaServers
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -53,7 +52,7 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.allGoodColor
@@ -72,9 +71,7 @@ fun AllMediaServersScreen(
blossomServersViewModel.load()
}
MediaServersScaffold(blossomServersViewModel, accountViewModel) {
nav.popBack()
}
MediaServersScaffold(blossomServersViewModel, accountViewModel, nav)
}
@OptIn(ExperimentalMaterial3Api::class)
@@ -82,20 +79,13 @@ fun AllMediaServersScreen(
fun MediaServersScaffold(
blossomServersViewModel: BlossomServersViewModel,
accountViewModel: AccountViewModel,
onClose: () -> Unit,
nav: INav,
) {
Scaffold(
topBar = {
SavingTopBar(
titleRes = R.string.media_servers,
onCancel = {
blossomServersViewModel.refresh()
onClose()
},
onPost = {
blossomServersViewModel.saveFileServers()
onClose()
},
TopBarWithBackButton(
caption = stringRes(id = R.string.media_servers),
nav = nav,
)
},
) { padding ->
@@ -132,8 +122,8 @@ fun MediaCacheSection(accountViewModel: AccountViewModel) {
modifier =
Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.border(1.dp, MaterialTheme.colorScheme.outlineVariant, RoundedCornerShape(16.dp)),
.clip(RoundedCornerShape(20.dp))
.background(MaterialTheme.colorScheme.surfaceContainer),
) {
Row(
modifier = Modifier.fillMaxWidth().padding(14.dp),
@@ -123,12 +123,17 @@ class BlossomServersViewModel : ViewModel() {
}
fun addServerList(serverList: List<String>) {
serverList.forEach { serverUrl ->
addServer(serverUrl)
}
var added = false
serverList.forEach { if (addServerInternal(it)) added = true }
if (added) persist()
}
fun addServer(serverUrl: String) {
if (addServerInternal(serverUrl)) persist()
}
/** Adds a server to the in-memory list (no persist). Returns true if it was new. */
private fun addServerInternal(serverUrl: String): Boolean {
val normalizedUrl =
try {
Rfc3986.normalize(serverUrl.trim())
@@ -147,15 +152,12 @@ class BlossomServersViewModel : ViewModel() {
normalizedUrl,
ServerType.Blossom,
)
if (_fileServers.value.contains(serverRef)) {
return
} else {
_fileServers.update {
it.plus(serverRef)
}
probeServer(serverRef.baseUrl)
}
if (_fileServers.value.contains(serverRef)) return false
_fileServers.update { it.plus(serverRef) }
probeServer(serverRef.baseUrl)
isModified = true
return true
}
fun removeServer(
@@ -176,21 +178,22 @@ class BlossomServersViewModel : ViewModel() {
}
pruneHealth()
isModified = true
persist()
}
}
fun removeAllServers() {
_fileServers.update { emptyList() }
isModified = true
}
/**
* Publishes any pending change. Called after each discrete edit (add/remove) and,
* for a reorder, once the drag gesture completes — so a drag doesn't publish a
* kind-10063 event on every intermediate swap.
*/
fun persistPending() = persist()
fun saveFileServers() {
if (isModified) {
accountViewModel.launchSigner {
val serverList = _fileServers.value.map { it.baseUrl }
account.sendBlossomServersList(serverList)
refresh()
}
private fun persist() {
if (!isModified) return
isModified = false
accountViewModel.launchSigner {
account.sendBlossomServersList(_fileServers.value.map { it.baseUrl })
}
}
+1
View File
@@ -1557,6 +1557,7 @@
<string name="media_servers_priority_section">Upload priority</string>
<string name="media_servers_add_section">Add a server</string>
<string name="media_servers_recommended_label">Recommended</string>
<string name="media_servers_add_url_label">Or paste a server address</string>
<string name="media_servers_cache_section">On-device cache</string>
<string name="media_server_primary_badge">Primary</string>
<string name="media_server_added">Added</string>