From ecbe2c67047a8a1ec4707d6d6d08597efb4a43c8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 03:57:58 +0000 Subject: [PATCH] feat(blossom): app-level "Sync all" with floating progress; rename to "My Blossom Data" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sync-all now runs like the PoW miner — an app-level job with a floating progress banner — instead of a screen-bound loop, so it keeps going as the user navigates: - BlossomMirrorQueue (app-level, on applicationIOScope via Amethyst.instance): runs the BUD-04 fan-out, exposes an aggregate BlossomSyncState for the banner and a results SharedFlow so an open manager flips each pill green/grey live. Servers that need payment are skipped (pay those per-row). - DisplayBlossomSyncProgress: a bottom floating banner mounted at the navigation root (sibling of the mining/broadcast banners) with a determinate bar, "x / N · host", failed count, and cancel/dismiss. - Manager: a "Sync all" banner appears when any file has gaps; tapping it enqueues the sweep and optimistically spins the affected pills. Also, per request: the screen is renamed "My Blossom Data" and moved into the left drawer's "You" section (just before My Emoji Packs); the standalone Settings-catalog entry is removed and the Media Servers shortcut relabeled. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Ckbnz1N94W1hnNC9xpsCNP --- .../com/vitorpamplona/amethyst/AppModules.kt | 4 + .../uploads/blossom/BlossomMirrorQueue.kt | 137 ++++++++++++++++++ .../mediaServers/AllMediaServersLIstView.kt | 2 +- .../mediaServers/BlossomBlobManagerScreen.kt | 27 +++- .../BlossomBlobManagerViewModel.kt | 38 +++++ .../DisplayBlossomSyncProgress.kt | 130 +++++++++++++++++ .../amethyst/ui/navigation/AppNavigation.kt | 2 + .../ui/navigation/bottombars/NavBarItem.kt | 10 ++ .../loggedIn/BottomBarFeedPreloaders.kt | 1 + .../settings/SettingsCatalogBuilder.kt | 1 - amethyst/src/main/res/values/strings.xml | 5 + 11 files changed, 354 insertions(+), 3 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomMirrorQueue.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/DisplayBlossomSyncProgress.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt index 72d8c52e0f..8a4f709931 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt @@ -107,6 +107,7 @@ import com.vitorpamplona.amethyst.service.resourceusage.UsageKeys import com.vitorpamplona.amethyst.service.safeCacheDir import com.vitorpamplona.amethyst.service.scheduledposts.ScheduledPostWorkGate import com.vitorpamplona.amethyst.service.scheduledposts.ScheduledPostWorker +import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomMirrorQueue import com.vitorpamplona.amethyst.service.uploads.blossom.bud10.BlossomServerResolver import com.vitorpamplona.amethyst.service.uploads.blossom.bud10.LocalBlossomCacheProbe import com.vitorpamplona.amethyst.service.uploads.nip95.Nip95CacheFactory @@ -795,6 +796,9 @@ class AppModules( } } + /** App-level BUD-04 mirror sweep, so "sync all" keeps running as the user navigates. */ + val blossomMirrorQueue by lazy { BlossomMirrorQueue(applicationIOScope) } + val powJobRestorer by lazy { PowJobRestorer(powPublishQueue, powJobStore, scheduledPostStore) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomMirrorQueue.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomMirrorQueue.kt new file mode 100644 index 0000000000..fb637725f0 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomMirrorQueue.kt @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.uploads.blossom + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.service.upload.BlossomClient +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nipB7Blossom.BlossomServerUrl +import com.vitorpamplona.quartz.utils.Log +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import kotlin.coroutines.cancellation.CancellationException + +/** Aggregate progress of a running "sync all my blobs to all my servers" sweep. */ +@Immutable +data class BlossomSyncState( + val total: Int, + val done: Int, + val failed: Int, + val running: Boolean, + val currentHost: String? = null, +) { + val succeeded get() = done - failed + val fraction get() = if (total == 0) 0f else done.toFloat() / total +} + +/** One completed mirror step, streamed so an open manager screen can flip its pill live. */ +@Immutable +data class BlossomMirrorResult( + val hash: HexKey, + val server: String, + val ok: Boolean, +) + +/** + * App-level BUD-04 mirror queue, modeled on the PoW publish queue: it runs on the + * application IO scope (via [Amethyst.instance]) so a sweep keeps going while the + * user navigates the app, and exposes [state] for a floating progress banner mounted + * at the navigation root. Servers that require payment are skipped (counted as + * failed) — those are paid for individually from the manager screen. + */ +class BlossomMirrorQueue( + private val scope: CoroutineScope, +) { + data class Task( + val hash: HexKey, + val sourceUrl: String, + val size: Long?, + val targets: List, + ) + + private val _state = MutableStateFlow(null) + val state: StateFlow = _state.asStateFlow() + + private val _results = MutableSharedFlow(extraBufferCapacity = 128) + val results: SharedFlow = _results.asSharedFlow() + + private var job: Job? = null + + val isRunning get() = _state.value?.running == true + + /** Enqueue a sweep. No-op if one is already running or there's nothing to do. */ + fun start( + account: Account, + tasks: List, + ) { + if (isRunning) return + val work = tasks.flatMap { t -> t.targets.map { t to it } } + if (work.isEmpty()) return + + job = + scope.launch { + var done = 0 + var failed = 0 + _state.value = BlossomSyncState(total = work.size, done = 0, failed = 0, running = true) + for ((task, target) in work) { + _state.value = _state.value?.copy(currentHost = BlossomServerUrl.domain(target)) + val ok = + try { + val auth = account.createBlossomUploadAuth(task.hash, task.size ?: 0L, "Mirror ${task.hash}").toAuthorizationHeader() + BlossomClient(Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForUploads(target)) + .mirror(task.sourceUrl, target, auth) + true + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Log.w("BlossomMirrorQueue", "mirror ${task.hash} -> $target failed", e) + false + } + _results.tryEmit(BlossomMirrorResult(task.hash, target, ok)) + done++ + if (!ok) failed++ + _state.value = _state.value?.copy(done = done, failed = failed) + } + _state.value = _state.value?.copy(running = false, currentHost = null) + } + } + + /** Cancel a running sweep and clear the banner. */ + fun cancel() { + job?.cancel() + job = null + _state.value = null + } + + /** Dismiss the finished-summary banner (no effect while still running). */ + fun dismiss() { + if (!isRunning) _state.value = null + } +} 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 0e01b2c88d..22e036e31e 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 @@ -231,7 +231,7 @@ private fun UploadBehaviorSection( verticalAlignment = Alignment.CenterVertically, ) { Text( - text = stringRes(id = R.string.manage_stored_files), + text = stringRes(id = R.string.my_blossom_data), style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/BlossomBlobManagerScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/BlossomBlobManagerScreen.kt index fd9f74fd9d..d474c5e45f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/BlossomBlobManagerScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/BlossomBlobManagerScreen.kt @@ -114,7 +114,7 @@ fun BlossomBlobManagerScreen( Scaffold( topBar = { TopBarExtensibleWithBackButton( - title = { Text(stringRes(R.string.manage_stored_files)) }, + title = { Text(stringRes(R.string.my_blossom_data)) }, showBackButton = nav.canPop(), popBack = { nav.popBack() }, actions = { @@ -166,6 +166,9 @@ fun BlossomBlobManagerScreen( contentPadding = PaddingValues(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp), ) { + if (blobs.any { it.hasMissing }) { + item { SyncAllBanner(onSyncAll = { vm.syncAll() }) } + } items(blobs, key = { it.hash }) { row -> BlobCard(row, vm) } @@ -201,6 +204,28 @@ private fun StatusGlyph( } } +@Composable +private fun SyncAllBanner(onSyncAll: () -> Unit) { + Row( + modifier = + Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(20.dp)) + .background(MaterialTheme.colorScheme.surfaceContainerHighest) + .padding(14.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) { + Text(stringRes(R.string.blossom_sync_gaps), style = MaterialTheme.typography.bodyMedium) + } + FilledTonalButton(onClick = onSyncAll) { + Icon(symbol = MaterialSymbols.CloudUpload, contentDescription = null, modifier = Modifier.size(18.dp)) + Spacer(Modifier.size(8.dp)) + Text(stringRes(R.string.blossom_sync_all)) + } + } +} + @OptIn(ExperimentalLayoutApi::class) @Composable private fun BlobCard( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/BlossomBlobManagerViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/BlossomBlobManagerViewModel.kt index b276c082c8..1b3c9a9915 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/BlossomBlobManagerViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/BlossomBlobManagerViewModel.kt @@ -28,6 +28,7 @@ import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.service.upload.BlossomClient import com.vitorpamplona.amethyst.commons.service.upload.BlossomPaymentException import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomMirrorQueue import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomPaymentHandler import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.HexKey @@ -121,8 +122,20 @@ class BlossomBlobManagerViewModel : ViewModel() { private val _pendingPayment = MutableStateFlow(null) val pendingPayment = _pendingPayment.asStateFlow() + private var resultCollectorStarted = false + fun init(accountViewModel: AccountViewModel) { this.account = accountViewModel.account + // Reflect the app-level sync sweep's per-server results onto the pills, so an + // open manager turns dots green live even though the work runs in the background. + if (!resultCollectorStarted) { + resultCollectorStarted = true + viewModelScope.launch { + Amethyst.instance.blossomMirrorQueue.results.collect { r -> + setServerState(r.hash, r.server, if (r.ok) PresenceState.PRESENT else PresenceState.MISSING) + } + } + } } private fun clientFor(server: String) = BlossomClient(Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForUploads(server)) @@ -295,6 +308,31 @@ class BlossomBlobManagerViewModel : ViewModel() { } } + /** + * BUD-04 sweep: hand the whole "fill every gap" job to the app-level + * [BlossomMirrorQueue] so it keeps running (with a floating progress banner) as + * the user navigates away. The pills we're about to fill go straight to a spinner; + * the queue's [results] stream (collected in [init]) flips each to green/grey as it + * lands, so an open manager stays in sync with the background sweep. + */ + fun syncAll() { + val tasks = + _blobs.value + .filter { it.hasMissing && it.url != null } + .map { BlossomMirrorQueue.Task(it.hash, it.url!!, it.size, it.missingServers) } + if (tasks.isEmpty()) return + + _blobs.value = + _blobs.value.map { row -> + if (!row.hasMissing) { + row + } else { + row.copy(servers = row.servers.map { if (it.state == PresenceState.MISSING) it.copy(state = PresenceState.PENDING) else it }) + } + } + Amethyst.instance.blossomMirrorQueue.start(account, tasks) + } + private suspend fun mirrorOne( source: String, hash: HexKey, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/DisplayBlossomSyncProgress.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/DisplayBlossomSyncProgress.kt new file mode 100644 index 0000000000..f2e1e65682 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/DisplayBlossomSyncProgress.kt @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.actions.mediaServers + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.Amethyst +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.stringRes +import com.vitorpamplona.amethyst.ui.theme.allGoodColor +import com.vitorpamplona.amethyst.ui.theme.grayText + +/** + * App-wide floating banner for the BUD-04 "sync all" sweep, mounted at the navigation + * root (a sibling of the mining/broadcast banners) so it floats over every screen and + * survives navigation. Observes the app-level [Amethyst.instance.blossomMirrorQueue]. + */ +@Composable +fun DisplayBlossomSyncProgress() { + val queue = Amethyst.instance.blossomMirrorQueue + val state by queue.state.collectAsStateWithLifecycle() + + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.BottomCenter) { + AnimatedVisibility( + visible = state != null, + enter = slideInVertically { it } + fadeIn(), + exit = slideOutVertically { it } + fadeOut(), + modifier = + Modifier + .navigationBarsPadding() + .padding(start = 12.dp, end = 12.dp, bottom = 116.dp) + .widthIn(max = 560.dp), + ) { + val s = state ?: return@AnimatedVisibility + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.surfaceContainer, + tonalElevation = 3.dp, + shadowElevation = 6.dp, + modifier = Modifier.fillMaxWidth(), + ) { + Column(modifier = Modifier.padding(14.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + symbol = if (s.running) MaterialSymbols.CloudUpload else MaterialSymbols.Check, + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = if (s.running) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.allGoodColor, + ) + Text( + text = if (s.running) stringRes(R.string.blossom_syncing) else stringRes(R.string.blossom_sync_done), + style = MaterialTheme.typography.titleSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f).padding(horizontal = 10.dp), + ) + IconButton( + onClick = { if (s.running) queue.cancel() else queue.dismiss() }, + modifier = Modifier.size(28.dp), + ) { + Icon(symbol = MaterialSymbols.Close, contentDescription = null, modifier = Modifier.size(18.dp)) + } + } + + LinearProgressIndicator( + progress = { s.fraction }, + modifier = Modifier.fillMaxWidth().padding(top = 10.dp), + ) + + Text( + text = + buildString { + append("${s.done} / ${s.total}") + if (s.running && s.currentHost != null) append(" · ${s.currentHost}") + if (s.failed > 0) append(" · ${s.failed} failed") + }, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.grayText, + modifier = Modifier.padding(top = 6.dp), + ) + } + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 6165aaaef2..53e8a2c3bc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -57,6 +57,7 @@ import com.vitorpamplona.amethyst.service.resourceusage.ScreenTimeIntegrator import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataScreen import com.vitorpamplona.amethyst.ui.actions.mediaServers.AllMediaServersScreen import com.vitorpamplona.amethyst.ui.actions.mediaServers.BlossomBlobManagerScreen +import com.vitorpamplona.amethyst.ui.actions.mediaServers.DisplayBlossomSyncProgress import com.vitorpamplona.amethyst.ui.actions.paymentTargets.PaymentTargetsScreen import com.vitorpamplona.amethyst.ui.broadcast.DisplayBroadcastProgress import com.vitorpamplona.amethyst.ui.call.CallActivity @@ -351,6 +352,7 @@ fun AppNavigation( DisplayCrashMessages(accountViewModel, nav) DisplayResourceUsageAlert(accountViewModel, nav) DisplayBroadcastProgress(accountViewModel) + DisplayBlossomSyncProgress() ObserveIncomingCalls(accountViewModel) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/NavBarItem.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/NavBarItem.kt index c3a53e7919..165b506fdd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/NavBarItem.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/NavBarItem.kt @@ -46,6 +46,7 @@ enum class NavBarItem { DRAFTS, SCHEDULED_POSTS, INTEREST_SETS, + BLOSSOM_DATA, EMOJI_PACKS, WALLET, NOSTR_SIGNER, @@ -182,6 +183,13 @@ val NavBarCatalog: Map = icon = MaterialSymbols.AutoAwesome, resolveRoute = { Route.EditFavoriteAlgoFeeds }, ), + NavBarItem.BLOSSOM_DATA to + NavBarItemDef( + id = NavBarItem.BLOSSOM_DATA, + labelRes = R.string.my_blossom_data, + icon = MaterialSymbols.Storage, + resolveRoute = { Route.ManageBlossomBlobs }, + ), NavBarItem.EMOJI_PACKS to NavBarItemDef( id = NavBarItem.EMOJI_PACKS, @@ -449,6 +457,7 @@ val DrawerYouItems: List = NavBarItem.DRAFTS, NavBarItem.SCHEDULED_POSTS, NavBarItem.INTEREST_SETS, + NavBarItem.BLOSSOM_DATA, NavBarItem.EMOJI_PACKS, NavBarItem.WALLET, NavBarItem.NOSTR_SIGNER, @@ -502,6 +511,7 @@ val BottomBarCategories: List = NavBarItem.SCHEDULED_POSTS, NavBarItem.INTEREST_SETS, NavBarItem.FAVORITE_ALGO_FEEDS, + NavBarItem.BLOSSOM_DATA, NavBarItem.EMOJI_PACKS, NavBarItem.WALLET, NavBarItem.NOSTR_SIGNER, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BottomBarFeedPreloaders.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BottomBarFeedPreloaders.kt index 41b489db8e..ddce6cde6e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BottomBarFeedPreloaders.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BottomBarFeedPreloaders.kt @@ -168,6 +168,7 @@ private fun PreloadFor( NavBarItem.DRAFTS, NavBarItem.SCHEDULED_POSTS, NavBarItem.INTEREST_SETS, + NavBarItem.BLOSSOM_DATA, NavBarItem.EMOJI_PACKS, NavBarItem.WALLET, NavBarItem.NOSTR_SIGNER, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsCatalogBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsCatalogBuilder.kt index a2fdee8592..5e85e88c94 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsCatalogBuilder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsCatalogBuilder.kt @@ -66,7 +66,6 @@ fun buildSettingsCatalog( symEntry(R.string.event_sync_title, MaterialSymbols.Sync, R.string.event_sync_search_keywords, Route.EventSync), symEntry(R.string.route_import_follows, MaterialSymbols.GroupAdd, R.string.import_follows_search_keywords, Route.ImportFollowsSelectUser), symEntry(R.string.media_servers, MaterialSymbols.CloudUpload, R.string.media_servers_search_keywords, Route.EditMediaServers), - symEntry(R.string.manage_stored_files, MaterialSymbols.Storage, R.string.media_servers_search_keywords, Route.ManageBlossomBlobs), symEntry(R.string.nests_servers_title, MaterialSymbols.CloudUpload, R.string.nests_servers_search_keywords, Route.EditNestsServers), symEntry(R.string.reactions, MaterialSymbols.FavoriteBorder, R.string.reactions_search_keywords, Route.UpdateReactionType), symEntry(R.string.zaps, MaterialSymbols.Bolt, R.string.zaps_search_keywords, Route.UpdateZapAmount()), diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 3b014b22fc..8f194d9370 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1551,7 +1551,12 @@ Optimize media on the server Upload through the server\'s /media endpoint so it can strip metadata and compress the file. The stored file may differ from the original. Manage stored files + My Blossom Data Refresh + Sync all + Some of your files aren\'t on all your servers yet. + Copying your files across servers… + Sync complete No stored files found on your Blossom servers. Mirror to missing Delete from…