From a99fc59de82e42aca68bd83c4aef3cd8fa26f54c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 17 Jul 2026 23:21:02 +0000 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01GJ77Hm5L7fXEbPWbUds1iA --- .../mediaServers/AllMediaServersLIstView.kt | 30 +++++++----- .../mediaServers/AllMediaServersScreen.kt | 26 ++++------ .../mediaServers/BlossomServersViewModel.kt | 47 ++++++++++--------- amethyst/src/main/res/values/strings.xml | 1 + 4 files changed, 52 insertions(+), 52 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/AllMediaServersLIstView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/AllMediaServersLIstView.kt index f6d120ebf7..a37ccaf0b0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/AllMediaServersLIstView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/AllMediaServersLIstView.kt @@ -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. */ diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/AllMediaServersScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/AllMediaServersScreen.kt index 760935d1db..cefa1fe242 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/AllMediaServersScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/AllMediaServersScreen.kt @@ -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), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/BlossomServersViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/BlossomServersViewModel.kt index a5e5607a40..c22154e5c5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/BlossomServersViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/BlossomServersViewModel.kt @@ -123,12 +123,17 @@ class BlossomServersViewModel : ViewModel() { } fun addServerList(serverList: List) { - 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 }) } } diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index dcc3c4a25e..ba81aba405 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1557,6 +1557,7 @@ Upload priority Add a server Recommended + Or paste a server address On-device cache Primary Added