mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
refactor: single-image uploads go through the shared orchestrator
Add commonsUI uploadToDefaultServer(media, account, uploader, onError) and use it in place of the five hand-rolled strip/compress/NIP-96/Blossom copies in the profile, emoji pack, emoji pack metadata, relay group metadata and long-form cover uploaders. Drop the Context plumbing the music and podcast composers no longer need. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P
This commit is contained in:
+2
-4
@@ -46,7 +46,6 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -91,7 +90,6 @@ fun NewUserMetadataScreen(
|
||||
) {
|
||||
val postViewModel: NewUserMetadataViewModel = viewModel()
|
||||
postViewModel.init(accountViewModel)
|
||||
val context = LocalContext.current
|
||||
|
||||
val socialExpanded = rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
@@ -213,7 +211,7 @@ fun NewUserMetadataScreen(
|
||||
tint = MaterialTheme.colorScheme.placeholderText,
|
||||
modifier = Modifier.padding(start = 5.dp),
|
||||
) {
|
||||
postViewModel.uploadForPicture(it, context, onError = accountViewModel.toastManager::toast)
|
||||
postViewModel.uploadForPicture(it, accountViewModel.host.mediaUploader, onError = accountViewModel.toastManager::toast)
|
||||
}
|
||||
},
|
||||
singleLine = true,
|
||||
@@ -238,7 +236,7 @@ fun NewUserMetadataScreen(
|
||||
tint = MaterialTheme.colorScheme.placeholderText,
|
||||
modifier = Modifier.padding(start = 5.dp),
|
||||
) {
|
||||
postViewModel.uploadForBanner(it, context, onError = accountViewModel.toastManager::toast)
|
||||
postViewModel.uploadForBanner(it, accountViewModel.host.mediaUploader, onError = accountViewModel.toastManager::toast)
|
||||
}
|
||||
},
|
||||
singleLine = true,
|
||||
|
||||
+10
-93
@@ -20,33 +20,19 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.actions
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.vitorpamplona.amethyst.commons.model.Account
|
||||
import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.avif_metadata_strip_failed
|
||||
import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details
|
||||
import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_upload_cancelled
|
||||
import com.vitorpamplona.amethyst.commons.resources.server_did_not_provide_a_url_after_uploading
|
||||
import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality
|
||||
import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader
|
||||
import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.uploads.uploadToDefaultServer
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.service.uploads.AvifMetadataNotVerifiableException
|
||||
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.quartz.nip39ExtIdentities.GitHubIdentity
|
||||
import com.vitorpamplona.quartz.nip39ExtIdentities.MastodonIdentity
|
||||
import com.vitorpamplona.quartz.nip39ExtIdentities.TwitterIdentity
|
||||
import com.vitorpamplona.quartz.nip39ExtIdentities.identityClaims
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
class NewUserMetadataViewModel : ViewModel() {
|
||||
private lateinit var accountViewModel: AccountViewModel
|
||||
@@ -164,13 +150,13 @@ class NewUserMetadataViewModel : ViewModel() {
|
||||
|
||||
fun uploadForPicture(
|
||||
uri: SelectedMedia,
|
||||
context: Context,
|
||||
uploader: MediaUploader,
|
||||
onError: (String, String) -> Unit,
|
||||
) {
|
||||
accountViewModel.launchSigner {
|
||||
upload(
|
||||
galleryUri = uri,
|
||||
context = context,
|
||||
uploader = uploader,
|
||||
onError = onError,
|
||||
)?.let {
|
||||
picture.value = it
|
||||
@@ -180,13 +166,13 @@ class NewUserMetadataViewModel : ViewModel() {
|
||||
|
||||
fun uploadForBanner(
|
||||
uri: SelectedMedia,
|
||||
context: Context,
|
||||
uploader: MediaUploader,
|
||||
onError: (String, String) -> Unit,
|
||||
) {
|
||||
accountViewModel.launchSigner {
|
||||
upload(
|
||||
galleryUri = uri,
|
||||
context = context,
|
||||
uploader = uploader,
|
||||
onError = onError,
|
||||
)?.let {
|
||||
banner.value = it
|
||||
@@ -196,14 +182,14 @@ class NewUserMetadataViewModel : ViewModel() {
|
||||
|
||||
fun uploadPictureAndSave(
|
||||
uri: SelectedMedia,
|
||||
context: Context,
|
||||
uploader: MediaUploader,
|
||||
onError: (String, String) -> Unit,
|
||||
) {
|
||||
load()
|
||||
accountViewModel.launchSigner {
|
||||
upload(
|
||||
galleryUri = uri,
|
||||
context = context,
|
||||
uploader = uploader,
|
||||
onError = onError,
|
||||
)?.let {
|
||||
picture.value = it
|
||||
@@ -215,81 +201,12 @@ class NewUserMetadataViewModel : ViewModel() {
|
||||
|
||||
private suspend fun upload(
|
||||
galleryUri: SelectedMedia,
|
||||
context: Context,
|
||||
uploader: MediaUploader,
|
||||
onError: (String, String) -> Unit,
|
||||
): String? {
|
||||
isUploadingImageForPicture = true
|
||||
|
||||
val strippingResult =
|
||||
if (account.settings.stripLocationOnUpload) {
|
||||
try {
|
||||
MetadataStripper.strip(galleryUri.uri, galleryUri.mimeType, context.applicationContext)
|
||||
} catch (e: AvifMetadataNotVerifiableException) {
|
||||
isUploadingImageForPicture = false
|
||||
onError(
|
||||
loadStringRes(Res.string.metadata_strip_failed_title),
|
||||
loadStringRes(Res.string.avif_metadata_strip_failed, e.message ?: e.javaClass.simpleName),
|
||||
)
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val sourceUri =
|
||||
if (account.settings.stripLocationOnUpload &&
|
||||
strippingResult != null &&
|
||||
!strippingResult.stripped
|
||||
) {
|
||||
onError(
|
||||
loadStringRes(Res.string.metadata_strip_failed_title),
|
||||
loadStringRes(Res.string.metadata_strip_failed_upload_cancelled),
|
||||
)
|
||||
return null
|
||||
} else {
|
||||
strippingResult?.uri ?: galleryUri.uri
|
||||
}
|
||||
|
||||
val compResult = MediaCompressor().compress(sourceUri, galleryUri.mimeType, CompressorQuality.MEDIUM, context.applicationContext)
|
||||
|
||||
return 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 = accountViewModel.httpClientBuilder::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 = accountViewModel.httpClientBuilder::okHttpClientForUploads,
|
||||
httpAuth = account::createBlossomUploadAuth,
|
||||
context = context,
|
||||
)
|
||||
}
|
||||
|
||||
if (result.url == null) {
|
||||
onError(loadStringRes(Res.string.failed_to_upload_media_no_details), loadStringRes(Res.string.server_did_not_provide_a_url_after_uploading))
|
||||
}
|
||||
|
||||
result.url
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
onError(loadStringRes(Res.string.failed_to_upload_media_no_details), e.message ?: e.javaClass.simpleName)
|
||||
null
|
||||
uploadToDefaultServer(galleryUri, account, uploader, onError)
|
||||
} finally {
|
||||
isUploadingImageForPicture = false
|
||||
}
|
||||
|
||||
+1
-3
@@ -60,7 +60,6 @@ import androidx.compose.ui.Alignment
|
||||
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.text.input.TextFieldValue
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
@@ -211,7 +210,6 @@ private fun RelayGroupMetadataScaffold(
|
||||
// existing group already implies a working relay, so callers pass true.
|
||||
nip29Support: Boolean? = true,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
// NIP-29 §Subgroups relay support detection: `"nip29": { "subgroups": true }` in the NIP-11.
|
||||
@@ -236,7 +234,7 @@ private fun RelayGroupMetadataScaffold(
|
||||
|
||||
val onSubmit: () -> Unit = {
|
||||
viewModel.submit(
|
||||
context = context,
|
||||
uploader = accountViewModel.host.mediaUploader,
|
||||
onSuccess = onSuccess,
|
||||
onError = accountViewModel.toastManager::toast,
|
||||
)
|
||||
|
||||
+6
-92
@@ -20,7 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -29,29 +28,17 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.model.Account
|
||||
import com.vitorpamplona.amethyst.commons.model.buzz.BuzzRelayDialect
|
||||
import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType
|
||||
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.avif_metadata_strip_failed
|
||||
import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details
|
||||
import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_sign_events
|
||||
import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_upload
|
||||
import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_upload_cancelled
|
||||
import com.vitorpamplona.amethyst.commons.resources.read_only_user
|
||||
import com.vitorpamplona.amethyst.commons.resources.server_did_not_provide_a_url_after_uploading
|
||||
import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality
|
||||
import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader
|
||||
import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.uploads.uploadToDefaultServer
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.service.uploads.AvifMetadataNotVerifiableException
|
||||
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.quartz.buzz.workspace.BUZZ_CHANNEL_TYPE_FORUM
|
||||
import com.vitorpamplona.quartz.buzz.workspace.BUZZ_CHANNEL_TYPE_STREAM
|
||||
import com.vitorpamplona.quartz.buzz.workspace.newBuzzChannelId
|
||||
@@ -61,7 +48,6 @@ import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
import com.vitorpamplona.quartz.utils.RandomInstance
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
/**
|
||||
* Backs the create/edit NIP-29 group metadata screens. Holds the full editable metadata
|
||||
@@ -221,7 +207,7 @@ class RelayGroupMetadataViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
fun submit(
|
||||
context: Context,
|
||||
uploader: MediaUploader,
|
||||
onSuccess: () -> Unit,
|
||||
onError: (String, String) -> Unit,
|
||||
) {
|
||||
@@ -231,7 +217,7 @@ class RelayGroupMetadataViewModel : ViewModel() {
|
||||
try {
|
||||
val local = pickedMedia
|
||||
if (local != null) {
|
||||
val uploadedUrl = uploadImage(local, context, onError) ?: return@launch
|
||||
val uploadedUrl = uploadImage(local, uploader, onError) ?: return@launch
|
||||
picture.value = TextFieldValue(uploadedUrl)
|
||||
pickedMedia = null
|
||||
}
|
||||
@@ -311,79 +297,7 @@ class RelayGroupMetadataViewModel : ViewModel() {
|
||||
|
||||
private suspend fun uploadImage(
|
||||
galleryUri: SelectedMedia,
|
||||
context: Context,
|
||||
uploader: MediaUploader,
|
||||
onError: (String, String) -> Unit,
|
||||
): String? {
|
||||
val sourceUri =
|
||||
try {
|
||||
if (account.settings.stripLocationOnUpload) {
|
||||
val result = MetadataStripper.strip(galleryUri.uri, galleryUri.mimeType, context.applicationContext)
|
||||
if (!result.stripped) {
|
||||
onError(
|
||||
loadStringRes(Res.string.metadata_strip_failed_title),
|
||||
loadStringRes(Res.string.metadata_strip_failed_upload_cancelled),
|
||||
)
|
||||
return null
|
||||
}
|
||||
result.uri
|
||||
} else {
|
||||
galleryUri.uri
|
||||
}
|
||||
} catch (e: AvifMetadataNotVerifiableException) {
|
||||
onError(
|
||||
loadStringRes(Res.string.metadata_strip_failed_title),
|
||||
loadStringRes(Res.string.avif_metadata_strip_failed, e.message ?: e.javaClass.simpleName),
|
||||
)
|
||||
return null
|
||||
}
|
||||
val compResult = MediaCompressor().compress(sourceUri, galleryUri.mimeType, CompressorQuality.MEDIUM, context.applicationContext)
|
||||
|
||||
return 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,
|
||||
)
|
||||
}
|
||||
|
||||
result.url ?: run {
|
||||
onError(
|
||||
loadStringRes(Res.string.failed_to_upload_media_no_details),
|
||||
loadStringRes(Res.string.server_did_not_provide_a_url_after_uploading),
|
||||
)
|
||||
null
|
||||
}
|
||||
} catch (_: SignerExceptions.ReadOnlyException) {
|
||||
onError(
|
||||
loadStringRes(Res.string.failed_to_upload_media_no_details),
|
||||
loadStringRes(Res.string.login_with_a_private_key_to_be_able_to_upload),
|
||||
)
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
onError(loadStringRes(Res.string.failed_to_upload_media_no_details), e.message ?: e.javaClass.simpleName)
|
||||
null
|
||||
}
|
||||
}
|
||||
): String? = uploadToDefaultServer(galleryUri, account, uploader, onError)
|
||||
}
|
||||
|
||||
+2
-32
@@ -51,7 +51,6 @@ import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details
|
||||
import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_sign_events
|
||||
import com.vitorpamplona.amethyst.commons.resources.read_only_user
|
||||
import com.vitorpamplona.amethyst.commons.resources.server_did_not_provide_a_url_after_uploading
|
||||
import com.vitorpamplona.amethyst.commons.service.pow.PoWReplay
|
||||
import com.vitorpamplona.amethyst.commons.service.upload.MediaUploadTracker
|
||||
import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation
|
||||
@@ -61,7 +60,6 @@ import com.vitorpamplona.amethyst.commons.service.uploads.MultiOrchestrator
|
||||
import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia
|
||||
import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMediaProcessing
|
||||
import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator
|
||||
import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.IMessageField
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState
|
||||
@@ -72,6 +70,7 @@ import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor
|
||||
import com.vitorpamplona.amethyst.commons.ui.text.onUiThread
|
||||
import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord
|
||||
import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource
|
||||
import com.vitorpamplona.amethyst.commons.ui.uploads.uploadToDefaultServer
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber
|
||||
@@ -487,36 +486,7 @@ class LongFormPostViewModel :
|
||||
galleryUri: SelectedMedia,
|
||||
uploader: MediaUploader,
|
||||
onError: (String, String) -> Unit,
|
||||
): String? {
|
||||
val state =
|
||||
UploadOrchestrator().upload(
|
||||
uri = galleryUri.uri,
|
||||
mimeType = galleryUri.mimeType,
|
||||
alt = null,
|
||||
contentWarningReason = null,
|
||||
compressionQuality = CompressorQuality.MEDIUM,
|
||||
server = account.settings.defaultFileServer,
|
||||
account = account,
|
||||
uploader = uploader,
|
||||
)
|
||||
|
||||
return when (state) {
|
||||
is UploadingState.Finished -> {
|
||||
val uploaded = state.result
|
||||
if (uploaded is UploadOrchestrator.OrchestratorResult.ServerResult) {
|
||||
uploaded.url
|
||||
} else {
|
||||
onError(loadStringRes(Res.string.failed_to_upload_media_no_details), loadStringRes(Res.string.server_did_not_provide_a_url_after_uploading))
|
||||
null
|
||||
}
|
||||
}
|
||||
is UploadingState.Error -> {
|
||||
onError(loadStringRes(Res.string.failed_to_upload_media_no_details), loadStringRes(state.errorResource, *state.params))
|
||||
null
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
): String? = uploadToDefaultServer(galleryUri, account, uploader, onError)
|
||||
|
||||
fun upload(
|
||||
alt: String?,
|
||||
|
||||
+1
-3
@@ -41,7 +41,6 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
@@ -119,7 +118,6 @@ fun AddEmojiDialog(
|
||||
onValueChange = { url = it },
|
||||
label = { Text(stringRes(Res.string.emoji_url_label)) },
|
||||
leadingIcon = {
|
||||
val context = LocalContext.current
|
||||
SelectSingleFromGallery(
|
||||
isUploading = viewModel.isUploadingEmojiImage,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
@@ -127,7 +125,7 @@ fun AddEmojiDialog(
|
||||
) { selected ->
|
||||
viewModel.uploadEmojiImage(
|
||||
uri = selected,
|
||||
context = context,
|
||||
uploader = accountViewModel.host.mediaUploader,
|
||||
onUploaded = { uploadedUrl -> url = uploadedUrl },
|
||||
onError = accountViewModel.toastManager::toast,
|
||||
)
|
||||
|
||||
+8
-93
@@ -20,7 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.emojipacks.display
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -28,31 +27,15 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.model.Account
|
||||
import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.avif_metadata_strip_failed
|
||||
import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details
|
||||
import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_upload
|
||||
import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_upload_cancelled
|
||||
import com.vitorpamplona.amethyst.commons.resources.server_did_not_provide_a_url_after_uploading
|
||||
import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality
|
||||
import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader
|
||||
import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.service.uploads.AvifMetadataNotVerifiableException
|
||||
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.quartz.nip01Core.signers.SignerExceptions
|
||||
import com.vitorpamplona.amethyst.commons.ui.uploads.uploadToDefaultServer
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
@Stable
|
||||
class EmojiPackViewModel(
|
||||
@@ -86,14 +69,14 @@ class EmojiPackViewModel(
|
||||
|
||||
fun uploadEmojiImage(
|
||||
uri: SelectedMedia,
|
||||
context: Context,
|
||||
uploader: MediaUploader,
|
||||
onUploaded: (String) -> Unit,
|
||||
onError: (String, String) -> Unit,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
upload(
|
||||
uri,
|
||||
context,
|
||||
uploader,
|
||||
onUploading = { isUploadingEmojiImage = it },
|
||||
onUploaded = onUploaded,
|
||||
onError = onError,
|
||||
@@ -103,83 +86,15 @@ class EmojiPackViewModel(
|
||||
|
||||
private suspend fun upload(
|
||||
galleryUri: SelectedMedia,
|
||||
context: Context,
|
||||
uploader: MediaUploader,
|
||||
onUploading: (Boolean) -> Unit,
|
||||
onUploaded: (String) -> Unit,
|
||||
onError: (String, String) -> Unit,
|
||||
) {
|
||||
onUploading(true)
|
||||
|
||||
val sourceUri =
|
||||
try {
|
||||
if (account.settings.stripLocationOnUpload) {
|
||||
val result = MetadataStripper.strip(galleryUri.uri, galleryUri.mimeType, context.applicationContext)
|
||||
if (!result.stripped) {
|
||||
onError(
|
||||
loadStringRes(Res.string.metadata_strip_failed_title),
|
||||
loadStringRes(Res.string.metadata_strip_failed_upload_cancelled),
|
||||
)
|
||||
onUploading(false)
|
||||
return
|
||||
}
|
||||
result.uri
|
||||
} else {
|
||||
galleryUri.uri
|
||||
}
|
||||
} catch (e: AvifMetadataNotVerifiableException) {
|
||||
onUploading(false)
|
||||
onError(
|
||||
loadStringRes(Res.string.metadata_strip_failed_title),
|
||||
loadStringRes(Res.string.avif_metadata_strip_failed, e.message ?: e.javaClass.simpleName),
|
||||
)
|
||||
return
|
||||
}
|
||||
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(loadStringRes(Res.string.failed_to_upload_media_no_details), loadStringRes(Res.string.server_did_not_provide_a_url_after_uploading))
|
||||
}
|
||||
} catch (_: SignerExceptions.ReadOnlyException) {
|
||||
onUploading(false)
|
||||
onError(loadStringRes(Res.string.failed_to_upload_media_no_details), loadStringRes(Res.string.login_with_a_private_key_to_be_able_to_upload))
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
onUploading(false)
|
||||
onError(loadStringRes(Res.string.failed_to_upload_media_no_details), e.message ?: e.javaClass.simpleName)
|
||||
}
|
||||
val url = uploadToDefaultServer(galleryUri, account, uploader, onError)
|
||||
onUploading(false)
|
||||
if (url != null) onUploaded(url)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
|
||||
+1
-3
@@ -53,7 +53,6 @@ import androidx.compose.ui.Alignment
|
||||
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.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
@@ -109,7 +108,6 @@ private fun EmojiPackMetadataScaffold(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
var wantsToPickImage by remember { mutableStateOf(false) }
|
||||
@@ -127,7 +125,7 @@ private fun EmojiPackMetadataScaffold(
|
||||
|
||||
val onSubmit: () -> Unit = {
|
||||
viewModel.submit(
|
||||
context = context,
|
||||
uploader = accountViewModel.host.mediaUploader,
|
||||
onSuccess = { nav.popBack() },
|
||||
onError = accountViewModel.toastManager::toast,
|
||||
)
|
||||
|
||||
+6
-94
@@ -20,7 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.emojipacks.list.metadata
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -29,32 +28,19 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.model.Account
|
||||
import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType
|
||||
import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.OwnedEmojiPack
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.avif_metadata_strip_failed
|
||||
import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details
|
||||
import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_sign_events
|
||||
import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_upload
|
||||
import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_upload_cancelled
|
||||
import com.vitorpamplona.amethyst.commons.resources.read_only_user
|
||||
import com.vitorpamplona.amethyst.commons.resources.server_did_not_provide_a_url_after_uploading
|
||||
import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality
|
||||
import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader
|
||||
import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.uploads.uploadToDefaultServer
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.service.uploads.AvifMetadataNotVerifiableException
|
||||
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.quartz.nip01Core.signers.SignerExceptions
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
@Stable
|
||||
class EmojiPackMetadataViewModel : ViewModel() {
|
||||
@@ -101,7 +87,7 @@ class EmojiPackMetadataViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
fun submit(
|
||||
context: Context,
|
||||
uploader: MediaUploader,
|
||||
onSuccess: () -> Unit,
|
||||
onError: (String, String) -> Unit,
|
||||
) {
|
||||
@@ -111,7 +97,7 @@ class EmojiPackMetadataViewModel : ViewModel() {
|
||||
try {
|
||||
val local = pickedMedia
|
||||
if (local != null) {
|
||||
val uploadedUrl = uploadImage(local, context, onError)
|
||||
val uploadedUrl = uploadImage(local, uploader, onError)
|
||||
if (uploadedUrl == null) {
|
||||
isWorking = false
|
||||
return@launch
|
||||
@@ -165,81 +151,7 @@ class EmojiPackMetadataViewModel : ViewModel() {
|
||||
|
||||
private suspend fun uploadImage(
|
||||
galleryUri: SelectedMedia,
|
||||
context: Context,
|
||||
uploader: MediaUploader,
|
||||
onError: (String, String) -> Unit,
|
||||
): String? {
|
||||
val sourceUri =
|
||||
try {
|
||||
if (account.settings.stripLocationOnUpload) {
|
||||
val result = MetadataStripper.strip(galleryUri.uri, galleryUri.mimeType, context.applicationContext)
|
||||
if (!result.stripped) {
|
||||
onError(
|
||||
loadStringRes(Res.string.metadata_strip_failed_title),
|
||||
loadStringRes(Res.string.metadata_strip_failed_upload_cancelled),
|
||||
)
|
||||
return null
|
||||
}
|
||||
result.uri
|
||||
} else {
|
||||
galleryUri.uri
|
||||
}
|
||||
} catch (e: AvifMetadataNotVerifiableException) {
|
||||
onError(
|
||||
loadStringRes(Res.string.metadata_strip_failed_title),
|
||||
loadStringRes(Res.string.avif_metadata_strip_failed, e.message ?: e.javaClass.simpleName),
|
||||
)
|
||||
return null
|
||||
}
|
||||
val compResult = MediaCompressor().compress(sourceUri, galleryUri.mimeType, CompressorQuality.MEDIUM, context.applicationContext)
|
||||
|
||||
return 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) {
|
||||
result.url
|
||||
} else {
|
||||
onError(
|
||||
loadStringRes(Res.string.failed_to_upload_media_no_details),
|
||||
loadStringRes(Res.string.server_did_not_provide_a_url_after_uploading),
|
||||
)
|
||||
null
|
||||
}
|
||||
} catch (_: SignerExceptions.ReadOnlyException) {
|
||||
onError(
|
||||
loadStringRes(Res.string.failed_to_upload_media_no_details),
|
||||
loadStringRes(Res.string.login_with_a_private_key_to_be_able_to_upload),
|
||||
)
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
onError(loadStringRes(Res.string.failed_to_upload_media_no_details), e.message ?: e.javaClass.simpleName)
|
||||
null
|
||||
}
|
||||
}
|
||||
): String? = uploadToDefaultServer(galleryUri, account, uploader, onError)
|
||||
}
|
||||
|
||||
-3
@@ -57,7 +57,6 @@ import androidx.compose.ui.Alignment
|
||||
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.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -116,7 +115,6 @@ fun NewMusicPlaylistScreen(
|
||||
nav: INav,
|
||||
) {
|
||||
val vm: NewMusicPlaylistViewModel = viewModel()
|
||||
val context = LocalContext.current
|
||||
|
||||
LaunchedEffect(accountViewModel) {
|
||||
vm.init(accountViewModel, editDTag)
|
||||
@@ -155,7 +153,6 @@ fun NewMusicPlaylistScreen(
|
||||
onPost = {
|
||||
if (!vm.isValid() || isBusy) return@SendingTopBar
|
||||
vm.saveAndPublish(
|
||||
context = context,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
},
|
||||
|
||||
+1
-7
@@ -20,7 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.music
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.vitorpamplona.amethyst.commons.model.Account
|
||||
@@ -185,10 +184,7 @@ class NewMusicPlaylistViewModel : ViewModel() {
|
||||
* runs on [AccountViewModel.viewModelScope] via `launchSigner`, so it keeps running even if
|
||||
* the user leaves the screen — they get a toast notification when it finishes either way.
|
||||
*/
|
||||
fun saveAndPublish(
|
||||
context: Context,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
fun saveAndPublish(accountViewModel: AccountViewModel) {
|
||||
if (isSending.value) return // double-tap guard
|
||||
if (!isValid()) return
|
||||
|
||||
@@ -218,7 +214,6 @@ class NewMusicPlaylistViewModel : ViewModel() {
|
||||
quality = CompressorQuality.fromSlider(mediaQualitySlider.value),
|
||||
stripMetadata = stripMetadata.value,
|
||||
loadedEvent = loadedEvent,
|
||||
appContext = context.applicationContext,
|
||||
uploader = accountViewModel.host.mediaUploader,
|
||||
)
|
||||
|
||||
@@ -266,7 +261,6 @@ class NewMusicPlaylistViewModel : ViewModel() {
|
||||
val quality: CompressorQuality,
|
||||
val stripMetadata: Boolean,
|
||||
val loadedEvent: MusicPlaylistEvent?,
|
||||
val appContext: Context,
|
||||
val uploader: MediaUploader,
|
||||
)
|
||||
|
||||
|
||||
-1
@@ -154,7 +154,6 @@ fun NewMusicTrackScreen(
|
||||
onPost = {
|
||||
if (!vm.isValid() || isBusy) return@SendingTopBar
|
||||
vm.saveAndPublish(
|
||||
context = context,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
},
|
||||
|
||||
+1
-6
@@ -259,10 +259,7 @@ class NewMusicTrackViewModel : ViewModel() {
|
||||
* Errors are reported through the global toast manager rather than a callback because
|
||||
* the screen may already be gone by the time we know the outcome.
|
||||
*/
|
||||
fun saveAndPublish(
|
||||
context: Context,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
fun saveAndPublish(accountViewModel: AccountViewModel) {
|
||||
if (isSending.value) return // double-tap guard
|
||||
|
||||
// Snapshot every input the upload needs into immutable locals BEFORE launching, so
|
||||
@@ -293,7 +290,6 @@ class NewMusicTrackViewModel : ViewModel() {
|
||||
quality = CompressorQuality.fromSlider(mediaQualitySlider.value),
|
||||
stripMetadata = stripMetadata.value,
|
||||
loadedEvent = loadedEvent,
|
||||
appContext = context.applicationContext,
|
||||
uploader = accountViewModel.host.mediaUploader,
|
||||
)
|
||||
|
||||
@@ -365,7 +361,6 @@ class NewMusicTrackViewModel : ViewModel() {
|
||||
val quality: CompressorQuality,
|
||||
val stripMetadata: Boolean,
|
||||
val loadedEvent: MusicTrackEvent?,
|
||||
val appContext: Context,
|
||||
val uploader: MediaUploader,
|
||||
)
|
||||
|
||||
|
||||
+1
-3
@@ -44,7 +44,6 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -97,7 +96,6 @@ fun EditPodcastShowScreen(
|
||||
nav: INav,
|
||||
) {
|
||||
val vm: EditPodcastShowViewModel = viewModel()
|
||||
val context = LocalContext.current
|
||||
|
||||
LaunchedEffect(accountViewModel) { vm.init(accountViewModel) }
|
||||
|
||||
@@ -124,7 +122,7 @@ fun EditPodcastShowScreen(
|
||||
isActive = { vm.isValid() && !isBusy },
|
||||
onPost = {
|
||||
if (!vm.isValid() || isBusy) return@SendingTopBar
|
||||
vm.saveAndPublish(context, accountViewModel)
|
||||
vm.saveAndPublish(accountViewModel)
|
||||
},
|
||||
)
|
||||
},
|
||||
|
||||
+1
-7
@@ -20,7 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.authoring
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.vitorpamplona.amethyst.commons.model.Account
|
||||
@@ -130,10 +129,7 @@ class EditPodcastShowViewModel : ViewModel() {
|
||||
|
||||
fun isValid(): Boolean = title.value.isNotBlank()
|
||||
|
||||
fun saveAndPublish(
|
||||
context: Context,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
fun saveAndPublish(accountViewModel: AccountViewModel) {
|
||||
if (isSending.value) return
|
||||
|
||||
val coverOrch = coverMedia.value
|
||||
@@ -171,7 +167,6 @@ class EditPodcastShowViewModel : ViewModel() {
|
||||
server = server,
|
||||
quality = CompressorQuality.fromSlider(mediaQualitySlider.value),
|
||||
stripMetadata = stripMetadata.value,
|
||||
appContext = context.applicationContext,
|
||||
uploader = accountViewModel.host.mediaUploader,
|
||||
)
|
||||
|
||||
@@ -223,7 +218,6 @@ class EditPodcastShowViewModel : ViewModel() {
|
||||
val server: ServerName?,
|
||||
val quality: CompressorQuality,
|
||||
val stripMetadata: Boolean,
|
||||
val appContext: Context,
|
||||
val uploader: MediaUploader,
|
||||
)
|
||||
}
|
||||
|
||||
+1
-1
@@ -152,7 +152,7 @@ fun NewPodcastEpisodeScreen(
|
||||
isActive = { vm.isValid() && !isBusy },
|
||||
onPost = {
|
||||
if (!vm.isValid() || isBusy) return@SendingTopBar
|
||||
vm.saveAndPublish(context, accountViewModel)
|
||||
vm.saveAndPublish(accountViewModel)
|
||||
},
|
||||
)
|
||||
},
|
||||
|
||||
+1
-6
@@ -174,10 +174,7 @@ class NewPodcastEpisodeViewModel : ViewModel() {
|
||||
/** Valid with a title and a resolvable audio source (picked file or a pasted URL). */
|
||||
fun isValid(): Boolean = title.value.isNotBlank() && (audioMedia.value != null || audioUrl.value.isNotBlank())
|
||||
|
||||
fun saveAndPublish(
|
||||
context: Context,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
fun saveAndPublish(accountViewModel: AccountViewModel) {
|
||||
if (isSending.value) return
|
||||
|
||||
val server = selectedServer.value
|
||||
@@ -208,7 +205,6 @@ class NewPodcastEpisodeViewModel : ViewModel() {
|
||||
server = server,
|
||||
quality = CompressorQuality.fromSlider(mediaQualitySlider.value),
|
||||
stripMetadata = stripMetadata.value,
|
||||
appContext = context.applicationContext,
|
||||
uploader = accountViewModel.host.mediaUploader,
|
||||
)
|
||||
|
||||
@@ -269,7 +265,6 @@ class NewPodcastEpisodeViewModel : ViewModel() {
|
||||
val server: ServerName,
|
||||
val quality: CompressorQuality,
|
||||
val stripMetadata: Boolean,
|
||||
val appContext: Context,
|
||||
val uploader: MediaUploader,
|
||||
)
|
||||
|
||||
|
||||
+1
-2
@@ -88,7 +88,6 @@ fun NewPodcastTrailerScreen(
|
||||
nav: INav,
|
||||
) {
|
||||
val vm: NewPodcastTrailerViewModel = viewModel()
|
||||
val context = LocalContext.current
|
||||
|
||||
LaunchedEffect(accountViewModel) { vm.init(accountViewModel) }
|
||||
StrippingFailureDialog(vm.strippingFailureConfirmation)
|
||||
@@ -112,7 +111,7 @@ fun NewPodcastTrailerScreen(
|
||||
isActive = { vm.isValid() && !isBusy },
|
||||
onPost = {
|
||||
if (!vm.isValid() || isBusy) return@SendingTopBar
|
||||
vm.saveAndPublish(context, accountViewModel)
|
||||
vm.saveAndPublish(accountViewModel)
|
||||
},
|
||||
)
|
||||
},
|
||||
|
||||
+1
-6
@@ -20,7 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.authoring
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.vitorpamplona.amethyst.commons.model.Account
|
||||
@@ -91,10 +90,7 @@ class NewPodcastTrailerViewModel : ViewModel() {
|
||||
|
||||
fun isValid(): Boolean = title.value.isNotBlank() && (media.value != null || url.value.isNotBlank())
|
||||
|
||||
fun saveAndPublish(
|
||||
context: Context,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
fun saveAndPublish(accountViewModel: AccountViewModel) {
|
||||
if (isSending.value) return
|
||||
val server = selectedServer.value
|
||||
if (server == null) {
|
||||
@@ -112,7 +108,6 @@ class NewPodcastTrailerViewModel : ViewModel() {
|
||||
val mediaSnap = media.value
|
||||
val quality = CompressorQuality.fromSlider(mediaQualitySlider.value)
|
||||
val strip = stripMetadata.value
|
||||
val appContext = context.applicationContext
|
||||
|
||||
isSending.value = true
|
||||
accountViewModel.launchSigner {
|
||||
|
||||
+1
-3
@@ -49,7 +49,6 @@ import androidx.compose.ui.layout.boundsInWindow
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.ClipEntry
|
||||
import androidx.compose.ui.platform.LocalClipboard
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
@@ -194,7 +193,6 @@ private fun ProfilePictureUploadButton(
|
||||
) {
|
||||
val postViewModel: NewUserMetadataViewModel = viewModel()
|
||||
postViewModel.init(accountViewModel)
|
||||
val context = LocalContext.current
|
||||
|
||||
var showGallerySelect by remember { mutableStateOf(false) }
|
||||
if (showGallerySelect) {
|
||||
@@ -202,7 +200,7 @@ private fun ProfilePictureUploadButton(
|
||||
onImageUri = { media ->
|
||||
showGallerySelect = false
|
||||
if (media != null) {
|
||||
postViewModel.uploadPictureAndSave(media, context, accountViewModel.toastManager::toast)
|
||||
postViewModel.uploadPictureAndSave(media, accountViewModel.host.mediaUploader, accountViewModel.toastManager::toast)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.ui.uploads
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.Account
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details
|
||||
import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_upload
|
||||
import com.vitorpamplona.amethyst.commons.resources.server_did_not_provide_a_url_after_uploading
|
||||
import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality
|
||||
import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader
|
||||
import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia
|
||||
import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator
|
||||
import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
||||
/**
|
||||
* Compresses, strips (when the account asks for it) and uploads one picked file to the account's
|
||||
* default media server, returning the hosted URL. Every failure, a metadata-stripping one
|
||||
* included, is reported through [onError] with a user-facing title and message, and yields null.
|
||||
*
|
||||
* For single-image fields (avatars, banners, pack and cover images) that only need a URL back.
|
||||
*/
|
||||
suspend fun uploadToDefaultServer(
|
||||
media: SelectedMedia,
|
||||
account: Account,
|
||||
uploader: MediaUploader,
|
||||
onError: (title: String, message: String) -> Unit,
|
||||
quality: CompressorQuality = CompressorQuality.MEDIUM,
|
||||
): String? {
|
||||
val state =
|
||||
try {
|
||||
UploadOrchestrator().upload(
|
||||
uri = media.uri,
|
||||
mimeType = media.mimeType,
|
||||
alt = null,
|
||||
contentWarningReason = null,
|
||||
compressionQuality = quality,
|
||||
server = account.settings.defaultFileServer,
|
||||
account = account,
|
||||
uploader = uploader,
|
||||
stripMetadata = account.settings.stripLocationOnUpload,
|
||||
onStrippingFailed = { false },
|
||||
)
|
||||
} catch (_: SignerExceptions.ReadOnlyException) {
|
||||
onError(loadStringRes(Res.string.failed_to_upload_media_no_details), loadStringRes(Res.string.login_with_a_private_key_to_be_able_to_upload))
|
||||
return null
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
onError(loadStringRes(Res.string.failed_to_upload_media_no_details), e.message ?: e::class.simpleName ?: "")
|
||||
return null
|
||||
}
|
||||
|
||||
return when (state) {
|
||||
is UploadingState.Finished -> {
|
||||
val uploaded = state.result
|
||||
if (uploaded is UploadOrchestrator.OrchestratorResult.ServerResult) {
|
||||
uploaded.url
|
||||
} else {
|
||||
onError(loadStringRes(Res.string.failed_to_upload_media_no_details), loadStringRes(Res.string.server_did_not_provide_a_url_after_uploading))
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
is UploadingState.Error -> {
|
||||
onError(loadStringRes(Res.string.failed_to_upload_media_no_details), loadStringRes(state.errorResource, *state.params))
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user