feat(emoji): upload cover image in emoji pack metadata screen
Reuses the NIP-96/Blossom uploader plumbing from BookmarkGroupMetadata — the cover image field now shows a gallery-picker icon on its leading side and populates the URL on upload success. The manual URL input still works as before. https://claude.ai/code/session_01SNG3nj8ZZDChggTsg1qznn
This commit is contained in:
+17
-2
@@ -37,11 +37,13 @@ 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.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
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 com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectSingleFromGallery
|
||||||
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
|
||||||
@@ -104,7 +106,7 @@ private fun EmojiPackMetadataScaffold(
|
|||||||
PackName(viewModel)
|
PackName(viewModel)
|
||||||
Spacer(modifier = DoubleVertSpacer)
|
Spacer(modifier = DoubleVertSpacer)
|
||||||
|
|
||||||
PackImage(viewModel)
|
PackImage(viewModel, accountViewModel)
|
||||||
Spacer(modifier = DoubleVertSpacer)
|
Spacer(modifier = DoubleVertSpacer)
|
||||||
|
|
||||||
PackDescription(viewModel)
|
PackDescription(viewModel)
|
||||||
@@ -184,7 +186,10 @@ private fun PackName(viewModel: EmojiPackMetadataViewModel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun PackImage(viewModel: EmojiPackMetadataViewModel) {
|
private fun PackImage(
|
||||||
|
viewModel: EmojiPackMetadataViewModel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
label = { Text(text = stringRes(R.string.emoji_pack_image_label)) },
|
label = { Text(text = stringRes(R.string.emoji_pack_image_label)) },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -196,6 +201,16 @@ private fun PackImage(viewModel: EmojiPackMetadataViewModel) {
|
|||||||
color = MaterialTheme.colorScheme.placeholderText,
|
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)
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+106
@@ -20,6 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.emojipacks.list.metadata
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.emojipacks.list.metadata
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -27,9 +28,24 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.nip30CustomEmojis.OwnedEmojiPack
|
import com.vitorpamplona.amethyst.model.nip30CustomEmojis.OwnedEmojiPack
|
||||||
|
import com.vitorpamplona.amethyst.service.uploads.CompressorQuality
|
||||||
|
import com.vitorpamplona.amethyst.service.uploads.MediaCompressor
|
||||||
|
import com.vitorpamplona.amethyst.service.uploads.MetadataStripper
|
||||||
|
import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader
|
||||||
|
import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||||
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.quartz.nip01Core.signers.SignerExceptions
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class EmojiPackMetadataViewModel : ViewModel() {
|
class EmojiPackMetadataViewModel : ViewModel() {
|
||||||
@@ -43,6 +59,8 @@ class EmojiPackMetadataViewModel : ViewModel() {
|
|||||||
val picture = mutableStateOf(TextFieldValue())
|
val picture = mutableStateOf(TextFieldValue())
|
||||||
val description = mutableStateOf(TextFieldValue())
|
val description = mutableStateOf(TextFieldValue())
|
||||||
|
|
||||||
|
var isUploadingImageForPicture by mutableStateOf(false)
|
||||||
|
|
||||||
val canPost by derivedStateOf {
|
val canPost by derivedStateOf {
|
||||||
name.value.text.isNotBlank()
|
name.value.text.isNotBlank()
|
||||||
}
|
}
|
||||||
@@ -91,4 +109,92 @@ class EmojiPackMetadataViewModel : ViewModel() {
|
|||||||
picture.value = TextFieldValue()
|
picture.value = TextFieldValue()
|
||||||
description.value = TextFieldValue()
|
description.value = TextFieldValue()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun uploadForPicture(
|
||||||
|
uri: SelectedMedia,
|
||||||
|
context: Context,
|
||||||
|
onError: (String, String) -> Unit,
|
||||||
|
) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
upload(
|
||||||
|
uri,
|
||||||
|
context,
|
||||||
|
onUploading = { isUploadingImageForPicture = it },
|
||||||
|
onUploaded = { picture.value = TextFieldValue(it) },
|
||||||
|
onError = onError,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun upload(
|
||||||
|
galleryUri: SelectedMedia,
|
||||||
|
context: Context,
|
||||||
|
onUploading: (Boolean) -> Unit,
|
||||||
|
onUploaded: (String) -> Unit,
|
||||||
|
onError: (String, String) -> Unit,
|
||||||
|
) {
|
||||||
|
onUploading(true)
|
||||||
|
|
||||||
|
val sourceUri =
|
||||||
|
if (account.settings.stripLocationOnUpload) {
|
||||||
|
val result = MetadataStripper.strip(galleryUri.uri, galleryUri.mimeType, context.applicationContext)
|
||||||
|
if (!result.stripped) {
|
||||||
|
onError(
|
||||||
|
stringRes(context, R.string.metadata_strip_failed_title),
|
||||||
|
stringRes(context, R.string.metadata_strip_failed_upload_cancelled),
|
||||||
|
)
|
||||||
|
onUploading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result.uri
|
||||||
|
} else {
|
||||||
|
galleryUri.uri
|
||||||
|
}
|
||||||
|
val compResult = MediaCompressor().compress(sourceUri, galleryUri.mimeType, CompressorQuality.MEDIUM, context.applicationContext)
|
||||||
|
|
||||||
|
try {
|
||||||
|
val result =
|
||||||
|
if (account.settings.defaultFileServer.type == ServerType.NIP96) {
|
||||||
|
Nip96Uploader().upload(
|
||||||
|
uri = compResult.uri,
|
||||||
|
contentType = compResult.contentType,
|
||||||
|
size = compResult.size,
|
||||||
|
alt = null,
|
||||||
|
sensitiveContent = null,
|
||||||
|
serverBaseUrl = account.settings.defaultFileServer.baseUrl,
|
||||||
|
okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads,
|
||||||
|
onProgress = {},
|
||||||
|
httpAuth = account::createHTTPAuthorization,
|
||||||
|
context = context,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
BlossomUploader().upload(
|
||||||
|
uri = compResult.uri,
|
||||||
|
contentType = compResult.contentType,
|
||||||
|
size = compResult.size,
|
||||||
|
alt = null,
|
||||||
|
sensitiveContent = null,
|
||||||
|
serverBaseUrl = account.settings.defaultFileServer.baseUrl,
|
||||||
|
okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads,
|
||||||
|
httpAuth = account::createBlossomUploadAuth,
|
||||||
|
context = context,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.url != null) {
|
||||||
|
onUploading(false)
|
||||||
|
onUploaded(result.url)
|
||||||
|
} else {
|
||||||
|
onUploading(false)
|
||||||
|
onError(stringRes(context, R.string.failed_to_upload_media_no_details), stringRes(context, R.string.server_did_not_provide_a_url_after_uploading))
|
||||||
|
}
|
||||||
|
} catch (_: SignerExceptions.ReadOnlyException) {
|
||||||
|
onUploading(false)
|
||||||
|
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))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
|
onUploading(false)
|
||||||
|
onError(stringRes(context, R.string.failed_to_upload_media_no_details), e.message ?: e.javaClass.simpleName)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user