mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 08:27:04 +00:00
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.
This commit is contained in:
+6
-24
@@ -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<ImmutableList<SelectedMedia>>(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,
|
||||
|
||||
+101
-19
@@ -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<SelectedMedia>,
|
||||
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 },
|
||||
|
||||
+13
@@ -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<SelectedMedia>,
|
||||
@@ -87,6 +94,12 @@ class NewBadgeModel : ViewModel() {
|
||||
this.description = ""
|
||||
}
|
||||
|
||||
fun setPickedMedia(uris: ImmutableList<SelectedMedia>) {
|
||||
this.multiOrchestrator = if (uris.isNotEmpty()) MultiOrchestrator(uris) else null
|
||||
}
|
||||
|
||||
fun hasPickedImage(): Boolean = multiOrchestrator != null
|
||||
|
||||
fun canPost(): Boolean =
|
||||
!isUploading &&
|
||||
multiOrchestrator != null &&
|
||||
|
||||
@@ -440,6 +440,8 @@
|
||||
<string name="badge_image_placeholder">https://example.com/badge.png</string>
|
||||
<string name="badge_thumb_label">Thumbnail URL (optional)</string>
|
||||
<string name="badge_thumb_placeholder">https://example.com/badge-thumb.png</string>
|
||||
<string name="badge_upload_image_cta">Upload an image</string>
|
||||
<string name="badge_upload_image_hint">Pick a square image to be the face of your badge.</string>
|
||||
<string name="award_badge_loading">Loading badge…</string>
|
||||
<string name="award_badge_search_label">Search users</string>
|
||||
<string name="award_badge_search_placeholder">Name, npub, or NIP-05</string>
|
||||
|
||||
Reference in New Issue
Block a user