Merge modern emoji metadata screen into integration branch
This commit is contained in:
+175
-81
@@ -20,38 +20,59 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.emojipacks.list.metadata
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.emojipacks.list.metadata
|
||||||
|
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
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.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.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.LocalTextStyle
|
import androidx.compose.material3.LocalTextStyle
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
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.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
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.input.KeyboardCapitalization
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextDirection
|
import androidx.compose.ui.text.style.TextDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import coil3.compose.AsyncImage
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectSingleFromGallery
|
import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelectSingle
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.CreatingTopBar
|
import com.vitorpamplona.amethyst.ui.navigation.topbars.CreatingTopBar
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar
|
import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun EmojiPackMetadataScreen(
|
fun EmojiPackMetadataScreen(
|
||||||
@@ -82,34 +103,65 @@ private fun EmojiPackMetadataScaffold(
|
|||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
|
var wantsToPickImage by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
if (wantsToPickImage) {
|
||||||
|
GallerySelectSingle(
|
||||||
|
onImageUri = { media ->
|
||||||
|
wantsToPickImage = false
|
||||||
|
if (media != null) {
|
||||||
|
viewModel.pickMedia(media)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val onSubmit: () -> Unit = {
|
||||||
|
viewModel.submit(
|
||||||
|
context = context,
|
||||||
|
onSuccess = { nav.popBack() },
|
||||||
|
onError = accountViewModel.toastManager::toast,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
EmojiPackMetadataTopBar(
|
EmojiPackMetadataTopBar(
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
accountViewModel = accountViewModel,
|
|
||||||
nav = nav,
|
nav = nav,
|
||||||
|
onSubmit = onSubmit,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { pad ->
|
) { pad ->
|
||||||
LazyColumn(
|
Surface(
|
||||||
Modifier
|
modifier =
|
||||||
.fillMaxSize()
|
Modifier
|
||||||
.padding(
|
.padding(pad)
|
||||||
start = 10.dp,
|
.consumeWindowInsets(pad)
|
||||||
end = 10.dp,
|
.imePadding(),
|
||||||
top = pad.calculateTopPadding(),
|
|
||||||
bottom = pad.calculateBottomPadding(),
|
|
||||||
).consumeWindowInsets(pad)
|
|
||||||
.imePadding(),
|
|
||||||
) {
|
) {
|
||||||
item {
|
Column(
|
||||||
PackName(viewModel)
|
Modifier
|
||||||
Spacer(modifier = DoubleVertSpacer)
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = 10.dp, vertical = 10.dp),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.verticalScroll(scrollState),
|
||||||
|
) {
|
||||||
|
PackImagePicker(
|
||||||
|
viewModel = viewModel,
|
||||||
|
onPickImage = { wantsToPickImage = true },
|
||||||
|
)
|
||||||
|
|
||||||
PackImage(viewModel, accountViewModel)
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
Spacer(modifier = DoubleVertSpacer)
|
|
||||||
|
|
||||||
PackDescription(viewModel)
|
PackFormFields(viewModel)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,8 +170,8 @@ private fun EmojiPackMetadataScaffold(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun EmojiPackMetadataTopBar(
|
private fun EmojiPackMetadataTopBar(
|
||||||
viewModel: EmojiPackMetadataViewModel,
|
viewModel: EmojiPackMetadataViewModel,
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
nav: INav,
|
nav: INav,
|
||||||
|
onSubmit: () -> Unit,
|
||||||
) {
|
) {
|
||||||
if (viewModel.isNewPack) {
|
if (viewModel.isNewPack) {
|
||||||
CreatingTopBar(
|
CreatingTopBar(
|
||||||
@@ -129,17 +181,7 @@ private fun EmojiPackMetadataTopBar(
|
|||||||
viewModel.clear()
|
viewModel.clear()
|
||||||
nav.popBack()
|
nav.popBack()
|
||||||
},
|
},
|
||||||
onPost = {
|
onPost = onSubmit,
|
||||||
try {
|
|
||||||
viewModel.createOrUpdate()
|
|
||||||
nav.popBack()
|
|
||||||
} catch (e: SignerExceptions.ReadOnlyException) {
|
|
||||||
accountViewModel.toastManager.toast(
|
|
||||||
R.string.read_only_user,
|
|
||||||
R.string.login_with_a_private_key_to_be_able_to_sign_events,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
SavingTopBar(
|
SavingTopBar(
|
||||||
@@ -149,83 +191,135 @@ private fun EmojiPackMetadataTopBar(
|
|||||||
viewModel.clear()
|
viewModel.clear()
|
||||||
nav.popBack()
|
nav.popBack()
|
||||||
},
|
},
|
||||||
onPost = {
|
onPost = onSubmit,
|
||||||
try {
|
|
||||||
viewModel.createOrUpdate()
|
|
||||||
nav.popBack()
|
|
||||||
} catch (e: SignerExceptions.ReadOnlyException) {
|
|
||||||
accountViewModel.toastManager.toast(
|
|
||||||
R.string.read_only_user,
|
|
||||||
R.string.login_with_a_private_key_to_be_able_to_sign_events,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun PackName(viewModel: EmojiPackMetadataViewModel) {
|
private fun PackImagePicker(
|
||||||
|
viewModel: EmojiPackMetadataViewModel,
|
||||||
|
onPickImage: () -> Unit,
|
||||||
|
) {
|
||||||
|
val picked = viewModel.pickedMedia
|
||||||
|
val currentUrl = viewModel.picture.value.text
|
||||||
|
|
||||||
|
when {
|
||||||
|
picked != null -> {
|
||||||
|
HeroImagePreview(
|
||||||
|
model = picked.uri,
|
||||||
|
onClick = onPickImage,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentUrl.isNotBlank() -> {
|
||||||
|
HeroImagePreview(
|
||||||
|
model = currentUrl,
|
||||||
|
onClick = onPickImage,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
UploadPlaceholder(onClick = onPickImage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun HeroImagePreview(
|
||||||
|
model: Any,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = model,
|
||||||
|
contentDescription = stringRes(R.string.emoji_pack_image_label),
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(1f)
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.clickable(onClick = onClick),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@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.emoji_pack_upload_image_cta),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = stringRes(R.string.emoji_pack_upload_image_hint),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PackFormFields(viewModel: EmojiPackMetadataViewModel) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
label = { Text(text = stringRes(R.string.emoji_pack_name_label)) },
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
value = viewModel.name.value,
|
value = viewModel.name.value,
|
||||||
onValueChange = { viewModel.name.value = it },
|
onValueChange = { viewModel.name.value = it },
|
||||||
|
label = { Text(text = stringRes(R.string.emoji_pack_name_label)) },
|
||||||
placeholder = {
|
placeholder = {
|
||||||
Text(
|
Text(
|
||||||
text = stringRes(R.string.emoji_pack_name_label),
|
text = stringRes(R.string.emoji_pack_name_label),
|
||||||
color = MaterialTheme.colorScheme.placeholderText,
|
color = MaterialTheme.colorScheme.placeholderText,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true,
|
||||||
keyboardOptions =
|
keyboardOptions =
|
||||||
KeyboardOptions.Default.copy(
|
KeyboardOptions.Default.copy(
|
||||||
capitalization = KeyboardCapitalization.Sentences,
|
capitalization = KeyboardCapitalization.Sentences,
|
||||||
),
|
),
|
||||||
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
private fun PackImage(
|
|
||||||
viewModel: EmojiPackMetadataViewModel,
|
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
) {
|
|
||||||
OutlinedTextField(
|
|
||||||
label = { Text(text = stringRes(R.string.emoji_pack_image_label)) },
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
value = viewModel.picture.value,
|
|
||||||
onValueChange = { viewModel.picture.value = it },
|
|
||||||
placeholder = {
|
|
||||||
Text(
|
|
||||||
text = "https://example.com/cover.jpg",
|
|
||||||
color = MaterialTheme.colorScheme.placeholderText,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
leadingIcon = {
|
|
||||||
val context = LocalContext.current
|
|
||||||
SelectSingleFromGallery(
|
|
||||||
isUploading = viewModel.isUploadingImageForPicture,
|
|
||||||
tint = MaterialTheme.colorScheme.placeholderText,
|
|
||||||
modifier = Modifier.padding(start = 2.dp),
|
|
||||||
) {
|
|
||||||
viewModel.uploadForPicture(it, context, onError = accountViewModel.toastManager::toast)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun PackDescription(viewModel: EmojiPackMetadataViewModel) {
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
label = { Text(text = stringRes(R.string.emoji_pack_description_label)) },
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
value = viewModel.description.value,
|
value = viewModel.description.value,
|
||||||
onValueChange = { viewModel.description.value = it },
|
onValueChange = { viewModel.description.value = it },
|
||||||
|
label = { Text(text = stringRes(R.string.emoji_pack_description_label)) },
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(120.dp),
|
||||||
|
minLines = 2,
|
||||||
|
maxLines = 6,
|
||||||
keyboardOptions =
|
keyboardOptions =
|
||||||
KeyboardOptions.Default.copy(
|
KeyboardOptions.Default.copy(
|
||||||
capitalization = KeyboardCapitalization.Sentences,
|
capitalization = KeyboardCapitalization.Sentences,
|
||||||
),
|
),
|
||||||
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||||
minLines = 3,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+119
-49
@@ -59,12 +59,25 @@ class EmojiPackMetadataViewModel : ViewModel() {
|
|||||||
val picture = mutableStateOf(TextFieldValue())
|
val picture = mutableStateOf(TextFieldValue())
|
||||||
val description = mutableStateOf(TextFieldValue())
|
val description = mutableStateOf(TextFieldValue())
|
||||||
|
|
||||||
var isUploadingImageForPicture by mutableStateOf(false)
|
/**
|
||||||
|
* Local image the user just picked from the gallery but hasn't uploaded yet.
|
||||||
|
* When non-null the hero preview shows this file and `submit()` will upload
|
||||||
|
* it before publishing the emoji pack event. Mutated only via [pickMedia] /
|
||||||
|
* [clearPickedMedia] so the setter name doesn't collide on the JVM.
|
||||||
|
*/
|
||||||
|
var pickedMedia by mutableStateOf<SelectedMedia?>(null)
|
||||||
|
private set
|
||||||
|
|
||||||
|
/** True while upload-then-publish is running. Disables the submit button and shows a spinner. */
|
||||||
|
var isWorking by mutableStateOf(false)
|
||||||
|
|
||||||
val canPost by derivedStateOf {
|
val canPost by derivedStateOf {
|
||||||
name.value.text.isNotBlank()
|
!isWorking && name.value.text.isNotBlank()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** True when either a remote cover URL exists OR the user has picked a local image. */
|
||||||
|
fun hasImage(): Boolean = pickedMedia != null || picture.value.text.isNotBlank()
|
||||||
|
|
||||||
fun init(accountViewModel: AccountViewModel) {
|
fun init(accountViewModel: AccountViewModel) {
|
||||||
this.accountViewModel = accountViewModel
|
this.accountViewModel = accountViewModel
|
||||||
this.account = accountViewModel.account
|
this.account = accountViewModel.account
|
||||||
@@ -81,25 +94,90 @@ class EmojiPackMetadataViewModel : ViewModel() {
|
|||||||
name.value = TextFieldValue(existing?.title ?: "")
|
name.value = TextFieldValue(existing?.title ?: "")
|
||||||
picture.value = TextFieldValue(existing?.image ?: "")
|
picture.value = TextFieldValue(existing?.image ?: "")
|
||||||
description.value = TextFieldValue(existing?.description ?: "")
|
description.value = TextFieldValue(existing?.description ?: "")
|
||||||
|
pickedMedia = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun pickMedia(media: SelectedMedia) {
|
||||||
|
pickedMedia = media
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearPickedMedia() {
|
||||||
|
pickedMedia = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kicks off the full create/update flow:
|
||||||
|
* 1. If a local image was picked, upload it first and update `picture`.
|
||||||
|
* 2. Build & sign the EmojiPackEvent with the (possibly newly uploaded) URL.
|
||||||
|
*
|
||||||
|
* Mirrors the badge-definition flow where the user never sees the URL and the
|
||||||
|
* image upload is implicit in pressing "Create" / "Save".
|
||||||
|
*/
|
||||||
|
fun submit(
|
||||||
|
context: Context,
|
||||||
|
onSuccess: () -> Unit,
|
||||||
|
onError: (String, String) -> Unit,
|
||||||
|
) {
|
||||||
|
if (isWorking) return
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
isWorking = true
|
||||||
|
try {
|
||||||
|
val local = pickedMedia
|
||||||
|
if (local != null) {
|
||||||
|
val uploadedUrl = uploadImage(local, context, onError)
|
||||||
|
if (uploadedUrl == null) {
|
||||||
|
isWorking = false
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
picture.value = TextFieldValue(uploadedUrl)
|
||||||
|
pickedMedia = null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
publish()
|
||||||
|
} catch (e: SignerExceptions.ReadOnlyException) {
|
||||||
|
onError(
|
||||||
|
stringRes(context, R.string.read_only_user),
|
||||||
|
stringRes(context, R.string.login_with_a_private_key_to_be_able_to_sign_events),
|
||||||
|
)
|
||||||
|
isWorking = false
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
clear()
|
||||||
|
onSuccess()
|
||||||
|
} finally {
|
||||||
|
isWorking = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun publish() {
|
||||||
|
val currentPack = pack
|
||||||
|
if (currentPack == null) {
|
||||||
|
account.createOwnedEmojiPack(
|
||||||
|
title = name.value.text,
|
||||||
|
description = description.value.text,
|
||||||
|
image = picture.value.text,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
account.updateOwnedEmojiPackMetadata(
|
||||||
|
dTag = currentPack.identifier,
|
||||||
|
newTitle = name.value.text,
|
||||||
|
newDescription = description.value.text,
|
||||||
|
newImage = picture.value.text,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retained for backward compatibility with the old "paste URL + upload button"
|
||||||
|
* flow. New UI goes through [submit]. The signer contract is unchanged: the
|
||||||
|
* final signed EmojiPackEvent still carries the published URL in `image`.
|
||||||
|
*/
|
||||||
|
@Suppress("unused")
|
||||||
fun createOrUpdate() {
|
fun createOrUpdate() {
|
||||||
accountViewModel.launchSigner {
|
accountViewModel.launchSigner {
|
||||||
val currentPack = pack
|
publish()
|
||||||
if (currentPack == null) {
|
|
||||||
account.createOwnedEmojiPack(
|
|
||||||
title = name.value.text,
|
|
||||||
description = description.value.text,
|
|
||||||
image = picture.value.text,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
account.updateOwnedEmojiPackMetadata(
|
|
||||||
dTag = currentPack.identifier,
|
|
||||||
newTitle = name.value.text,
|
|
||||||
newDescription = description.value.text,
|
|
||||||
newImage = picture.value.text,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
clear()
|
clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,33 +186,21 @@ class EmojiPackMetadataViewModel : ViewModel() {
|
|||||||
name.value = TextFieldValue()
|
name.value = TextFieldValue()
|
||||||
picture.value = TextFieldValue()
|
picture.value = TextFieldValue()
|
||||||
description.value = TextFieldValue()
|
description.value = TextFieldValue()
|
||||||
|
pickedMedia = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun uploadForPicture(
|
/**
|
||||||
uri: SelectedMedia,
|
* Uploads [galleryUri] using the user's configured default file server,
|
||||||
context: Context,
|
* respecting the account's strip-location-on-upload preference. Returns the
|
||||||
onError: (String, String) -> Unit,
|
* published URL or null on failure (having already called [onError]).
|
||||||
) {
|
*
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
* Mirrors the NIP-96/Blossom block used by `BookmarkGroupMetadataViewModel.upload`.
|
||||||
upload(
|
*/
|
||||||
uri,
|
private suspend fun uploadImage(
|
||||||
context,
|
|
||||||
onUploading = { isUploadingImageForPicture = it },
|
|
||||||
onUploaded = { picture.value = TextFieldValue(it) },
|
|
||||||
onError = onError,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun upload(
|
|
||||||
galleryUri: SelectedMedia,
|
galleryUri: SelectedMedia,
|
||||||
context: Context,
|
context: Context,
|
||||||
onUploading: (Boolean) -> Unit,
|
|
||||||
onUploaded: (String) -> Unit,
|
|
||||||
onError: (String, String) -> Unit,
|
onError: (String, String) -> Unit,
|
||||||
) {
|
): String? {
|
||||||
onUploading(true)
|
|
||||||
|
|
||||||
val sourceUri =
|
val sourceUri =
|
||||||
if (account.settings.stripLocationOnUpload) {
|
if (account.settings.stripLocationOnUpload) {
|
||||||
val result = MetadataStripper.strip(galleryUri.uri, galleryUri.mimeType, context.applicationContext)
|
val result = MetadataStripper.strip(galleryUri.uri, galleryUri.mimeType, context.applicationContext)
|
||||||
@@ -143,8 +209,7 @@ class EmojiPackMetadataViewModel : ViewModel() {
|
|||||||
stringRes(context, R.string.metadata_strip_failed_title),
|
stringRes(context, R.string.metadata_strip_failed_title),
|
||||||
stringRes(context, R.string.metadata_strip_failed_upload_cancelled),
|
stringRes(context, R.string.metadata_strip_failed_upload_cancelled),
|
||||||
)
|
)
|
||||||
onUploading(false)
|
return null
|
||||||
return
|
|
||||||
}
|
}
|
||||||
result.uri
|
result.uri
|
||||||
} else {
|
} else {
|
||||||
@@ -152,7 +217,7 @@ class EmojiPackMetadataViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
val compResult = MediaCompressor().compress(sourceUri, galleryUri.mimeType, CompressorQuality.MEDIUM, context.applicationContext)
|
val compResult = MediaCompressor().compress(sourceUri, galleryUri.mimeType, CompressorQuality.MEDIUM, context.applicationContext)
|
||||||
|
|
||||||
try {
|
return try {
|
||||||
val result =
|
val result =
|
||||||
if (account.settings.defaultFileServer.type == ServerType.NIP96) {
|
if (account.settings.defaultFileServer.type == ServerType.NIP96) {
|
||||||
Nip96Uploader().upload(
|
Nip96Uploader().upload(
|
||||||
@@ -182,19 +247,24 @@ class EmojiPackMetadataViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (result.url != null) {
|
if (result.url != null) {
|
||||||
onUploading(false)
|
result.url
|
||||||
onUploaded(result.url)
|
|
||||||
} else {
|
} else {
|
||||||
onUploading(false)
|
onError(
|
||||||
onError(stringRes(context, R.string.failed_to_upload_media_no_details), stringRes(context, R.string.server_did_not_provide_a_url_after_uploading))
|
stringRes(context, R.string.failed_to_upload_media_no_details),
|
||||||
|
stringRes(context, R.string.server_did_not_provide_a_url_after_uploading),
|
||||||
|
)
|
||||||
|
null
|
||||||
}
|
}
|
||||||
} catch (_: SignerExceptions.ReadOnlyException) {
|
} catch (_: SignerExceptions.ReadOnlyException) {
|
||||||
onUploading(false)
|
onError(
|
||||||
onError(stringRes(context, R.string.failed_to_upload_media_no_details), stringRes(context, R.string.login_with_a_private_key_to_be_able_to_upload))
|
stringRes(context, R.string.failed_to_upload_media_no_details),
|
||||||
|
stringRes(context, R.string.login_with_a_private_key_to_be_able_to_upload),
|
||||||
|
)
|
||||||
|
null
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (e is CancellationException) throw e
|
if (e is CancellationException) throw e
|
||||||
onUploading(false)
|
|
||||||
onError(stringRes(context, R.string.failed_to_upload_media_no_details), e.message ?: e.javaClass.simpleName)
|
onError(stringRes(context, R.string.failed_to_upload_media_no_details), e.message ?: e.javaClass.simpleName)
|
||||||
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2387,6 +2387,8 @@
|
|||||||
<string name="emoji_pack_name_label">Pack name</string>
|
<string name="emoji_pack_name_label">Pack name</string>
|
||||||
<string name="emoji_pack_description_label">Description (optional)</string>
|
<string name="emoji_pack_description_label">Description (optional)</string>
|
||||||
<string name="emoji_pack_image_label">Cover image URL (optional)</string>
|
<string name="emoji_pack_image_label">Cover image URL (optional)</string>
|
||||||
|
<string name="emoji_pack_upload_image_cta">Upload a cover image</string>
|
||||||
|
<string name="emoji_pack_upload_image_hint">Pick a square image to represent this emoji pack.</string>
|
||||||
<string name="no_emoji_packs">You don\'t have any emoji packs yet</string>
|
<string name="no_emoji_packs">You don\'t have any emoji packs yet</string>
|
||||||
<string name="emoji_pack_count">%1$d emojis</string>
|
<string name="emoji_pack_count">%1$d emojis</string>
|
||||||
<string name="my_emoji_list_title">My Emoji List</string>
|
<string name="my_emoji_list_title">My Emoji List</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user