mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-11 16:57:39 +00:00
feat(blossom): app-level "Sync all" with floating progress; rename to "My Blossom Data"
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ckbnz1N94W1hnNC9xpsCNP
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
+137
@@ -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<String>,
|
||||
)
|
||||
|
||||
private val _state = MutableStateFlow<BlossomSyncState?>(null)
|
||||
val state: StateFlow<BlossomSyncState?> = _state.asStateFlow()
|
||||
|
||||
private val _results = MutableSharedFlow<BlossomMirrorResult>(extraBufferCapacity = 128)
|
||||
val results: SharedFlow<BlossomMirrorResult> = _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<Task>,
|
||||
) {
|
||||
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
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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),
|
||||
)
|
||||
|
||||
+26
-1
@@ -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(
|
||||
|
||||
+38
@@ -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<PendingMirrorPayment?>(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,
|
||||
|
||||
+130
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+10
@@ -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<NavBarItem, NavBarItemDef> =
|
||||
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> =
|
||||
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<NavBarCategory> =
|
||||
NavBarItem.SCHEDULED_POSTS,
|
||||
NavBarItem.INTEREST_SETS,
|
||||
NavBarItem.FAVORITE_ALGO_FEEDS,
|
||||
NavBarItem.BLOSSOM_DATA,
|
||||
NavBarItem.EMOJI_PACKS,
|
||||
NavBarItem.WALLET,
|
||||
NavBarItem.NOSTR_SIGNER,
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
-1
@@ -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()),
|
||||
|
||||
@@ -1551,7 +1551,12 @@
|
||||
<string name="blossom_optimize_media">Optimize media on the server</string>
|
||||
<string name="blossom_optimize_media_caption">Upload through the server\'s /media endpoint so it can strip metadata and compress the file. The stored file may differ from the original.</string>
|
||||
<string name="manage_stored_files">Manage stored files</string>
|
||||
<string name="my_blossom_data">My Blossom Data</string>
|
||||
<string name="blossom_refresh">Refresh</string>
|
||||
<string name="blossom_sync_all">Sync all</string>
|
||||
<string name="blossom_sync_gaps">Some of your files aren\'t on all your servers yet.</string>
|
||||
<string name="blossom_syncing">Copying your files across servers…</string>
|
||||
<string name="blossom_sync_done">Sync complete</string>
|
||||
<string name="manage_stored_files_empty">No stored files found on your Blossom servers.</string>
|
||||
<string name="blossom_mirror_to_missing">Mirror to missing</string>
|
||||
<string name="blossom_delete_from">Delete from…</string>
|
||||
|
||||
Reference in New Issue
Block a user