From 8eeeae9601e599f009f5712822ef40cb762f174f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Apr 2026 21:20:38 +0000 Subject: [PATCH] refactor(badges/new): show form first with an upload placeholder The FAB now opens the new-badge dialog directly. The dialog renders a big bordered "Upload an image" placeholder where the picture will go; tapping it opens the gallery. Once the user picks an image, the placeholder is replaced by the existing ShowImageUploadGallery preview and tapping the preview lets them pick a different image. Lets the user see the whole form (name, description, server, quality, strip-metadata) immediately instead of being thrown into the picker the moment they hit the FAB. --- .../screen/loggedIn/badges/NewBadgeButton.kt | 30 +---- .../loggedIn/badges/post/NewBadgeDialog.kt | 120 +++++++++++++++--- .../loggedIn/badges/post/NewBadgeModel.kt | 13 ++ amethyst/src/main/res/values/strings.xml | 2 + 4 files changed, 122 insertions(+), 43 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/NewBadgeButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/NewBadgeButton.kt index 55d365f7ee..209be6fe91 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/NewBadgeButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/NewBadgeButton.kt @@ -21,8 +21,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.badges import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.AddPhotoAlternate import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -34,51 +32,35 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelect -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.badges.post.NewBadgeDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.badges.post.NewBadgeModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size26Modifier import com.vitorpamplona.amethyst.ui.theme.Size55Modifier -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf @Composable fun NewBadgeButton(accountViewModel: AccountViewModel) { - var wantsToPickImage by remember { mutableStateOf(false) } - var pickedMedia by remember { mutableStateOf>(persistentListOf()) } - + var showDialog by remember { mutableStateOf(false) } val postViewModel: NewBadgeModel = viewModel() - if (wantsToPickImage) { - GallerySelect( - onImageUri = { uris -> - wantsToPickImage = false - // We only need the first picked image for a badge. - pickedMedia = if (uris.isNotEmpty()) persistentListOf(uris.first()) else persistentListOf() - }, - ) - } - - if (pickedMedia.isNotEmpty()) { + if (showDialog) { NewBadgeDialog( - uris = pickedMedia, - onClose = { pickedMedia = persistentListOf() }, + onClose = { showDialog = false }, postViewModel = postViewModel, accountViewModel = accountViewModel, ) } FloatingActionButton( - onClick = { wantsToPickImage = true }, + onClick = { showDialog = true }, modifier = Size55Modifier, shape = CircleShape, containerColor = MaterialTheme.colorScheme.primary, ) { Icon( - imageVector = Icons.Default.AddPhotoAlternate, + painter = painterRes(R.drawable.ic_compose, 5), contentDescription = stringRes(id = R.string.new_badge), modifier = Size26Modifier, tint = Color.White, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/post/NewBadgeDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/post/NewBadgeDialog.kt index ab2c0ff8ca..6f3a40f909 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/post/NewBadgeDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/post/NewBadgeDialog.kt @@ -20,20 +20,28 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.badges.post +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AddPhotoAlternate import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold @@ -44,12 +52,16 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog @@ -57,7 +69,7 @@ import androidx.compose.ui.window.DialogProperties import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelect import com.vitorpamplona.amethyst.ui.actions.uploads.ShowImageUploadGallery import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge import com.vitorpamplona.amethyst.ui.components.TextSpinner @@ -69,13 +81,12 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size5dp import com.vitorpamplona.amethyst.ui.theme.placeholderText -import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @OptIn(ExperimentalMaterial3Api::class) @Composable fun NewBadgeDialog( - uris: ImmutableList, onClose: () -> Unit, postViewModel: NewBadgeModel, accountViewModel: AccountViewModel, @@ -85,12 +96,25 @@ fun NewBadgeDialog( val scrollState = rememberScrollState() - LaunchedEffect(uris) { - postViewModel.load(account, uris) + LaunchedEffect(account) { + postViewModel.init(account) } StrippingFailureDialog(postViewModel.strippingFailureConfirmation) + var wantsToPickImage by remember { mutableStateOf(false) } + + if (wantsToPickImage) { + GallerySelect( + onImageUri = { uris -> + wantsToPickImage = false + postViewModel.setPickedMedia( + if (uris.isNotEmpty()) persistentListOf(uris.first()) else persistentListOf(), + ) + }, + ) + } + Dialog( onDismissRequest = onClose, properties = @@ -137,7 +161,15 @@ fun NewBadgeDialog( .fillMaxWidth() .verticalScroll(scrollState), ) { - BadgeImageForm(postViewModel, accountViewModel) + BadgeImagePicker( + postViewModel = postViewModel, + accountViewModel = accountViewModel, + onPickImage = { wantsToPickImage = true }, + ) + + Spacer(modifier = Modifier.height(12.dp)) + + BadgeFormFields(postViewModel, accountViewModel) } } } @@ -146,7 +178,69 @@ fun NewBadgeDialog( } @Composable -private fun BadgeImageForm( +private fun BadgeImagePicker( + postViewModel: NewBadgeModel, + accountViewModel: AccountViewModel, + onPickImage: () -> Unit, +) { + if (postViewModel.hasPickedImage()) { + postViewModel.multiOrchestrator?.let { + // Tap the preview to swap to a different image. + Box(modifier = Modifier.clickable(onClick = onPickImage)) { + ShowImageUploadGallery( + list = it, + onDelete = { postViewModel.setPickedMedia(persistentListOf()) }, + accountViewModel = accountViewModel, + ) + } + } + } else { + UploadPlaceholder(onClick = onPickImage) + } +} + +@Composable +private fun UploadPlaceholder(onClick: () -> Unit) { + Box( + modifier = + Modifier + .fillMaxWidth() + .aspectRatio(1f) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.outline, + shape = RoundedCornerShape(12.dp), + ).clickable(onClick = onClick) + .padding(24.dp), + contentAlignment = Alignment.Center, + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Icon( + imageVector = Icons.Default.AddPhotoAlternate, + contentDescription = null, + modifier = Modifier.size(56.dp), + tint = MaterialTheme.colorScheme.primary, + ) + Spacer(modifier = Modifier.height(12.dp)) + Text( + text = stringRes(R.string.badge_upload_image_cta), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = stringRes(R.string.badge_upload_image_hint), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + } + } +} + +@Composable +private fun BadgeFormFields( postViewModel: NewBadgeModel, accountViewModel: AccountViewModel, ) { @@ -160,18 +254,6 @@ private fun BadgeImageForm( .toImmutableList() } - postViewModel.multiOrchestrator?.let { - ShowImageUploadGallery( - it, - // Only one item expected; removing via UI would orphan the dialog. - // Ignore deletes — Cancel clears state via cancelModel(). - onDelete = { }, - accountViewModel = accountViewModel, - ) - } - - Spacer(modifier = Modifier.height(8.dp)) - OutlinedTextField( value = postViewModel.name, onValueChange = { postViewModel.name = it }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/post/NewBadgeModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/post/NewBadgeModel.kt index f696544125..1c33781fe8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/post/NewBadgeModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/post/NewBadgeModel.kt @@ -75,6 +75,13 @@ class NewBadgeModel : ViewModel() { var onceUploaded: () -> Unit = {} + fun init(account: Account) { + if (this.account == account) return + this.account = account + this.selectedServer = defaultServer() + this.stripMetadata = account.settings.stripLocationOnUpload + } + fun load( account: Account, uris: ImmutableList, @@ -87,6 +94,12 @@ class NewBadgeModel : ViewModel() { this.description = "" } + fun setPickedMedia(uris: ImmutableList) { + this.multiOrchestrator = if (uris.isNotEmpty()) MultiOrchestrator(uris) else null + } + + fun hasPickedImage(): Boolean = multiOrchestrator != null + fun canPost(): Boolean = !isUploading && multiOrchestrator != null && diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index d50010c0e7..43f49356db 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -440,6 +440,8 @@ https://example.com/badge.png Thumbnail URL (optional) https://example.com/badge-thumb.png + Upload an image + Pick a square image to be the face of your badge. Loading badge… Search users Name, npub, or NIP-05