mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-09 16:14:40 +00:00
feat(blossom): redesign blob manager to match the app's modern UI
Brings the "Manage stored files" screen up to the design language main just applied to the Media Servers screen: - Rounded surfaceContainer cards instead of flat Cards. - Image thumbnail, or a file-type glyph in a rounded secondaryContainer box for non-images. - Monospace, truncated-both-ends hash; type · size subtitle. - Per-server presence as pills with a status dot — allGoodColor tint for present, muted for missing — replacing the ambiguous same-shaped chips. - A single MoreVert overflow (copy link / open / report / per-server delete) with leading icons, plus a full-width tonal "Mirror to missing" CTA. - Icon-in-rounded-box empty/error states and an ExtendedFloatingActionButton refresh. - BUD-07 pay dialog restored with a Bolt icon and tonal confirm; payment message now names the server. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ckbnz1N94W1hnNC9xpsCNP
This commit is contained in:
+228
-143
@@ -21,27 +21,32 @@
|
||||
package com.vitorpamplona.amethyst.ui.actions.mediaServers
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.AssistChip
|
||||
import androidx.compose.material3.AssistChipDefaults
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
@@ -57,11 +62,13 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
@@ -70,11 +77,13 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import coil3.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
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
|
||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportType
|
||||
|
||||
@@ -95,6 +104,7 @@ fun BlossomBlobManagerScreen(
|
||||
|
||||
pendingPayment?.let { pending ->
|
||||
BlossomPaymentDialog(
|
||||
host = vm.hostOf(pending.target),
|
||||
amountSats = pending.amountSats,
|
||||
reason = pending.payment.reason,
|
||||
onConfirm = { vm.confirmPendingPayment() },
|
||||
@@ -105,11 +115,12 @@ fun BlossomBlobManagerScreen(
|
||||
Scaffold(
|
||||
topBar = { TopBarWithBackButton(stringRes(R.string.manage_stored_files), nav) },
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(onClick = { vm.refresh() }) {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.Sync,
|
||||
contentDescription = stringRes(R.string.retry),
|
||||
modifier = Modifier.size(22.dp),
|
||||
if (blobs.isNotEmpty()) {
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = { vm.refresh() },
|
||||
icon = { Icon(symbol = MaterialSymbols.Sync, contentDescription = null, modifier = Modifier.size(20.dp)) },
|
||||
text = { Text(stringRes(R.string.retry)) },
|
||||
expanded = !loading,
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -119,179 +130,254 @@ fun BlossomBlobManagerScreen(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(
|
||||
start = 12.dp,
|
||||
end = 12.dp,
|
||||
top = padding.calculateTopPadding(),
|
||||
bottom = padding.calculateBottomPadding(),
|
||||
),
|
||||
) {
|
||||
when {
|
||||
loading && blobs.isEmpty() ->
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) { CircularProgressIndicator() }
|
||||
loading && blobs.isEmpty() -> CenteredState { CircularProgressIndicator() }
|
||||
|
||||
error != null && blobs.isEmpty() ->
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(error ?: "", color = MaterialTheme.colorScheme.error)
|
||||
TextButton(onClick = { vm.refresh() }) { Text(stringRes(R.string.retry)) }
|
||||
CenteredState {
|
||||
StatusGlyph(MaterialSymbols.Warning, MaterialTheme.colorScheme.error)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Text(error ?: "", color = MaterialTheme.colorScheme.grayText)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
OutlinedButton(onClick = { vm.refresh() }) { Text(stringRes(R.string.retry)) }
|
||||
}
|
||||
|
||||
blobs.isEmpty() ->
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) { Text(stringRes(R.string.manage_stored_files_empty), color = MaterialTheme.colorScheme.grayText) }
|
||||
CenteredState {
|
||||
StatusGlyph(MaterialSymbols.Storage, MaterialTheme.colorScheme.grayText)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Text(
|
||||
stringRes(R.string.manage_stored_files_empty),
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
)
|
||||
}
|
||||
|
||||
else ->
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
items(blobs, key = { it.hash }) { row ->
|
||||
BlobCard(row, vm)
|
||||
}
|
||||
item { Spacer(Modifier.height(72.dp)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CenteredState(content: @Composable () -> Unit) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().padding(32.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) { content() }
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatusGlyph(
|
||||
symbol: MaterialSymbol,
|
||||
tint: Color,
|
||||
) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.size(72.dp)
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerHighest),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(symbol = symbol, contentDescription = null, modifier = Modifier.size(34.dp), tint = tint)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun BlobCard(
|
||||
row: BlobRow,
|
||||
vm: BlossomBlobManagerViewModel,
|
||||
) {
|
||||
var deleteMenuOpen by remember { mutableStateOf(false) }
|
||||
var menuOpen by remember { mutableStateOf(false) }
|
||||
var reportOpen by remember { mutableStateOf(false) }
|
||||
val clipboard = LocalClipboardManager.current
|
||||
val context = LocalContext.current
|
||||
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().padding(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
if (row.url != null && row.type?.startsWith("image/") == true) {
|
||||
AsyncImage(
|
||||
model = row.url,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.size(44.dp).clip(RoundedCornerShape(8.dp)),
|
||||
)
|
||||
}
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = row.hash.take(16) + "…",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
Text(
|
||||
text = listOfNotNull(row.type, row.size?.let { humanBytes(it) }).joinToString(" · "),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||
.padding(14.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
// Header: thumbnail / file glyph + hash + overflow menu.
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
BlobThumbnail(row)
|
||||
Column(modifier = Modifier.weight(1f).padding(horizontal = 12.dp)) {
|
||||
Text(
|
||||
text = row.hash.take(12) + "…" + row.hash.takeLast(6),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
Text(
|
||||
text = listOfNotNull(row.type, row.size?.let { humanBytes(it) }).joinToString(" · "),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
)
|
||||
}
|
||||
|
||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
row.serversPresent.forEach { server ->
|
||||
AssistChip(
|
||||
onClick = {},
|
||||
label = { Text(vm.hostOf(server), style = MaterialTheme.typography.labelSmall) },
|
||||
colors =
|
||||
AssistChipDefaults.assistChipColors(
|
||||
labelColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
)
|
||||
Box {
|
||||
IconButton(onClick = { menuOpen = true }) {
|
||||
Icon(symbol = MaterialSymbols.MoreVert, contentDescription = null)
|
||||
}
|
||||
row.serversMissing.forEach { server ->
|
||||
AssistChip(
|
||||
onClick = {},
|
||||
label = { Text(vm.hostOf(server), style = MaterialTheme.typography.labelSmall) },
|
||||
colors =
|
||||
AssistChipDefaults.assistChipColors(
|
||||
labelColor = MaterialTheme.colorScheme.grayText,
|
||||
),
|
||||
)
|
||||
DropdownMenu(expanded = menuOpen, onDismissRequest = { menuOpen = false }) {
|
||||
if (row.url != null) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.copy)) },
|
||||
leadingIcon = { MenuIcon(MaterialSymbols.ContentCopy) },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
clipboard.setText(AnnotatedString(row.url))
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.blossom_open)) },
|
||||
leadingIcon = { MenuIcon(MaterialSymbols.AutoMirrored.OpenInNew) },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
runCatching { context.startActivity(Intent(Intent.ACTION_VIEW, row.url.toUri())) }
|
||||
},
|
||||
)
|
||||
}
|
||||
if (row.serversPresent.isNotEmpty()) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.blossom_report)) },
|
||||
leadingIcon = { MenuIcon(MaterialSymbols.Report) },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
reportOpen = true
|
||||
},
|
||||
)
|
||||
HorizontalDivider()
|
||||
row.serversPresent.forEach { server ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.blossom_delete_from_host, vm.hostOf(server))) },
|
||||
leadingIcon = { MenuIcon(MaterialSymbols.Delete, MaterialTheme.colorScheme.error) },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
vm.delete(row.hash, server)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
// Per-server presence pills.
|
||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
row.serversPresent.forEach { ServerPill(vm.hostOf(it), present = true) }
|
||||
row.serversMissing.forEach { ServerPill(vm.hostOf(it), present = false) }
|
||||
}
|
||||
|
||||
// Primary CTA: fill the gaps.
|
||||
if (row.serversMissing.isNotEmpty() && row.url != null) {
|
||||
FilledTonalButton(
|
||||
onClick = { vm.mirrorToMissing(row) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
if (row.serversMissing.isNotEmpty() && row.url != null) {
|
||||
OutlinedButton(onClick = { vm.mirrorToMissing(row) }) {
|
||||
Text(stringRes(R.string.blossom_mirror_to_missing), style = MaterialTheme.typography.labelMedium)
|
||||
}
|
||||
}
|
||||
|
||||
if (row.serversPresent.isNotEmpty()) {
|
||||
Column {
|
||||
OutlinedButton(onClick = { deleteMenuOpen = true }) {
|
||||
Text(stringRes(R.string.blossom_delete_from), style = MaterialTheme.typography.labelMedium)
|
||||
}
|
||||
DropdownMenu(expanded = deleteMenuOpen, onDismissRequest = { deleteMenuOpen = false }) {
|
||||
row.serversPresent.forEach { server ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(vm.hostOf(server)) },
|
||||
onClick = {
|
||||
deleteMenuOpen = false
|
||||
vm.delete(row.hash, server)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (row.serversPresent.isNotEmpty()) {
|
||||
TextButton(onClick = { reportOpen = true }) {
|
||||
Text(stringRes(R.string.blossom_report), style = MaterialTheme.typography.labelMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (row.url != null) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
TextButton(onClick = { clipboard.setText(AnnotatedString(row.url)) }) {
|
||||
Text(stringRes(R.string.copy), style = MaterialTheme.typography.labelMedium)
|
||||
}
|
||||
TextButton(onClick = {
|
||||
runCatching { context.startActivity(Intent(Intent.ACTION_VIEW, row.url.toUri())) }
|
||||
}) {
|
||||
Text(stringRes(R.string.blossom_open), style = MaterialTheme.typography.labelMedium)
|
||||
}
|
||||
}
|
||||
Icon(symbol = MaterialSymbols.CloudUpload, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.size(8.dp))
|
||||
Text(stringRes(R.string.blossom_mirror_to_missing))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (reportOpen) {
|
||||
BlossomReportDialog(
|
||||
row = row,
|
||||
vm = vm,
|
||||
onDismiss = { reportOpen = false },
|
||||
BlossomReportDialog(row = row, vm = vm, onDismiss = { reportOpen = false })
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BlobThumbnail(row: BlobRow) {
|
||||
val shape = RoundedCornerShape(12.dp)
|
||||
if (row.url != null && row.type?.startsWith("image/") == true) {
|
||||
AsyncImage(
|
||||
model = row.url,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.size(48.dp).clip(shape),
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier.size(48.dp).clip(shape).background(MaterialTheme.colorScheme.secondaryContainer),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
val glyph = if (row.type?.startsWith("video/") == true) MaterialSymbols.Download else MaterialSymbols.Storage
|
||||
Icon(
|
||||
symbol = glyph,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(22.dp),
|
||||
tint = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ServerPill(
|
||||
host: String,
|
||||
present: Boolean,
|
||||
) {
|
||||
val dot = if (present) MaterialTheme.colorScheme.allGoodColor else MaterialTheme.colorScheme.grayText
|
||||
val bg =
|
||||
if (present) {
|
||||
MaterialTheme.colorScheme.allGoodColor.copy(alpha = 0.12f)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surfaceContainerHighest
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.clip(CircleShape).background(bg).padding(horizontal = 10.dp, vertical = 5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
Box(modifier = Modifier.size(7.dp).clip(CircleShape).background(dot))
|
||||
Text(
|
||||
text = host,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = if (present) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.grayText,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MenuIcon(
|
||||
symbol: MaterialSymbol,
|
||||
tint: Color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
) {
|
||||
Icon(symbol = symbol, contentDescription = null, modifier = Modifier.size(20.dp), tint = tint)
|
||||
}
|
||||
|
||||
private fun humanBytes(bytes: Long): String =
|
||||
when {
|
||||
bytes >= 1_000_000 -> "${bytes / 1_000_000} MB"
|
||||
bytes >= 1_000 -> "${bytes / 1_000} KB"
|
||||
else -> "$bytes B"
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BlossomPaymentDialog(
|
||||
host: String,
|
||||
amountSats: Long?,
|
||||
reason: String?,
|
||||
onConfirm: () -> Unit,
|
||||
@@ -299,18 +385,21 @@ private fun BlossomPaymentDialog(
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
icon = { Icon(symbol = MaterialSymbols.Bolt, contentDescription = null, tint = MaterialTheme.colorScheme.allGoodColor) },
|
||||
title = { Text(stringRes(R.string.blossom_payment_title)) },
|
||||
text = {
|
||||
Text(
|
||||
text =
|
||||
listOfNotNull(
|
||||
stringRes(R.string.blossom_payment_message),
|
||||
stringRes(R.string.blossom_payment_message, host),
|
||||
reason,
|
||||
).joinToString("\n\n"),
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
Button(onClick = onConfirm) {
|
||||
FilledTonalButton(onClick = onConfirm) {
|
||||
Icon(symbol = MaterialSymbols.Bolt, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.size(8.dp))
|
||||
Text(
|
||||
if (amountSats != null) {
|
||||
pluralStringResource(R.plurals.blossom_pay_sats, amountSats.toInt(), amountSats.toInt())
|
||||
@@ -326,13 +415,6 @@ private fun BlossomPaymentDialog(
|
||||
)
|
||||
}
|
||||
|
||||
private fun humanBytes(bytes: Long): String =
|
||||
when {
|
||||
bytes >= 1_000_000 -> "${bytes / 1_000_000} MB"
|
||||
bytes >= 1_000 -> "${bytes / 1_000} KB"
|
||||
else -> "$bytes B"
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BlossomReportDialog(
|
||||
row: BlobRow,
|
||||
@@ -347,11 +429,14 @@ private fun BlossomReportDialog(
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
icon = { Icon(symbol = MaterialSymbols.Report, contentDescription = null) },
|
||||
title = { Text(stringRes(R.string.blossom_report_title)) },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Column {
|
||||
OutlinedButton(onClick = { typeMenuOpen = true }) { Text(type.code) }
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Box {
|
||||
OutlinedButton(onClick = { typeMenuOpen = true }, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(type.code)
|
||||
}
|
||||
DropdownMenu(expanded = typeMenuOpen, onDismissRequest = { typeMenuOpen = false }) {
|
||||
ReportType.entries.forEach { rt ->
|
||||
DropdownMenuItem(
|
||||
@@ -373,7 +458,7 @@ private fun BlossomReportDialog(
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(onClick = {
|
||||
FilledTonalButton(onClick = {
|
||||
vm.report(row.hash, server, type, comment)
|
||||
onDismiss()
|
||||
}) { Text(stringRes(R.string.blossom_send)) }
|
||||
|
||||
@@ -1554,11 +1554,12 @@
|
||||
<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>
|
||||
<string name="blossom_delete_from_host">Delete from %1$s</string>
|
||||
<string name="blossom_report">Report</string>
|
||||
<string name="blossom_open">Open</string>
|
||||
<string name="blossom_payment_required">This server requires payment to upload: %1$s</string>
|
||||
<string name="blossom_payment_title">Payment required</string>
|
||||
<string name="blossom_payment_message">This server charges a lightning payment to store the file. Pay from your connected wallet to continue.</string>
|
||||
<string name="blossom_payment_message">%1$s charges a lightning payment to store this file. Pay from your connected wallet to continue.</string>
|
||||
<string name="blossom_pay">Pay</string>
|
||||
<plurals name="blossom_pay_sats">
|
||||
<item quantity="one">Pay %1$d sat</item>
|
||||
|
||||
Reference in New Issue
Block a user