fix: Don't lose images / captions when editing with failed uploads (#1054)
Previous code would remove image attachments from the compose editor if there was a problem uploading or updating them. This caused a particular problem with image captions. You could attach a valid image, then write a caption that was too long for the server. The server would reject the status, and the status was saved to drafts. Then you open the draft, which tries to upload the image again with a too-long caption. The upload is rejected, and the image, along with the caption, is removed. Fix this. - Change `QueuedMedia` to track the upload state as a `Result<_,_>`, so any error messages are preserved and available to the UI. - The different `Ok` types for the upload state contain the upload progress percentage (if appropriate) or the server's ID for the uploaded media. - Change `ProgressImageView` to accept the upload state `Result`. If the result is an error the image is drawn with a red overlay and white "error" icon. - If an upload is in an error state allow the user to click on it. That shows a dialog explaining the error, and provides options to edit the image, change the caption, etc. - When changing the caption make the API call to change it on the server (if the attachment has been uploaded). This makes the user aware of any errors sooner in the process, so they can correct them. Fixes #879
This commit is contained in:
parent
ec52df3d61
commit
abfd3240bd
|
@ -109,7 +109,9 @@ import app.pachli.util.setDrawableTint
|
||||||
import com.canhub.cropper.CropImage
|
import com.canhub.cropper.CropImage
|
||||||
import com.canhub.cropper.CropImageContract
|
import com.canhub.cropper.CropImageContract
|
||||||
import com.canhub.cropper.options
|
import com.canhub.cropper.options
|
||||||
|
import com.github.michaelbull.result.Result
|
||||||
import com.github.michaelbull.result.getOrElse
|
import com.github.michaelbull.result.getOrElse
|
||||||
|
import com.github.michaelbull.result.mapBoth
|
||||||
import com.github.michaelbull.result.onFailure
|
import com.github.michaelbull.result.onFailure
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
import com.google.android.material.color.MaterialColors
|
import com.google.android.material.color.MaterialColors
|
||||||
|
@ -253,7 +255,7 @@ class ComposeActivity :
|
||||||
val mediaAdapter = MediaPreviewAdapter(
|
val mediaAdapter = MediaPreviewAdapter(
|
||||||
this,
|
this,
|
||||||
onAddCaption = { item ->
|
onAddCaption = { item ->
|
||||||
CaptionDialog.newInstance(item.localId, item.description, item.uri).show(supportFragmentManager, "caption_dialog")
|
CaptionDialog.newInstance(item.localId, item.serverId, item.description, item.uri).show(supportFragmentManager, "caption_dialog")
|
||||||
},
|
},
|
||||||
onAddFocus = { item ->
|
onAddFocus = { item ->
|
||||||
makeFocusDialog(item.focus, item.uri) { newFocus ->
|
makeFocusDialog(item.focus, item.uri) { newFocus ->
|
||||||
|
@ -524,14 +526,6 @@ class ComposeActivity :
|
||||||
enablePollButton(media.isEmpty())
|
enablePollButton(media.isEmpty())
|
||||||
}.collect()
|
}.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
lifecycleScope.launch {
|
|
||||||
viewModel.uploadError.collect { mediaUploaderError ->
|
|
||||||
val message = mediaUploaderError.fmt(this@ComposeActivity)
|
|
||||||
|
|
||||||
displayPermamentMessage(getString(R.string.error_media_upload_sending_fmt, message))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return List of states of the different bottomsheets */
|
/** @return List of states of the different bottomsheets */
|
||||||
|
@ -1272,14 +1266,8 @@ class ComposeActivity :
|
||||||
* User is editing a new post, and can either save the changes as a draft or discard them.
|
* User is editing a new post, and can either save the changes as a draft or discard them.
|
||||||
*/
|
*/
|
||||||
private fun getSaveAsDraftOrDiscardDialog(contentText: String, contentWarning: String): AlertDialog.Builder {
|
private fun getSaveAsDraftOrDiscardDialog(contentText: String, contentWarning: String): AlertDialog.Builder {
|
||||||
val warning = if (viewModel.media.value.isNotEmpty()) {
|
val builder = AlertDialog.Builder(this)
|
||||||
R.string.compose_save_draft_loses_media
|
.setTitle(R.string.compose_save_draft)
|
||||||
} else {
|
|
||||||
R.string.compose_save_draft
|
|
||||||
}
|
|
||||||
|
|
||||||
return AlertDialog.Builder(this)
|
|
||||||
.setMessage(warning)
|
|
||||||
.setPositiveButton(R.string.action_save) { _, _ ->
|
.setPositiveButton(R.string.action_save) { _, _ ->
|
||||||
viewModel.stopUploads()
|
viewModel.stopUploads()
|
||||||
saveDraftAndFinish(contentText, contentWarning)
|
saveDraftAndFinish(contentText, contentWarning)
|
||||||
|
@ -1288,6 +1276,12 @@ class ComposeActivity :
|
||||||
viewModel.stopUploads()
|
viewModel.stopUploads()
|
||||||
deleteDraftAndFinish()
|
deleteDraftAndFinish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (viewModel.media.value.isNotEmpty()) {
|
||||||
|
builder.setMessage(R.string.compose_save_draft_loses_media)
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1295,14 +1289,8 @@ class ComposeActivity :
|
||||||
* discard them.
|
* discard them.
|
||||||
*/
|
*/
|
||||||
private fun getUpdateDraftOrDiscardDialog(contentText: String, contentWarning: String): AlertDialog.Builder {
|
private fun getUpdateDraftOrDiscardDialog(contentText: String, contentWarning: String): AlertDialog.Builder {
|
||||||
val warning = if (viewModel.media.value.isNotEmpty()) {
|
val builder = AlertDialog.Builder(this)
|
||||||
R.string.compose_save_draft_loses_media
|
.setTitle(R.string.compose_save_draft)
|
||||||
} else {
|
|
||||||
R.string.compose_save_draft
|
|
||||||
}
|
|
||||||
|
|
||||||
return AlertDialog.Builder(this)
|
|
||||||
.setMessage(warning)
|
|
||||||
.setPositiveButton(R.string.action_save) { _, _ ->
|
.setPositiveButton(R.string.action_save) { _, _ ->
|
||||||
viewModel.stopUploads()
|
viewModel.stopUploads()
|
||||||
saveDraftAndFinish(contentText, contentWarning)
|
saveDraftAndFinish(contentText, contentWarning)
|
||||||
|
@ -1311,6 +1299,12 @@ class ComposeActivity :
|
||||||
viewModel.stopUploads()
|
viewModel.stopUploads()
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (viewModel.media.value.isNotEmpty()) {
|
||||||
|
builder.setMessage(R.string.compose_save_draft_loses_media)
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1392,23 +1386,39 @@ class ComposeActivity :
|
||||||
val uri: Uri,
|
val uri: Uri,
|
||||||
val type: Type,
|
val type: Type,
|
||||||
val mediaSize: Long,
|
val mediaSize: Long,
|
||||||
val uploadPercent: Int = 0,
|
|
||||||
val id: String? = null,
|
|
||||||
val description: String? = null,
|
val description: String? = null,
|
||||||
val focus: Attachment.Focus? = null,
|
val focus: Attachment.Focus? = null,
|
||||||
val state: State,
|
val uploadState: Result<UploadState, MediaUploaderError>,
|
||||||
) {
|
) {
|
||||||
enum class Type {
|
enum class Type {
|
||||||
IMAGE,
|
IMAGE,
|
||||||
VIDEO,
|
VIDEO,
|
||||||
AUDIO,
|
AUDIO,
|
||||||
}
|
}
|
||||||
enum class State {
|
|
||||||
UPLOADING,
|
/**
|
||||||
UNPROCESSED,
|
* Server's ID for this attachment. May be null if the media is still
|
||||||
PROCESSED,
|
* being uploaded, or it was uploaded and there was an error that
|
||||||
PUBLISHED,
|
* meant it couldn't be processed. Attachments that have an error
|
||||||
}
|
* *after* processing have a non-null `serverId`.
|
||||||
|
*/
|
||||||
|
val serverId: String?
|
||||||
|
get() = uploadState.mapBoth(
|
||||||
|
{ state ->
|
||||||
|
when (state) {
|
||||||
|
is UploadState.Uploading -> null
|
||||||
|
is UploadState.Uploaded.Processing -> state.serverId
|
||||||
|
is UploadState.Uploaded.Processed -> state.serverId
|
||||||
|
is UploadState.Uploaded.Published -> state.serverId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ error ->
|
||||||
|
when (error) {
|
||||||
|
is MediaUploaderError.UpdateMediaError -> error.serverId
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTimeSet(time: Date?) {
|
override fun onTimeSet(time: Date?) {
|
||||||
|
@ -1425,8 +1435,8 @@ class ComposeActivity :
|
||||||
scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUpdateDescription(localId: Int, description: String) {
|
override fun onUpdateDescription(localId: Int, serverId: String?, description: String) {
|
||||||
viewModel.updateDescription(localId, description)
|
viewModel.updateDescription(localId, serverId, description)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -28,6 +28,7 @@ import androidx.lifecycle.viewModelScope
|
||||||
import app.pachli.R
|
import app.pachli.R
|
||||||
import app.pachli.components.compose.ComposeActivity.QueuedMedia
|
import app.pachli.components.compose.ComposeActivity.QueuedMedia
|
||||||
import app.pachli.components.compose.ComposeAutoCompleteAdapter.AutocompleteResult
|
import app.pachli.components.compose.ComposeAutoCompleteAdapter.AutocompleteResult
|
||||||
|
import app.pachli.components.compose.UploadState.Uploaded
|
||||||
import app.pachli.components.drafts.DraftHelper
|
import app.pachli.components.drafts.DraftHelper
|
||||||
import app.pachli.components.search.SearchType
|
import app.pachli.components.search.SearchType
|
||||||
import app.pachli.core.common.PachliError
|
import app.pachli.core.common.PachliError
|
||||||
|
@ -52,20 +53,20 @@ import at.connyduck.calladapter.networkresult.fold
|
||||||
import com.github.michaelbull.result.Err
|
import com.github.michaelbull.result.Err
|
||||||
import com.github.michaelbull.result.Ok
|
import com.github.michaelbull.result.Ok
|
||||||
import com.github.michaelbull.result.Result
|
import com.github.michaelbull.result.Result
|
||||||
|
import com.github.michaelbull.result.andThen
|
||||||
import com.github.michaelbull.result.get
|
import com.github.michaelbull.result.get
|
||||||
import com.github.michaelbull.result.getOrElse
|
import com.github.michaelbull.result.getOrElse
|
||||||
import com.github.michaelbull.result.mapBoth
|
import com.github.michaelbull.result.mapBoth
|
||||||
import com.github.michaelbull.result.mapError
|
import com.github.michaelbull.result.mapError
|
||||||
|
import com.github.michaelbull.result.onFailure
|
||||||
|
import com.github.michaelbull.result.onSuccess
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import io.github.z4kn4fein.semver.constraints.toConstraint
|
import io.github.z4kn4fein.semver.constraints.toConstraint
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
@ -144,8 +145,6 @@ class ComposeViewModel @Inject constructor(
|
||||||
|
|
||||||
private val _media: MutableStateFlow<List<QueuedMedia>> = MutableStateFlow(emptyList())
|
private val _media: MutableStateFlow<List<QueuedMedia>> = MutableStateFlow(emptyList())
|
||||||
val media = _media.asStateFlow()
|
val media = _media.asStateFlow()
|
||||||
private val _uploadError = MutableSharedFlow<MediaUploaderError>(replay = 0, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
|
||||||
val uploadError = _uploadError.asSharedFlow()
|
|
||||||
private val _closeConfirmation = MutableStateFlow(ConfirmationKind.NONE)
|
private val _closeConfirmation = MutableStateFlow(ConfirmationKind.NONE)
|
||||||
val closeConfirmation = _closeConfirmation.asStateFlow()
|
val closeConfirmation = _closeConfirmation.asStateFlow()
|
||||||
private val _statusLength = MutableStateFlow(0)
|
private val _statusLength = MutableStateFlow(0)
|
||||||
|
@ -227,7 +226,7 @@ class ComposeViewModel @Inject constructor(
|
||||||
mediaSize = mediaSize,
|
mediaSize = mediaSize,
|
||||||
description = description,
|
description = description,
|
||||||
focus = focus,
|
focus = focus,
|
||||||
state = QueuedMedia.State.UPLOADING,
|
uploadState = Ok(UploadState.Uploading(percentage = 0)),
|
||||||
)
|
)
|
||||||
stashMediaItem = mediaItem
|
stashMediaItem = mediaItem
|
||||||
|
|
||||||
|
@ -245,35 +244,8 @@ class ComposeViewModel @Inject constructor(
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
mediaUploader
|
mediaUploader
|
||||||
.uploadMedia(mediaItem, instanceInfo.value)
|
.uploadMedia(mediaItem, instanceInfo.value)
|
||||||
.collect { event ->
|
.collect { uploadResult ->
|
||||||
val item = media.value.find { it.localId == mediaItem.localId }
|
updateMediaItem(mediaItem.localId) { it.copy(uploadState = uploadResult) }
|
||||||
?: return@collect
|
|
||||||
var newMediaItem: QueuedMedia? = null
|
|
||||||
val uploadEvent = event.getOrElse {
|
|
||||||
_media.update { mediaList -> mediaList.filter { it.localId != mediaItem.localId } }
|
|
||||||
_uploadError.emit(it)
|
|
||||||
return@collect
|
|
||||||
}
|
|
||||||
|
|
||||||
newMediaItem = when (uploadEvent) {
|
|
||||||
is UploadEvent.ProgressEvent -> item.copy(uploadPercent = uploadEvent.percentage)
|
|
||||||
is UploadEvent.FinishedEvent -> {
|
|
||||||
item.copy(
|
|
||||||
id = uploadEvent.media.mediaId,
|
|
||||||
uploadPercent = -1,
|
|
||||||
state = if (uploadEvent.media.processed) {
|
|
||||||
QueuedMedia.State.PROCESSED
|
|
||||||
} else {
|
|
||||||
QueuedMedia.State.UNPROCESSED
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
newMediaItem.let {
|
|
||||||
_media.update { mediaList ->
|
|
||||||
mediaList.map { mediaItem -> if (mediaItem.localId == it.localId) it else mediaItem }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -288,11 +260,9 @@ class ComposeViewModel @Inject constructor(
|
||||||
uri = uri,
|
uri = uri,
|
||||||
type = type,
|
type = type,
|
||||||
mediaSize = 0,
|
mediaSize = 0,
|
||||||
uploadPercent = -1,
|
|
||||||
id = id,
|
|
||||||
description = description,
|
description = description,
|
||||||
focus = focus,
|
focus = focus,
|
||||||
state = QueuedMedia.State.PUBLISHED,
|
uploadState = Ok(Uploaded.Published(id)),
|
||||||
)
|
)
|
||||||
mediaList + mediaItem
|
mediaList + mediaItem
|
||||||
}
|
}
|
||||||
|
@ -457,11 +427,11 @@ class ComposeViewModel @Inject constructor(
|
||||||
val attachedMedia = media.value.map { item ->
|
val attachedMedia = media.value.map { item ->
|
||||||
MediaToSend(
|
MediaToSend(
|
||||||
localId = item.localId,
|
localId = item.localId,
|
||||||
id = item.id,
|
id = item.serverId,
|
||||||
uri = item.uri.toString(),
|
uri = item.uri.toString(),
|
||||||
description = item.description,
|
description = item.description,
|
||||||
focus = item.focus,
|
focus = item.focus,
|
||||||
processed = item.state == QueuedMedia.State.PROCESSED || item.state == QueuedMedia.State.PUBLISHED,
|
processed = item.uploadState.get() is Uploaded.Processed || item.uploadState.get() is Uploaded.Published,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val tootToSend = StatusToSend(
|
val tootToSend = StatusToSend(
|
||||||
|
@ -498,16 +468,45 @@ class ComposeViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateDescription(localId: Int, description: String) {
|
fun updateDescription(localId: Int, serverId: String?, description: String) {
|
||||||
updateMediaItem(localId) { mediaItem ->
|
// If the image hasn't been uploaded then update the state locally.
|
||||||
mediaItem.copy(description = description)
|
if (serverId == null) {
|
||||||
|
updateMediaItem(localId) { mediaItem -> mediaItem.copy(description = description) }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the remote description and report any errors. Update the local description
|
||||||
|
// if there are errors so the user still has the text and can try and correct it.
|
||||||
|
viewModelScope.launch {
|
||||||
|
api.updateMedia(serverId, description = description)
|
||||||
|
.andThen { api.getMedia(serverId) }
|
||||||
|
.onSuccess { response ->
|
||||||
|
val state = if (response.code == 200) {
|
||||||
|
Uploaded.Processed(serverId)
|
||||||
|
} else {
|
||||||
|
Uploaded.Processing(serverId)
|
||||||
|
}
|
||||||
|
updateMediaItem(localId) {
|
||||||
|
it.copy(
|
||||||
|
description = description,
|
||||||
|
uploadState = Ok(state),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.mapError { MediaUploaderError.UpdateMediaError(serverId, it) }
|
||||||
|
.onFailure { error ->
|
||||||
|
updateMediaItem(localId) {
|
||||||
|
it.copy(
|
||||||
|
description = description,
|
||||||
|
uploadState = Err(error),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateFocus(localId: Int, focus: Attachment.Focus) {
|
fun updateFocus(localId: Int, focus: Attachment.Focus) {
|
||||||
updateMediaItem(localId) { mediaItem ->
|
updateMediaItem(localId) { mediaItem -> mediaItem.copy(focus = focus) }
|
||||||
mediaItem.copy(focus = focus)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun searchAutocompleteSuggestions(token: String): List<AutocompleteResult> {
|
suspend fun searchAutocompleteSuggestions(token: String): List<AutocompleteResult> {
|
||||||
|
|
|
@ -21,30 +21,44 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.PopupMenu
|
import android.widget.PopupMenu
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.recyclerview.widget.AsyncListDiffer
|
import androidx.recyclerview.widget.AsyncListDiffer
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import app.pachli.R
|
import app.pachli.R
|
||||||
|
import app.pachli.components.compose.ComposeActivity.QueuedMedia
|
||||||
|
import app.pachli.components.compose.UploadState.Uploaded
|
||||||
import app.pachli.components.compose.view.ProgressImageView
|
import app.pachli.components.compose.view.ProgressImageView
|
||||||
import app.pachli.core.designsystem.R as DR
|
import app.pachli.core.designsystem.R as DR
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
|
import com.github.michaelbull.result.get
|
||||||
|
import com.github.michaelbull.result.onFailure
|
||||||
|
import com.github.michaelbull.result.onSuccess
|
||||||
|
|
||||||
class MediaPreviewAdapter(
|
class MediaPreviewAdapter(
|
||||||
context: Context,
|
context: Context,
|
||||||
private val onAddCaption: (ComposeActivity.QueuedMedia) -> Unit,
|
private val onAddCaption: (QueuedMedia) -> Unit,
|
||||||
private val onAddFocus: (ComposeActivity.QueuedMedia) -> Unit,
|
private val onAddFocus: (QueuedMedia) -> Unit,
|
||||||
private val onEditImage: (ComposeActivity.QueuedMedia) -> Unit,
|
private val onEditImage: (QueuedMedia) -> Unit,
|
||||||
private val onRemove: (ComposeActivity.QueuedMedia) -> Unit,
|
private val onRemove: (QueuedMedia) -> Unit,
|
||||||
) : RecyclerView.Adapter<MediaPreviewAdapter.PreviewViewHolder>() {
|
) : RecyclerView.Adapter<MediaPreviewAdapter.PreviewViewHolder>() {
|
||||||
|
|
||||||
fun submitList(list: List<ComposeActivity.QueuedMedia>) {
|
fun submitList(list: List<QueuedMedia>) {
|
||||||
this.differ.submitList(list)
|
this.differ.submitList(list)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onMediaClick(position: Int, view: View) {
|
private fun onMediaClick(position: Int, view: View) {
|
||||||
val item = differ.currentList[position]
|
val item = differ.currentList[position]
|
||||||
|
|
||||||
|
// Handle error
|
||||||
|
item.uploadState
|
||||||
|
.onSuccess { showMediaPopup(item, view) }
|
||||||
|
.onFailure { showMediaError(item, it, view) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showMediaPopup(item: QueuedMedia, view: View) {
|
||||||
val popup = PopupMenu(view.context, view)
|
val popup = PopupMenu(view.context, view)
|
||||||
val addCaptionId = 1
|
val addCaptionId = 1
|
||||||
val addFocusId = 2
|
val addFocusId = 2
|
||||||
|
@ -52,9 +66,9 @@ class MediaPreviewAdapter(
|
||||||
val removeId = 4
|
val removeId = 4
|
||||||
|
|
||||||
popup.menu.add(0, addCaptionId, 0, R.string.action_set_caption)
|
popup.menu.add(0, addCaptionId, 0, R.string.action_set_caption)
|
||||||
if (item.type == ComposeActivity.QueuedMedia.Type.IMAGE) {
|
if (item.type == QueuedMedia.Type.IMAGE) {
|
||||||
popup.menu.add(0, addFocusId, 0, R.string.action_set_focus)
|
popup.menu.add(0, addFocusId, 0, R.string.action_set_focus)
|
||||||
if (item.state != ComposeActivity.QueuedMedia.State.PUBLISHED) {
|
if (item.uploadState.get() !is Uploaded.Published) {
|
||||||
// Already-published items can't be edited
|
// Already-published items can't be edited
|
||||||
popup.menu.add(0, editImageId, 0, R.string.action_edit_image)
|
popup.menu.add(0, editImageId, 0, R.string.action_edit_image)
|
||||||
}
|
}
|
||||||
|
@ -72,6 +86,15 @@ class MediaPreviewAdapter(
|
||||||
popup.show()
|
popup.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showMediaError(item: QueuedMedia, error: MediaUploaderError, view: View) {
|
||||||
|
AlertDialog.Builder(view.context)
|
||||||
|
.setTitle(R.string.action_post_failed)
|
||||||
|
.setMessage(view.context.getString(R.string.upload_failed_msg_fmt, error.fmt(view.context)))
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||||
|
.setNegativeButton(R.string.upload_failed_modify_attachment) { _, _ -> showMediaPopup(item, view) }
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
private val thumbnailViewSize =
|
private val thumbnailViewSize =
|
||||||
context.resources.getDimensionPixelSize(DR.dimen.compose_media_preview_size)
|
context.resources.getDimensionPixelSize(DR.dimen.compose_media_preview_size)
|
||||||
|
|
||||||
|
@ -84,8 +107,10 @@ class MediaPreviewAdapter(
|
||||||
override fun onBindViewHolder(holder: PreviewViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: PreviewViewHolder, position: Int) {
|
||||||
val item = differ.currentList[position]
|
val item = differ.currentList[position]
|
||||||
holder.progressImageView.setChecked(!item.description.isNullOrEmpty())
|
holder.progressImageView.setChecked(!item.description.isNullOrEmpty())
|
||||||
holder.progressImageView.setProgress(item.uploadPercent)
|
|
||||||
if (item.type == ComposeActivity.QueuedMedia.Type.AUDIO) {
|
holder.progressImageView.setResult(item.uploadState)
|
||||||
|
|
||||||
|
if (item.type == QueuedMedia.Type.AUDIO) {
|
||||||
// TODO: Fancy waveform display?
|
// TODO: Fancy waveform display?
|
||||||
holder.progressImageView.setImageResource(R.drawable.ic_music_box_preview_24dp)
|
holder.progressImageView.setImageResource(R.drawable.ic_music_box_preview_24dp)
|
||||||
} else {
|
} else {
|
||||||
|
@ -114,12 +139,12 @@ class MediaPreviewAdapter(
|
||||||
|
|
||||||
private val differ = AsyncListDiffer(
|
private val differ = AsyncListDiffer(
|
||||||
this,
|
this,
|
||||||
object : DiffUtil.ItemCallback<ComposeActivity.QueuedMedia>() {
|
object : DiffUtil.ItemCallback<QueuedMedia>() {
|
||||||
override fun areItemsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean {
|
override fun areItemsTheSame(oldItem: QueuedMedia, newItem: QueuedMedia): Boolean {
|
||||||
return oldItem.localId == newItem.localId
|
return oldItem.localId == newItem.localId
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean {
|
override fun areContentsTheSame(oldItem: QueuedMedia, newItem: QueuedMedia): Boolean {
|
||||||
return oldItem == newItem
|
return oldItem == newItem
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -29,6 +29,7 @@ import app.pachli.BuildConfig
|
||||||
import app.pachli.R
|
import app.pachli.R
|
||||||
import app.pachli.components.compose.ComposeActivity.QueuedMedia
|
import app.pachli.components.compose.ComposeActivity.QueuedMedia
|
||||||
import app.pachli.components.compose.MediaUploaderError.PrepareMediaError
|
import app.pachli.components.compose.MediaUploaderError.PrepareMediaError
|
||||||
|
import app.pachli.components.compose.UploadState.Uploaded
|
||||||
import app.pachli.core.common.PachliError
|
import app.pachli.core.common.PachliError
|
||||||
import app.pachli.core.common.string.randomAlphanumericString
|
import app.pachli.core.common.string.randomAlphanumericString
|
||||||
import app.pachli.core.common.util.formatNumber
|
import app.pachli.core.common.util.formatNumber
|
||||||
|
@ -57,6 +58,7 @@ import kotlinx.coroutines.channels.awaitClose
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
|
@ -68,18 +70,6 @@ import okio.sink
|
||||||
import okio.source
|
import okio.source
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
/**
|
|
||||||
* Media that has been fully uploaded to the server and may still be being
|
|
||||||
* processed.
|
|
||||||
*
|
|
||||||
* @property mediaId Server-side identifier for this media item
|
|
||||||
* @property processed True if the server has finished processing this media item
|
|
||||||
*/
|
|
||||||
data class UploadedMedia(
|
|
||||||
val mediaId: String,
|
|
||||||
val processed: Boolean,
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Media that has been prepared for uploading.
|
* Media that has been prepared for uploading.
|
||||||
*
|
*
|
||||||
|
@ -164,44 +154,66 @@ sealed interface MediaUploaderError : PachliError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** [ApiError] wrapper. */
|
/**
|
||||||
|
* An error occurred uploading the media, and there is no remote ID.
|
||||||
|
*
|
||||||
|
* [ApiError] wrapper.
|
||||||
|
*/
|
||||||
@JvmInline
|
@JvmInline
|
||||||
value class UploadMediaError(private val error: ApiError) : MediaUploaderError, PachliError by error
|
value class UploadMediaError(private val error: ApiError) : MediaUploaderError, PachliError by error
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An error occurred updating media that has already been uploaded.
|
||||||
|
*
|
||||||
|
* @param serverId Server's ID for the media
|
||||||
|
*/
|
||||||
|
data class UpdateMediaError(val serverId: String, val error: ApiError) : MediaUploaderError, PachliError by error
|
||||||
|
|
||||||
/** Server did return media with ID [uploadId]. */
|
/** Server did return media with ID [uploadId]. */
|
||||||
data class UploadIdNotFoundError(val uploadId: Int) : MediaUploaderError {
|
data class UploadIdNotFoundError(val uploadId: Int) : MediaUploaderError {
|
||||||
override val resourceId = R.string.error_media_uploader_upload_not_found_fmt
|
override val resourceId = R.string.error_media_uploader_upload_not_found_fmt
|
||||||
override val formatArgs = arrayOf(uploadId.toString())
|
override val formatArgs = arrayOf(uploadId.toString())
|
||||||
override val cause = null
|
override val cause = null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Catch-all for arbitrary throwables */
|
|
||||||
data class ThrowableError(private val throwable: Throwable) : MediaUploaderError {
|
|
||||||
override val resourceId = R.string.error_media_uploader_throwable_fmt
|
|
||||||
override val formatArgs = arrayOf(throwable.localizedMessage ?: "")
|
|
||||||
override val cause = null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Events that happen over the life of a media upload. */
|
/** State of a media upload. */
|
||||||
sealed interface UploadEvent {
|
sealed interface UploadState {
|
||||||
/**
|
/**
|
||||||
* Upload has made progress.
|
* Upload is in progress, but incomplete.
|
||||||
*
|
*
|
||||||
* @property percentage What percent of the file has been uploaded.
|
* @property percentage What percent of the file has been uploaded.
|
||||||
*/
|
*/
|
||||||
data class ProgressEvent(val percentage: Int) : UploadEvent
|
data class Uploading(val percentage: Int) : UploadState
|
||||||
|
|
||||||
/**
|
sealed interface Uploaded : UploadState {
|
||||||
* Upload has finished.
|
val serverId: String
|
||||||
*
|
|
||||||
* @property media The uploaded media
|
/**
|
||||||
*/
|
* Upload has completed, but the server is still processing the media.
|
||||||
data class FinishedEvent(val media: UploadedMedia) : UploadEvent
|
*
|
||||||
|
* @property serverId Server-side identifier for this media item
|
||||||
|
*/
|
||||||
|
data class Processing(override val serverId: String) : UploadState.Uploaded
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload has completed, and the server has processed the media.
|
||||||
|
*
|
||||||
|
* @property serverId Server-side identifier for this media item
|
||||||
|
*/
|
||||||
|
data class Processed(override val serverId: String) : UploadState.Uploaded
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post has been published, editing is impossible.
|
||||||
|
*
|
||||||
|
* @property serverId Server-side identifier for this media item
|
||||||
|
*/
|
||||||
|
data class Published(override val serverId: String) : UploadState.Uploaded
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class UploadData(
|
data class UploadData(
|
||||||
val flow: Flow<Result<UploadEvent, MediaUploaderError>>,
|
val flow: Flow<Result<UploadState, MediaUploaderError>>,
|
||||||
val scope: CoroutineScope,
|
val scope: CoroutineScope,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -231,28 +243,29 @@ class MediaUploader @Inject constructor(
|
||||||
return mostRecentId++
|
return mostRecentId++
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getMediaUploadState(localId: Int): Result<UploadEvent.FinishedEvent, MediaUploaderError> {
|
/**
|
||||||
return uploads[localId]?.flow
|
* Waits for the upload with [localId] to finish (Ok state is one of the
|
||||||
// Can't use filterIsInstance<Ok<UploadEvent.FinishedEvent>> here because the type
|
* [Uploaded][UploadState.Uploaded] types), or return an error.
|
||||||
// inside Ok<...> is erased, so the first Ok<_> result is returned, crashing with a
|
*/
|
||||||
// class cast error if it's a ProgressEvent.
|
suspend fun waitForUploadToFinish(localId: Int): Result<Uploaded, MediaUploaderError> {
|
||||||
// Kotlin doesn't warn about this, see
|
return uploads[localId]?.flow?.filter {
|
||||||
// https://discuss.kotlinlang.org/t/is-as-operators-are-unsafe-for-reified-types/22470
|
it.get() is Uploaded || it.get() == null
|
||||||
?.first { it.get() is UploadEvent.FinishedEvent } as? Ok<UploadEvent.FinishedEvent>
|
}?.first() as? Result<Uploaded, MediaUploaderError>
|
||||||
?: Err(MediaUploaderError.UploadIdNotFoundError(localId))
|
?: Err(MediaUploaderError.UploadIdNotFoundError(localId))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uploads media.
|
* Uploads media.
|
||||||
|
*
|
||||||
* @param media the media to upload
|
* @param media the media to upload
|
||||||
* @param instanceInfo info about the current media to make sure the media gets resized correctly
|
* @param instanceInfo info about the current media to make sure the media gets resized correctly
|
||||||
* @return A Flow emitting upload events.
|
* @return A Flow emitting upload events.
|
||||||
* The Flow is hot, in order to cancel upload or clear resources call [cancelUploadScope].
|
* The Flow is hot, in order to cancel upload or clear resources call [cancelUploadScope].
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
fun uploadMedia(media: QueuedMedia, instanceInfo: InstanceInfo): Flow<Result<UploadEvent, MediaUploaderError>> {
|
fun uploadMedia(media: QueuedMedia, instanceInfo: InstanceInfo): Flow<Result<UploadState, MediaUploaderError>> {
|
||||||
val uploadScope = CoroutineScope(Dispatchers.IO)
|
val uploadScope = CoroutineScope(Dispatchers.IO)
|
||||||
val uploadFlow: Flow<Result<UploadEvent, MediaUploaderError>> = flow {
|
val uploadFlow: Flow<Result<UploadState, MediaUploaderError>> = flow {
|
||||||
if (shouldResizeMedia(media, instanceInfo)) {
|
if (shouldResizeMedia(media, instanceInfo)) {
|
||||||
emit(downsize(media, instanceInfo))
|
emit(downsize(media, instanceInfo))
|
||||||
} else {
|
} else {
|
||||||
|
@ -371,7 +384,7 @@ class MediaUploader @Inject constructor(
|
||||||
|
|
||||||
private val contentResolver = context.contentResolver
|
private val contentResolver = context.contentResolver
|
||||||
|
|
||||||
private suspend fun upload(media: QueuedMedia): Flow<Result<UploadEvent, MediaUploaderError.UploadMediaError>> {
|
private suspend fun upload(media: QueuedMedia): Flow<Result<UploadState, MediaUploaderError.UploadMediaError>> {
|
||||||
return callbackFlow {
|
return callbackFlow {
|
||||||
var mimeType = contentResolver.getType(media.uri)
|
var mimeType = contentResolver.getType(media.uri)
|
||||||
|
|
||||||
|
@ -405,7 +418,7 @@ class MediaUploader @Inject constructor(
|
||||||
media.mediaSize,
|
media.mediaSize,
|
||||||
) { percentage ->
|
) { percentage ->
|
||||||
if (percentage != lastProgress) {
|
if (percentage != lastProgress) {
|
||||||
trySend(Ok(UploadEvent.ProgressEvent(percentage)))
|
trySend(Ok(UploadState.Uploading(percentage)))
|
||||||
}
|
}
|
||||||
lastProgress = percentage
|
lastProgress = percentage
|
||||||
}
|
}
|
||||||
|
@ -424,7 +437,13 @@ class MediaUploader @Inject constructor(
|
||||||
|
|
||||||
val uploadResult = mediaUploadApi.uploadMedia(body, description, focus)
|
val uploadResult = mediaUploadApi.uploadMedia(body, description, focus)
|
||||||
.mapEither(
|
.mapEither(
|
||||||
{ UploadEvent.FinishedEvent(UploadedMedia(it.body.id, it.code == 200)) },
|
{
|
||||||
|
if (it.code == 200) {
|
||||||
|
Uploaded.Processed(it.body.id)
|
||||||
|
} else {
|
||||||
|
Uploaded.Processing(it.body.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
{ MediaUploaderError.UploadMediaError(it) },
|
{ MediaUploaderError.UploadMediaError(it) },
|
||||||
)
|
)
|
||||||
send(uploadResult)
|
send(uploadResult)
|
||||||
|
|
|
@ -68,10 +68,12 @@ class CaptionDialog : DialogFragment() {
|
||||||
input.requestFocus()
|
input.requestFocus()
|
||||||
|
|
||||||
val localId = arguments?.getInt(ARG_LOCAL_ID) ?: error("Missing localId")
|
val localId = arguments?.getInt(ARG_LOCAL_ID) ?: error("Missing localId")
|
||||||
|
val serverId = arguments?.getString(ARG_SERVER_ID)
|
||||||
|
|
||||||
val dialog = AlertDialog.Builder(context)
|
val dialog = AlertDialog.Builder(context)
|
||||||
.setView(binding.root)
|
.setView(binding.root)
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
listener.onUpdateDescription(localId, input.text.toString())
|
listener.onUpdateDescription(localId, serverId, input.text.toString())
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
.create()
|
.create()
|
||||||
|
@ -125,22 +127,25 @@ class CaptionDialog : DialogFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Listener {
|
interface Listener {
|
||||||
fun onUpdateDescription(localId: Int, description: String)
|
fun onUpdateDescription(localId: Int, serverId: String?, description: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val KEY_DESCRIPTION = "app.pachli.KEY_DESCRIPTION"
|
private const val KEY_DESCRIPTION = "app.pachli.KEY_DESCRIPTION"
|
||||||
|
private const val ARG_LOCAL_ID = "app.pachli.ARG_LOCAL_ID"
|
||||||
|
private const val ARG_SERVER_ID = "app.pachli.ARG_SERVER_ID"
|
||||||
private const val ARG_EXISTING_DESCRIPTION = "app.pachli.ARG_EXISTING_DESCRIPTION"
|
private const val ARG_EXISTING_DESCRIPTION = "app.pachli.ARG_EXISTING_DESCRIPTION"
|
||||||
private const val ARG_PREVIEW_URI = "app.pachli.ARG_PREVIEW_URI"
|
private const val ARG_PREVIEW_URI = "app.pachli.ARG_PREVIEW_URI"
|
||||||
private const val ARG_LOCAL_ID = "app.pachli.ARG_LOCAL_ID"
|
|
||||||
|
|
||||||
fun newInstance(
|
fun newInstance(
|
||||||
localId: Int,
|
localId: Int,
|
||||||
|
serverId: String? = null,
|
||||||
existingDescription: String?,
|
existingDescription: String?,
|
||||||
previewUri: Uri,
|
previewUri: Uri,
|
||||||
) = CaptionDialog().apply {
|
) = CaptionDialog().apply {
|
||||||
arguments = bundleOf(
|
arguments = bundleOf(
|
||||||
ARG_LOCAL_ID to localId,
|
ARG_LOCAL_ID to localId,
|
||||||
|
ARG_SERVER_ID to serverId,
|
||||||
ARG_EXISTING_DESCRIPTION to existingDescription,
|
ARG_EXISTING_DESCRIPTION to existingDescription,
|
||||||
ARG_PREVIEW_URI to previewUri,
|
ARG_PREVIEW_URI to previewUri,
|
||||||
)
|
)
|
||||||
|
|
|
@ -24,12 +24,22 @@ import android.graphics.PorterDuff
|
||||||
import android.graphics.PorterDuffXfermode
|
import android.graphics.PorterDuffXfermode
|
||||||
import android.graphics.RectF
|
import android.graphics.RectF
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
|
import androidx.annotation.OptIn
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
import app.pachli.R
|
import app.pachli.R
|
||||||
|
import app.pachli.components.compose.MediaUploaderError
|
||||||
|
import app.pachli.components.compose.UploadState
|
||||||
import app.pachli.core.designsystem.R as DR
|
import app.pachli.core.designsystem.R as DR
|
||||||
|
import app.pachli.core.ui.makeIcon
|
||||||
import app.pachli.view.MediaPreviewImageView
|
import app.pachli.view.MediaPreviewImageView
|
||||||
import at.connyduck.sparkbutton.helpers.Utils
|
import at.connyduck.sparkbutton.helpers.Utils
|
||||||
|
import com.github.michaelbull.result.Ok
|
||||||
|
import com.github.michaelbull.result.Result
|
||||||
|
import com.github.michaelbull.result.onFailure
|
||||||
|
import com.github.michaelbull.result.onSuccess
|
||||||
|
import com.google.android.material.badge.ExperimentalBadgeUtils
|
||||||
import com.google.android.material.color.MaterialColors
|
import com.google.android.material.color.MaterialColors
|
||||||
|
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||||
|
|
||||||
class ProgressImageView
|
class ProgressImageView
|
||||||
@JvmOverloads constructor(
|
@JvmOverloads constructor(
|
||||||
|
@ -37,7 +47,7 @@ class ProgressImageView
|
||||||
attrs: AttributeSet? = null,
|
attrs: AttributeSet? = null,
|
||||||
defStyleAttr: Int = 0,
|
defStyleAttr: Int = 0,
|
||||||
) : MediaPreviewImageView(context, attrs, defStyleAttr) {
|
) : MediaPreviewImageView(context, attrs, defStyleAttr) {
|
||||||
private var progress = -1
|
private var result: Result<UploadState, MediaUploaderError> = Ok(UploadState.Uploading(0))
|
||||||
private val progressRect = RectF()
|
private val progressRect = RectF()
|
||||||
private val biggerRect = RectF()
|
private val biggerRect = RectF()
|
||||||
private val circlePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
private val circlePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
@ -60,14 +70,15 @@ class ProgressImageView
|
||||||
}
|
}
|
||||||
private val circleRadius = Utils.dpToPx(context, 14)
|
private val circleRadius = Utils.dpToPx(context, 14)
|
||||||
private val circleMargin = Utils.dpToPx(context, 14)
|
private val circleMargin = Utils.dpToPx(context, 14)
|
||||||
|
private val uploadErrorRadius = Utils.dpToPx(context, 24)
|
||||||
|
|
||||||
fun setProgress(progress: Int) {
|
private val uploadErrorDrawable = makeIcon(context, GoogleMaterial.Icon.gmd_error, 48).apply {
|
||||||
this.progress = progress
|
setTint(Color.WHITE)
|
||||||
if (progress != -1) {
|
}
|
||||||
setColorFilter(Color.rgb(123, 123, 123), PorterDuff.Mode.MULTIPLY)
|
|
||||||
} else {
|
@OptIn(ExperimentalBadgeUtils::class)
|
||||||
clearColorFilter()
|
fun setResult(result: Result<UploadState, MediaUploaderError>) {
|
||||||
}
|
this.result = result
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,6 +93,24 @@ class ProgressImageView
|
||||||
|
|
||||||
override fun onDraw(canvas: Canvas) {
|
override fun onDraw(canvas: Canvas) {
|
||||||
super.onDraw(canvas)
|
super.onDraw(canvas)
|
||||||
|
|
||||||
|
result.onSuccess { value ->
|
||||||
|
val percentage = when (value) {
|
||||||
|
is UploadState.Uploading -> value.percentage
|
||||||
|
else -> -1
|
||||||
|
}
|
||||||
|
onDrawSuccess(canvas, percentage)
|
||||||
|
}.onFailure { error ->
|
||||||
|
onDrawError(canvas)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onDrawSuccess(canvas: Canvas, progress: Int) {
|
||||||
|
clearColorFilter()
|
||||||
|
if (progress != -1) {
|
||||||
|
setColorFilter(Color.rgb(123, 123, 123), PorterDuff.Mode.MULTIPLY)
|
||||||
|
}
|
||||||
|
|
||||||
val angle = progress / 100f * 360 - 90
|
val angle = progress / 100f * 360 - 90
|
||||||
val halfWidth = width / 2f
|
val halfWidth = width / 2f
|
||||||
val halfHeight = height / 2f
|
val halfHeight = height / 2f
|
||||||
|
@ -107,4 +136,18 @@ class ProgressImageView
|
||||||
)
|
)
|
||||||
captionDrawable.draw(canvas)
|
captionDrawable.draw(canvas)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onDrawError(canvas: Canvas) {
|
||||||
|
setColorFilter(
|
||||||
|
MaterialColors.getColor(this, androidx.appcompat.R.attr.colorError),
|
||||||
|
PorterDuff.Mode.DARKEN,
|
||||||
|
)
|
||||||
|
uploadErrorDrawable.setBounds(
|
||||||
|
(width / 2) - uploadErrorRadius,
|
||||||
|
(height / 2) - uploadErrorRadius,
|
||||||
|
(width / 2) + uploadErrorRadius,
|
||||||
|
(height / 2) + uploadErrorRadius,
|
||||||
|
)
|
||||||
|
uploadErrorDrawable.draw(canvas)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,8 @@ import app.pachli.core.network.model.Status
|
||||||
import app.pachli.core.network.retrofit.MastodonApi
|
import app.pachli.core.network.retrofit.MastodonApi
|
||||||
import at.connyduck.calladapter.networkresult.fold
|
import at.connyduck.calladapter.networkresult.fold
|
||||||
import com.github.michaelbull.result.getOrElse
|
import com.github.michaelbull.result.getOrElse
|
||||||
|
import com.github.michaelbull.result.onFailure
|
||||||
|
import com.github.michaelbull.result.onSuccess
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
@ -145,14 +147,14 @@ class SendStatusService : Service() {
|
||||||
// first, wait for media uploads to finish
|
// first, wait for media uploads to finish
|
||||||
val media = statusToSend.media.map { mediaItem ->
|
val media = statusToSend.media.map { mediaItem ->
|
||||||
if (mediaItem.id == null) {
|
if (mediaItem.id == null) {
|
||||||
val uploadState = mediaUploader.getMediaUploadState(mediaItem.localId)
|
val uploadState = mediaUploader.waitForUploadToFinish(mediaItem.localId)
|
||||||
val media = uploadState.getOrElse {
|
val media = uploadState.getOrElse {
|
||||||
Timber.w("failed uploading media: %s", it.fmt(this@SendStatusService))
|
Timber.w("failed uploading media: %s", it.fmt(this@SendStatusService))
|
||||||
failSending(statusId)
|
failSending(statusId)
|
||||||
stopSelfWhenDone()
|
stopSelfWhenDone()
|
||||||
return@launch
|
return@launch
|
||||||
}.media
|
}
|
||||||
mediaItem.copy(id = media.mediaId, processed = media.processed)
|
mediaItem.copy(id = media.serverId)
|
||||||
} else {
|
} else {
|
||||||
mediaItem
|
mediaItem
|
||||||
}
|
}
|
||||||
|
@ -165,15 +167,13 @@ class SendStatusService : Service() {
|
||||||
delay(1000L * mediaCheckRetries)
|
delay(1000L * mediaCheckRetries)
|
||||||
media.forEach { mediaItem ->
|
media.forEach { mediaItem ->
|
||||||
if (!mediaItem.processed) {
|
if (!mediaItem.processed) {
|
||||||
when (mastodonApi.getMedia(mediaItem.id!!).code()) {
|
mastodonApi.getMedia(mediaItem.id!!)
|
||||||
200 -> mediaItem.processed = true // success
|
.onSuccess { mediaItem.processed = it.code == 200 }
|
||||||
206 -> { } // media is still being processed, continue checking
|
.onFailure {
|
||||||
else -> { // some kind of server error, retrying probably doesn't make sense
|
|
||||||
failSending(statusId)
|
failSending(statusId)
|
||||||
stopSelfWhenDone()
|
stopSelfWhenDone()
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mediaCheckRetries++
|
mediaCheckRetries++
|
||||||
|
@ -190,13 +190,11 @@ class SendStatusService : Service() {
|
||||||
media.forEach { mediaItem ->
|
media.forEach { mediaItem ->
|
||||||
if (mediaItem.processed && (mediaItem.description != null || mediaItem.focus != null)) {
|
if (mediaItem.processed && (mediaItem.description != null || mediaItem.focus != null)) {
|
||||||
mastodonApi.updateMedia(mediaItem.id!!, mediaItem.description, mediaItem.focus?.toMastodonApiString())
|
mastodonApi.updateMedia(mediaItem.id!!, mediaItem.description, mediaItem.focus?.toMastodonApiString())
|
||||||
.fold({
|
.onFailure { error ->
|
||||||
}, { throwable ->
|
Timber.w("failed to update media on status send: %s", error)
|
||||||
Timber.w(throwable, "failed to update media on status send")
|
failOrRetry(error.throwable, statusId)
|
||||||
failOrRetry(throwable, statusId)
|
|
||||||
|
|
||||||
return@launch
|
return@launch
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -590,7 +590,6 @@
|
||||||
<string name="ui_error_bookmark_fmt">%1$s :فشل إجراء إضافة المنشور إلى الفواصل المرجعية</string>
|
<string name="ui_error_bookmark_fmt">%1$s :فشل إجراء إضافة المنشور إلى الفواصل المرجعية</string>
|
||||||
<string name="notification_unknown_name">غير معروف</string>
|
<string name="notification_unknown_name">غير معروف</string>
|
||||||
<string name="pref_update_notification_frequency_always">دائما</string>
|
<string name="pref_update_notification_frequency_always">دائما</string>
|
||||||
<string name="error_media_upload_sending_fmt">اخفق التحميل: %s</string>
|
|
||||||
<string name="pref_update_notification_frequency_never">أبدًا</string>
|
<string name="pref_update_notification_frequency_never">أبدًا</string>
|
||||||
<string name="filter_keyword_display_format">%s (الكلمةكاملة)</string>
|
<string name="filter_keyword_display_format">%s (الكلمةكاملة)</string>
|
||||||
<string name="filter_keyword_addition_title">إضافة كلمة مفتاحية</string>
|
<string name="filter_keyword_addition_title">إضافة كلمة مفتاحية</string>
|
||||||
|
|
|
@ -612,8 +612,7 @@
|
||||||
\nHwn yw <a href="https://github.com/mastodon/mastodon/issues/25398">mater Mastodon #25398</a>.</string>
|
\nHwn yw <a href="https://github.com/mastodon/mastodon/issues/25398">mater Mastodon #25398</a>.</string>
|
||||||
<string name="load_newest_notifications">Llwytho hysbysiadau diweddaraf</string>
|
<string name="load_newest_notifications">Llwytho hysbysiadau diweddaraf</string>
|
||||||
<string name="compose_delete_draft">Dileu\'r drafft\?</string>
|
<string name="compose_delete_draft">Dileu\'r drafft\?</string>
|
||||||
<string name="error_media_upload_sending_fmt">Methodd y lanlwytho: %s</string>
|
|
||||||
<string name="error_media_playback">Methodd chwarae: %s</string>
|
<string name="error_media_playback">Methodd chwarae: %s</string>
|
||||||
<string name="dialog_delete_filter_positive_action">Dileu</string>
|
<string name="dialog_delete_filter_positive_action">Dileu</string>
|
||||||
<string name="dialog_delete_filter_text">Dileu\'r hidlydd \'%1$s\'\?</string>
|
<string name="dialog_delete_filter_text">Dileu\'r hidlydd \'%1$s\'\?</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -568,7 +568,6 @@
|
||||||
<string name="error_missing_edits">Deinem Server ist bekannt, dass dieser Beitrag bearbeitet wurde. Allerdings besitzt er keine Kopien der Änderungen, weshalb diese nicht angezeigt werden können.
|
<string name="error_missing_edits">Deinem Server ist bekannt, dass dieser Beitrag bearbeitet wurde. Allerdings besitzt er keine Kopien der Änderungen, weshalb diese nicht angezeigt werden können.
|
||||||
\n
|
\n
|
||||||
\nHierbei handelt es sich um <a href="https://github.com/mastodon/mastodon/issues/25398">Mastodon Issue #25398</a>.</string>
|
\nHierbei handelt es sich um <a href="https://github.com/mastodon/mastodon/issues/25398">Mastodon Issue #25398</a>.</string>
|
||||||
<string name="error_media_upload_sending_fmt">Das Hochladen ist fehlgeschlagen: %s</string>
|
|
||||||
<string name="ui_error_translate_status_fmt">Übersetzung fehlgeschlagen: %1$s</string>
|
<string name="ui_error_translate_status_fmt">Übersetzung fehlgeschlagen: %1$s</string>
|
||||||
<string name="update_dialog_negative">Erinnere mich nie</string>
|
<string name="update_dialog_negative">Erinnere mich nie</string>
|
||||||
<string name="translating">Übersetze…</string>
|
<string name="translating">Übersetze…</string>
|
||||||
|
@ -603,4 +602,4 @@
|
||||||
<string name="reaction_name_and_count">%1$s %2$d</string>
|
<string name="reaction_name_and_count">%1$s %2$d</string>
|
||||||
<string name="announcement_date">%1$s %2$s</string>
|
<string name="announcement_date">%1$s %2$s</string>
|
||||||
<string name="announcement_date_updated">(Aktualisiert: %1$s)</string>
|
<string name="announcement_date_updated">(Aktualisiert: %1$s)</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -93,7 +93,6 @@
|
||||||
<string name="dialog_redraft_post_warning">Διαγραφή και αναδιατύπωση αυτής της δημοσίευσης;</string>
|
<string name="dialog_redraft_post_warning">Διαγραφή και αναδιατύπωση αυτής της δημοσίευσης;</string>
|
||||||
<string name="error_following_hashtag_format">Σφάλμα ακολουθίας #%s</string>
|
<string name="error_following_hashtag_format">Σφάλμα ακολουθίας #%s</string>
|
||||||
<string name="error_unfollowing_hashtag_format">Σφάλμα μη-ακολουθίας #%s</string>
|
<string name="error_unfollowing_hashtag_format">Σφάλμα μη-ακολουθίας #%s</string>
|
||||||
<string name="error_media_upload_sending_fmt">Το ανέβασμα απέτυχε: %s</string>
|
|
||||||
<string name="error_media_upload_sending">Το ανέβασμα αρχείου απέτυχε.</string>
|
<string name="error_media_upload_sending">Το ανέβασμα αρχείου απέτυχε.</string>
|
||||||
<string name="title_home">Αρχική Σελίδα</string>
|
<string name="title_home">Αρχική Σελίδα</string>
|
||||||
<string name="error_sender_account_gone">Σφάλμα αποστολής ανάρτησης.</string>
|
<string name="error_sender_account_gone">Σφάλμα αποστολής ανάρτησης.</string>
|
||||||
|
|
|
@ -59,7 +59,6 @@
|
||||||
<string name="error_following_hashtags_unsupported">This instance does not support following hashtags.</string>
|
<string name="error_following_hashtags_unsupported">This instance does not support following hashtags.</string>
|
||||||
<string name="title_edits">Edits</string>
|
<string name="title_edits">Edits</string>
|
||||||
<string name="title_followed_hashtags">Followed hashtags</string>
|
<string name="title_followed_hashtags">Followed hashtags</string>
|
||||||
<string name="error_media_upload_sending_fmt">The upload failed: %s</string>
|
|
||||||
<string name="title_migration_relogin">Re-login for push notifications</string>
|
<string name="title_migration_relogin">Re-login for push notifications</string>
|
||||||
<string name="title_drafts">Drafts</string>
|
<string name="title_drafts">Drafts</string>
|
||||||
<string name="title_tab_public_trending_statuses">Posts</string>
|
<string name="title_tab_public_trending_statuses">Posts</string>
|
||||||
|
@ -69,4 +68,4 @@
|
||||||
<string name="title_tab_public_trending_hashtags">Hashtags</string>
|
<string name="title_tab_public_trending_hashtags">Hashtags</string>
|
||||||
<string name="error_muting_hashtag_format">Error muting #%s</string>
|
<string name="error_muting_hashtag_format">Error muting #%s</string>
|
||||||
<string name="title_scheduled_posts">Scheduled posts</string>
|
<string name="title_scheduled_posts">Scheduled posts</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -592,7 +592,6 @@
|
||||||
<string name="load_newest_statuses">Cargar las publicaciones más nuevas</string>
|
<string name="load_newest_statuses">Cargar las publicaciones más nuevas</string>
|
||||||
<string name="dialog_save_profile_changes_message">¿Quieres guardar tus cambios de perfil?</string>
|
<string name="dialog_save_profile_changes_message">¿Quieres guardar tus cambios de perfil?</string>
|
||||||
<string name="pref_update_notification_frequency_always">Siempre</string>
|
<string name="pref_update_notification_frequency_always">Siempre</string>
|
||||||
<string name="error_media_upload_sending_fmt">La carga falló: %s</string>
|
|
||||||
<string name="pref_update_notification_frequency_never">Nunca</string>
|
<string name="pref_update_notification_frequency_never">Nunca</string>
|
||||||
<string name="pref_title_show_self_boosts_description">Alguien impulsa su propia publicación</string>
|
<string name="pref_title_show_self_boosts_description">Alguien impulsa su propia publicación</string>
|
||||||
<string name="ui_error_filter_v1_load_fmt">La carga de filtros falló: %1$s</string>
|
<string name="ui_error_filter_v1_load_fmt">La carga de filtros falló: %1$s</string>
|
||||||
|
@ -655,7 +654,6 @@
|
||||||
<string name="compose_warn_language_dialog_fmt">El idioma de la publicación está configurado en %1$s, pero es posible que la hayas escrito en %2$s.</string>
|
<string name="compose_warn_language_dialog_fmt">El idioma de la publicación está configurado en %1$s, pero es posible que la hayas escrito en %2$s.</string>
|
||||||
<string name="error_prepare_media_unsupported_mime_type_fmt">el servidor no es compatible con el tipo de archivo: %1$s</string>
|
<string name="error_prepare_media_unsupported_mime_type_fmt">el servidor no es compatible con el tipo de archivo: %1$s</string>
|
||||||
<string name="error_pick_media_fmt">No se pudo adjuntar el archivo a la publicación: %1$s</string>
|
<string name="error_pick_media_fmt">No se pudo adjuntar el archivo a la publicación: %1$s</string>
|
||||||
<string name="error_media_uploader_throwable_fmt">%1$s</string>
|
|
||||||
<string name="error_prepare_media_content_resolver_missing_path_fmt">al agente de resolución de contenido le faltó una ruta: %1$s</string>
|
<string name="error_prepare_media_content_resolver_missing_path_fmt">al agente de resolución de contenido le faltó una ruta: %1$s</string>
|
||||||
<string name="error_prepare_media_content_resolver_unsupported_scheme_fmt">el agente de resolución de contenido tiene un esquema no compatible: %1$s</string>
|
<string name="error_prepare_media_content_resolver_unsupported_scheme_fmt">el agente de resolución de contenido tiene un esquema no compatible: %1$s</string>
|
||||||
<string name="error_prepare_media_file_is_too_large_fmt">el tamaño del archivo es %1$s, el máximo permitido es %2$s</string>
|
<string name="error_prepare_media_file_is_too_large_fmt">el tamaño del archivo es %1$s, el máximo permitido es %2$s</string>
|
||||||
|
@ -791,4 +789,4 @@
|
||||||
<string name="conversation_0_recipients">Ningún participante más</string>
|
<string name="conversation_0_recipients">Ningún participante más</string>
|
||||||
<string name="pref_title_confirm_status_language">Revisar idioma de la publicación antes de publicar</string>
|
<string name="pref_title_confirm_status_language">Revisar idioma de la publicación antes de publicar</string>
|
||||||
<string name="compose_warn_language_dialog_accept_and_dont_ask_fmt">Publicar como está (%1$s) y no volver a preguntar</string>
|
<string name="compose_warn_language_dialog_accept_and_dont_ask_fmt">Publicar como está (%1$s) y no volver a preguntar</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -568,8 +568,7 @@
|
||||||
<string name="notification_listenable_worker_description">آگاهیها هنگامی که تاسکی در پسزمینه کار میکند</string>
|
<string name="notification_listenable_worker_description">آگاهیها هنگامی که تاسکی در پسزمینه کار میکند</string>
|
||||||
<string name="notification_notification_worker">واکشی آگاهیها…</string>
|
<string name="notification_notification_worker">واکشی آگاهیها…</string>
|
||||||
<string name="notification_prune_cache">نگهداری انباره…</string>
|
<string name="notification_prune_cache">نگهداری انباره…</string>
|
||||||
<string name="error_media_upload_sending_fmt">بارگذاری شکست خورد: %s</string>
|
|
||||||
<string name="error_media_playback">پخش شکست خورد: %s</string>
|
<string name="error_media_playback">پخش شکست خورد: %s</string>
|
||||||
<string name="dialog_delete_filter_positive_action">حذف</string>
|
<string name="dialog_delete_filter_positive_action">حذف</string>
|
||||||
<string name="dialog_delete_filter_text">«%1$s» حذف پالایهٔ ؟</string>
|
<string name="dialog_delete_filter_text">«%1$s» حذف پالایهٔ ؟</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -300,7 +300,6 @@
|
||||||
<string name="confirmation_unblocked">Käyttäjän esto poistettu</string>
|
<string name="confirmation_unblocked">Käyttäjän esto poistettu</string>
|
||||||
<string name="title_edits">Muokkaukset</string>
|
<string name="title_edits">Muokkaukset</string>
|
||||||
<string name="title_followed_hashtags">Seuratut aihetunnisteet</string>
|
<string name="title_followed_hashtags">Seuratut aihetunnisteet</string>
|
||||||
<string name="error_media_upload_sending_fmt">Lataus epäonnistui: %s</string>
|
|
||||||
<string name="add_account_description">Lisää uusi Mastodon-tili</string>
|
<string name="add_account_description">Lisää uusi Mastodon-tili</string>
|
||||||
<string name="dialog_title_finishing_media_upload">Mediatiedoston latausta viimeistellään</string>
|
<string name="dialog_title_finishing_media_upload">Mediatiedoston latausta viimeistellään</string>
|
||||||
<string name="pref_title_show_self_boosts_description">Joku tehostaa omaa julkaisuaan</string>
|
<string name="pref_title_show_self_boosts_description">Joku tehostaa omaa julkaisuaan</string>
|
||||||
|
@ -631,7 +630,6 @@
|
||||||
<string name="compose_warn_language_dialog_change_language_fmt">Merkitse kieleksi %1$s ja julkaise</string>
|
<string name="compose_warn_language_dialog_change_language_fmt">Merkitse kieleksi %1$s ja julkaise</string>
|
||||||
<string name="compose_warn_language_dialog_accept_language_fmt">Julkaise muuttamattomana (%1$s)</string>
|
<string name="compose_warn_language_dialog_accept_language_fmt">Julkaise muuttamattomana (%1$s)</string>
|
||||||
<string name="error_media_uploader_upload_not_found_fmt">medialatausta nimeltä %1$s ei löydy</string>
|
<string name="error_media_uploader_upload_not_found_fmt">medialatausta nimeltä %1$s ei löydy</string>
|
||||||
<string name="error_media_uploader_throwable_fmt">%1$s</string>
|
|
||||||
<string name="error_prepare_media_content_resolver_missing_path_fmt">sisällönselvittäjä-URI:ltä puuttui polku: %1$s</string>
|
<string name="error_prepare_media_content_resolver_missing_path_fmt">sisällönselvittäjä-URI:ltä puuttui polku: %1$s</string>
|
||||||
<string name="error_prepare_media_io_fmt">%1$s</string>
|
<string name="error_prepare_media_io_fmt">%1$s</string>
|
||||||
<string name="error_prepare_media_file_is_too_large_fmt">tiedoston koko on %1$s, suurin sallittu on %2$s</string>
|
<string name="error_prepare_media_file_is_too_large_fmt">tiedoston koko on %1$s, suurin sallittu on %2$s</string>
|
||||||
|
@ -776,4 +774,4 @@
|
||||||
<string name="pref_title_confirm_status_language">Tarkasta julkaisun kieli ennen julkaisemista</string>
|
<string name="pref_title_confirm_status_language">Tarkasta julkaisun kieli ennen julkaisemista</string>
|
||||||
<string name="action_copy_item">Kopioi kohde</string>
|
<string name="action_copy_item">Kopioi kohde</string>
|
||||||
<string name="item_copied">Teksti kopioitu</string>
|
<string name="item_copied">Teksti kopioitu</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -572,7 +572,6 @@
|
||||||
<string name="filter_edit_keyword_title">Modifier mot-clé</string>
|
<string name="filter_edit_keyword_title">Modifier mot-clé</string>
|
||||||
<string name="filter_description_format">%s : %s</string>
|
<string name="filter_description_format">%s : %s</string>
|
||||||
<string name="action_translate_undo">Annuler la traduction</string>
|
<string name="action_translate_undo">Annuler la traduction</string>
|
||||||
<string name="error_media_upload_sending_fmt">Le téléversement a échoué : %s</string>
|
|
||||||
<string name="title_public_trending_statuses">Publications en tendance</string>
|
<string name="title_public_trending_statuses">Publications en tendance</string>
|
||||||
<string name="title_tab_public_trending_hashtags">Hashtags</string>
|
<string name="title_tab_public_trending_hashtags">Hashtags</string>
|
||||||
<string name="action_translate">Traduire</string>
|
<string name="action_translate">Traduire</string>
|
||||||
|
@ -607,4 +606,4 @@
|
||||||
<string name="notification_notification_worker">Récupération des notifications …</string>
|
<string name="notification_notification_worker">Récupération des notifications …</string>
|
||||||
<string name="notification_prune_cache">Maintenance du cache …</string>
|
<string name="notification_prune_cache">Maintenance du cache …</string>
|
||||||
<string name="announcement_date">%1$s %2$s</string>
|
<string name="announcement_date">%1$s %2$s</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -583,5 +583,4 @@
|
||||||
<string name="notification_listenable_worker_description">Brathan nuair a bhios Pachli ag obair sa chùlaibh</string>
|
<string name="notification_listenable_worker_description">Brathan nuair a bhios Pachli ag obair sa chùlaibh</string>
|
||||||
<string name="notification_notification_worker">A’ faighinn nam brathan…</string>
|
<string name="notification_notification_worker">A’ faighinn nam brathan…</string>
|
||||||
<string name="notification_prune_cache">Obair-ghlèidhidh air an tasgadan…</string>
|
<string name="notification_prune_cache">Obair-ghlèidhidh air an tasgadan…</string>
|
||||||
<string name="error_media_upload_sending_fmt">Dh’fhàillig leis an luchdadh suas: %s</string>
|
</resources>
|
||||||
</resources>
|
|
||||||
|
|
|
@ -561,7 +561,6 @@
|
||||||
<string name="notification_listenable_worker_description">Notificacións cando Pachli está a funcionar en segundo plano</string>
|
<string name="notification_listenable_worker_description">Notificacións cando Pachli está a funcionar en segundo plano</string>
|
||||||
<string name="notification_notification_worker">Obtendo as notificacións…</string>
|
<string name="notification_notification_worker">Obtendo as notificacións…</string>
|
||||||
<string name="notification_prune_cache">Mantemento da caché…</string>
|
<string name="notification_prune_cache">Mantemento da caché…</string>
|
||||||
<string name="error_media_upload_sending_fmt">Fallou a subida: %s</string>
|
|
||||||
<string name="dialog_delete_filter_positive_action">Eliminar</string>
|
<string name="dialog_delete_filter_positive_action">Eliminar</string>
|
||||||
<string name="dialog_delete_filter_text">Eliminar o filtro \'%1$s\'\?</string>
|
<string name="dialog_delete_filter_text">Eliminar o filtro \'%1$s\'\?</string>
|
||||||
<string name="pref_title_show_self_boosts">Mostrar autopromocións</string>
|
<string name="pref_title_show_self_boosts">Mostrar autopromocións</string>
|
||||||
|
@ -628,7 +627,6 @@
|
||||||
<string name="compose_warn_language_dialog_change_language_fmt">Cambiar o idioma a %1$s e publicar</string>
|
<string name="compose_warn_language_dialog_change_language_fmt">Cambiar o idioma a %1$s e publicar</string>
|
||||||
<string name="error_media_uploader_upload_not_found_fmt">non se atopou o multimedia subido con ID %1$s</string>
|
<string name="error_media_uploader_upload_not_found_fmt">non se atopou o multimedia subido con ID %1$s</string>
|
||||||
<string name="compose_warn_language_dialog_accept_language_fmt">Publicar tal como está (%1$s)</string>
|
<string name="compose_warn_language_dialog_accept_language_fmt">Publicar tal como está (%1$s)</string>
|
||||||
<string name="error_media_uploader_throwable_fmt">%1$s</string>
|
|
||||||
<string name="error_prepare_media_content_resolver_missing_path_fmt">ao resolutor da URI do contido fáltalle unha ruta: %1$s</string>
|
<string name="error_prepare_media_content_resolver_missing_path_fmt">ao resolutor da URI do contido fáltalle unha ruta: %1$s</string>
|
||||||
<string name="error_prepare_media_io_fmt">%1$s</string>
|
<string name="error_prepare_media_io_fmt">%1$s</string>
|
||||||
<string name="error_prepare_media_content_resolver_unsupported_scheme_fmt">o resolutor da URI do cotido non é compatible co esquema: %1$s</string>
|
<string name="error_prepare_media_content_resolver_unsupported_scheme_fmt">o resolutor da URI do cotido non é compatible co esquema: %1$s</string>
|
||||||
|
@ -770,4 +768,4 @@
|
||||||
<string name="pref_notification_fetch_ok_timestamp_fmt">✔ hai %1$s @ %2$s</string>
|
<string name="pref_notification_fetch_ok_timestamp_fmt">✔ hai %1$s @ %2$s</string>
|
||||||
<string name="pref_notification_fetch_err_timestamp_fmt">✖ hai %1$s @ %2$s</string>
|
<string name="pref_notification_fetch_err_timestamp_fmt">✖ hai %1$s @ %2$s</string>
|
||||||
<string name="pref_notification_fetch_needs_push">A conta non ten o método «push». Pechar a sesión e volver acceder podería arranxalo.</string>
|
<string name="pref_notification_fetch_needs_push">A conta non ten o método «push». Pechar a sesión e volver acceder podería arranxalo.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -210,7 +210,6 @@
|
||||||
<string name="title_public_trending_statuses">Postingan trending</string>
|
<string name="title_public_trending_statuses">Postingan trending</string>
|
||||||
<string name="title_edits">Sunting</string>
|
<string name="title_edits">Sunting</string>
|
||||||
<string name="title_followed_hashtags">Tagar yang diikuti</string>
|
<string name="title_followed_hashtags">Tagar yang diikuti</string>
|
||||||
<string name="error_media_upload_sending_fmt">Upload gagal: %s</string>
|
|
||||||
<string name="title_tab_public_trending_statuses">Postingan</string>
|
<string name="title_tab_public_trending_statuses">Postingan</string>
|
||||||
<string name="title_tab_public_trending_hashtags">Tagar</string>
|
<string name="title_tab_public_trending_hashtags">Tagar</string>
|
||||||
<string name="notification_header_report_format">%s dilaporkan %s</string>
|
<string name="notification_header_report_format">%s dilaporkan %s</string>
|
||||||
|
|
|
@ -606,9 +606,8 @@
|
||||||
<string name="update_dialog_neutral">Non ricordarmelo per questa versione</string>
|
<string name="update_dialog_neutral">Non ricordarmelo per questa versione</string>
|
||||||
<string name="pref_title_update_settings">Aggiornamenti Software</string>
|
<string name="pref_title_update_settings">Aggiornamenti Software</string>
|
||||||
<string name="pref_update_notification_frequency_always">Sempre</string>
|
<string name="pref_update_notification_frequency_always">Sempre</string>
|
||||||
<string name="error_media_upload_sending_fmt">Il caricamento è fallito: %s</string>
|
|
||||||
<string name="pref_update_notification_frequency_never">Mai</string>
|
<string name="pref_update_notification_frequency_never">Mai</string>
|
||||||
<string name="title_tab_public_trending_statuses">Post</string>
|
<string name="title_tab_public_trending_statuses">Post</string>
|
||||||
<string name="pref_update_notification_frequency_once_per_version">Una volta per versione</string>
|
<string name="pref_update_notification_frequency_once_per_version">Una volta per versione</string>
|
||||||
<string name="update_dialog_title">Un aggiornamento è disponibile</string>
|
<string name="update_dialog_title">Un aggiornamento è disponibile</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -551,7 +551,6 @@
|
||||||
<string name="load_newest_notifications">最新の通知を読み込む</string>
|
<string name="load_newest_notifications">最新の通知を読み込む</string>
|
||||||
<string name="compose_delete_draft">下書きを削除しますか?</string>
|
<string name="compose_delete_draft">下書きを削除しますか?</string>
|
||||||
<string name="error_list_load">リストを読み込む際のエラー</string>
|
<string name="error_list_load">リストを読み込む際のエラー</string>
|
||||||
<string name="error_media_upload_sending_fmt">アップロードに失敗しました: %s</string>
|
|
||||||
<string name="error_missing_edits">あなたのサーバーは、この投稿が変更されたことを把握していますが、編集履歴のコピーを備えていないので、表示できません。
|
<string name="error_missing_edits">あなたのサーバーは、この投稿が変更されたことを把握していますが、編集履歴のコピーを備えていないので、表示できません。
|
||||||
\n
|
\n
|
||||||
\nこれは<a href="https://github.com/mastodon/mastodon/issues/25398">Mastodonのissue #25398</a>です。</string>
|
\nこれは<a href="https://github.com/mastodon/mastodon/issues/25398">Mastodonのissue #25398</a>です。</string>
|
||||||
|
@ -595,4 +594,4 @@
|
||||||
<string name="action_translate">翻訳</string>
|
<string name="action_translate">翻訳</string>
|
||||||
<string name="update_dialog_title">アップデート可能です</string>
|
<string name="update_dialog_title">アップデート可能です</string>
|
||||||
<string name="action_translate_undo">翻訳を元に戻す</string>
|
<string name="action_translate_undo">翻訳を元に戻す</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -277,7 +277,6 @@
|
||||||
<string name="duration_indefinite">War talast</string>
|
<string name="duration_indefinite">War talast</string>
|
||||||
<string name="action_subscribe_account">Ḍfeṛ</string>
|
<string name="action_subscribe_account">Ḍfeṛ</string>
|
||||||
<string name="report_category_other">Wayeḍ</string>
|
<string name="report_category_other">Wayeḍ</string>
|
||||||
<string name="error_media_uploader_throwable_fmt">%1$s</string>
|
|
||||||
<string name="search_operator_attachment_dialog_video_label">Tavidyutt</string>
|
<string name="search_operator_attachment_dialog_video_label">Tavidyutt</string>
|
||||||
<string name="search_operator_language_checked_fmt">%1$s</string>
|
<string name="search_operator_language_checked_fmt">%1$s</string>
|
||||||
<string name="post_media_image">Tugna</string>
|
<string name="post_media_image">Tugna</string>
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
<string name="error_media_upload_type">ဤဖိုင်အမျိုးအစားကိုမတင်နိုင်ပါ။</string>
|
<string name="error_media_upload_type">ဤဖိုင်အမျိုးအစားကိုမတင်နိုင်ပါ။</string>
|
||||||
<string name="error_media_upload_opening">ဤဖိုင်ကိုမဖွင့်နိုင်ပါ။</string>
|
<string name="error_media_upload_opening">ဤဖိုင်ကိုမဖွင့်နိုင်ပါ။</string>
|
||||||
<string name="error_media_upload_image_or_video">ရုပ်ပုံနှင့်ဗီဒီယိုများအား ပိုစ့်တစ်ခုတည်းတွင် ယှဥ်တွဲမတင်နိုင်ပါ။</string>
|
<string name="error_media_upload_image_or_video">ရုပ်ပုံနှင့်ဗီဒီယိုများအား ပိုစ့်တစ်ခုတည်းတွင် ယှဥ်တွဲမတင်နိုင်ပါ။</string>
|
||||||
<string name="error_media_upload_sending_fmt">တင်ခြင်းမအောင်မြင်ပါ။: %s</string>
|
|
||||||
<string name="error_sender_account_gone">ပိုစ့်တင်ရာ၌ချို့ယွင်းမှု။</string>
|
<string name="error_sender_account_gone">ပိုစ့်တင်ရာ၌ချို့ယွင်းမှု။</string>
|
||||||
<string name="error_following_hashtag_format">စောင့်ကြည့်ရာ၌ ချို့ယွင်းမှု #%s</string>
|
<string name="error_following_hashtag_format">စောင့်ကြည့်ရာ၌ ချို့ယွင်းမှု #%s</string>
|
||||||
<string name="error_muting_hashtag_format">Mute လုပ်ရာ၌ ချို့ယွင်းချက် #%s</string>
|
<string name="error_muting_hashtag_format">Mute လုပ်ရာ၌ ချို့ယွင်းချက် #%s</string>
|
||||||
|
@ -103,4 +102,4 @@
|
||||||
<string name="action_post_failed_detail_plural">ပိုစ့်တင်ခြင်းမအောင်မြင်ပါ ပုံကြမ်းများထဲတွင်သိမ်းထားသည်။\n\nဆာဗာအားမချိတ်ဆက်နိုင်ခြင်း သို့ ပယ်ချခြင်းဖြစ်နိုင်သည်။</string>
|
<string name="action_post_failed_detail_plural">ပိုစ့်တင်ခြင်းမအောင်မြင်ပါ ပုံကြမ်းများထဲတွင်သိမ်းထားသည်။\n\nဆာဗာအားမချိတ်ဆက်နိုင်ခြင်း သို့ ပယ်ချခြင်းဖြစ်နိုင်သည်။</string>
|
||||||
<string name="action_post_failed_detail">ပိုစ့်တင်ခြင်းမအောင်မြင်ပါ ပုံကြမ်းများထဲတွင်သိမ်းထားသည်။\n\nဆာဗာအားမချိတ်ဆက်နိုင်ခြင်း သို့ ပယ်ချခြင်းဖြစ်နိုင်သည်။</string>
|
<string name="action_post_failed_detail">ပိုစ့်တင်ခြင်းမအောင်မြင်ပါ ပုံကြမ်းများထဲတွင်သိမ်းထားသည်။\n\nဆာဗာအားမချိတ်ဆက်နိုင်ခြင်း သို့ ပယ်ချခြင်းဖြစ်နိုင်သည်။</string>
|
||||||
<string name="action_block">ဘလော့ခ်</string>
|
<string name="action_block">ဘလော့ခ်</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -575,7 +575,6 @@
|
||||||
<string name="title_lists_failed">Lister - kunne ikke lastes inn</string>
|
<string name="title_lists_failed">Lister - kunne ikke lastes inn</string>
|
||||||
<string name="manage_lists">Håndter lister</string>
|
<string name="manage_lists">Håndter lister</string>
|
||||||
<string name="compose_schedule_date_time_fmt">%1$s %2$s</string>
|
<string name="compose_schedule_date_time_fmt">%1$s %2$s</string>
|
||||||
<string name="error_media_upload_sending_fmt">Mislyktes ved opplasting: %s</string>
|
|
||||||
<string name="title_public_trending_links">Trendende lenker</string>
|
<string name="title_public_trending_links">Trendende lenker</string>
|
||||||
<string name="title_tab_public_trending_hashtags">Emneknagger</string>
|
<string name="title_tab_public_trending_hashtags">Emneknagger</string>
|
||||||
<string name="title_tab_public_trending_links">Lenker</string>
|
<string name="title_tab_public_trending_links">Lenker</string>
|
||||||
|
@ -586,7 +585,6 @@
|
||||||
<string name="compose_warn_language_dialog_fmt">Språket til innlegget er %1$s men det kan skje at du har skrevet innlegget på %2$s.</string>
|
<string name="compose_warn_language_dialog_fmt">Språket til innlegget er %1$s men det kan skje at du har skrevet innlegget på %2$s.</string>
|
||||||
<string name="compose_warn_language_dialog_change_language_fmt">Byt språk til %1$s og tut</string>
|
<string name="compose_warn_language_dialog_change_language_fmt">Byt språk til %1$s og tut</string>
|
||||||
<string name="error_media_uploader_upload_not_found_fmt">Mediaopplasting med ID %1$s finnes ikke</string>
|
<string name="error_media_uploader_upload_not_found_fmt">Mediaopplasting med ID %1$s finnes ikke</string>
|
||||||
<string name="error_media_uploader_throwable_fmt">%1$s</string>
|
|
||||||
<string name="ui_error_translate_status_fmt">Oversettelse mislyktes: %1$s</string>
|
<string name="ui_error_translate_status_fmt">Oversettelse mislyktes: %1$s</string>
|
||||||
<string name="ui_error_filter_v1_load_fmt">Å laste inn filtere mislyktes: %1$s</string>
|
<string name="ui_error_filter_v1_load_fmt">Å laste inn filtere mislyktes: %1$s</string>
|
||||||
<string name="notification_severed_relationships_account_suspension_body">En moderator suspenderte kontoen</string>
|
<string name="notification_severed_relationships_account_suspension_body">En moderator suspenderte kontoen</string>
|
||||||
|
@ -772,4 +770,4 @@
|
||||||
<string name="search_operator_where_dialog_all">Alle innlegg</string>
|
<string name="search_operator_where_dialog_all">Alle innlegg</string>
|
||||||
<string name="search_operator_where_dialog_library_hint">Dine egne innlegg, fremhevinger, favoritmerker, bokmerker, og innlegg som @nevner deg</string>
|
<string name="search_operator_where_dialog_library_hint">Dine egne innlegg, fremhevinger, favoritmerker, bokmerker, og innlegg som @nevner deg</string>
|
||||||
<string name="search_operator_where_dialog_public">Fødererte innlegg</string>
|
<string name="search_operator_where_dialog_public">Fødererte innlegg</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -549,7 +549,6 @@
|
||||||
<string name="notification_notification_worker">Meldingen ophalen…</string>
|
<string name="notification_notification_worker">Meldingen ophalen…</string>
|
||||||
<string name="notification_listenable_worker_name">Achtergrond activiteit</string>
|
<string name="notification_listenable_worker_name">Achtergrond activiteit</string>
|
||||||
<string name="notification_listenable_worker_description">Meldingen als Pachli werkt op de achtergrond</string>
|
<string name="notification_listenable_worker_description">Meldingen als Pachli werkt op de achtergrond</string>
|
||||||
<string name="error_media_upload_sending_fmt">De upload is mislukt: %s</string>
|
|
||||||
<string name="socket_timeout_exception">Contact zoeken met je server duurde te lang</string>
|
<string name="socket_timeout_exception">Contact zoeken met je server duurde te lang</string>
|
||||||
<string name="dialog_delete_filter_positive_action">Verwijder</string>
|
<string name="dialog_delete_filter_positive_action">Verwijder</string>
|
||||||
<string name="dialog_delete_filter_text">Verwijder filter \'%1$s\'\?</string>
|
<string name="dialog_delete_filter_text">Verwijder filter \'%1$s\'\?</string>
|
||||||
|
@ -608,4 +607,4 @@
|
||||||
<string name="pref_update_check_no_updates">Er zijn geen updates beschikbaar</string>
|
<string name="pref_update_check_no_updates">Er zijn geen updates beschikbaar</string>
|
||||||
<string name="pref_update_next_scheduled_check">Volgende geplande controle: %1$s</string>
|
<string name="pref_update_next_scheduled_check">Volgende geplande controle: %1$s</string>
|
||||||
<string name="pref_summary_content_filters">Je server ondersteund geen filters</string>
|
<string name="pref_summary_content_filters">Je server ondersteund geen filters</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -568,5 +568,4 @@
|
||||||
<string name="notification_listenable_worker_description">Notificacions quand Tuska s’executa en rèireplan</string>
|
<string name="notification_listenable_worker_description">Notificacions quand Tuska s’executa en rèireplan</string>
|
||||||
<string name="notification_notification_worker">Recuperacion de las notificacions…</string>
|
<string name="notification_notification_worker">Recuperacion de las notificacions…</string>
|
||||||
<string name="notification_prune_cache">Manteniment del cache…</string>
|
<string name="notification_prune_cache">Manteniment del cache…</string>
|
||||||
<string name="error_media_upload_sending_fmt">Fracàs del mandadís : %s</string>
|
</resources>
|
||||||
</resources>
|
|
||||||
|
|
|
@ -557,7 +557,6 @@
|
||||||
<string name="notification_unknown_name">Desconhecido</string>
|
<string name="notification_unknown_name">Desconhecido</string>
|
||||||
<string name="total_usage">Uso total</string>
|
<string name="total_usage">Uso total</string>
|
||||||
<string name="pref_update_notification_frequency_always">Sempre</string>
|
<string name="pref_update_notification_frequency_always">Sempre</string>
|
||||||
<string name="error_media_upload_sending_fmt">O carregamento falhou: %s</string>
|
|
||||||
<string name="pref_update_notification_frequency_never">Nunca</string>
|
<string name="pref_update_notification_frequency_never">Nunca</string>
|
||||||
<string name="filter_keyword_display_format">%s (palavra inteira)</string>
|
<string name="filter_keyword_display_format">%s (palavra inteira)</string>
|
||||||
<string name="filter_keyword_addition_title">Adicionar palavra-chave</string>
|
<string name="filter_keyword_addition_title">Adicionar palavra-chave</string>
|
||||||
|
@ -614,4 +613,4 @@
|
||||||
<string name="load_newest_notifications">Carregar notificações mais recentes</string>
|
<string name="load_newest_notifications">Carregar notificações mais recentes</string>
|
||||||
<string name="action_discard">Descartar mudanças</string>
|
<string name="action_discard">Descartar mudanças</string>
|
||||||
<string name="pref_ui_text_size">Tamanho do texto da UI</string>
|
<string name="pref_ui_text_size">Tamanho do texto da UI</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -593,7 +593,6 @@
|
||||||
<string name="load_newest_statuses">Ladda de nyaste inläggen</string>
|
<string name="load_newest_statuses">Ladda de nyaste inläggen</string>
|
||||||
<string name="dialog_save_profile_changes_message">Vill du spara dina profiländringar?</string>
|
<string name="dialog_save_profile_changes_message">Vill du spara dina profiländringar?</string>
|
||||||
<string name="pref_update_notification_frequency_always">Alltid</string>
|
<string name="pref_update_notification_frequency_always">Alltid</string>
|
||||||
<string name="error_media_upload_sending_fmt">Uppladdning misslyckades: %s</string>
|
|
||||||
<string name="pref_update_notification_frequency_never">Aldrig</string>
|
<string name="pref_update_notification_frequency_never">Aldrig</string>
|
||||||
<string name="pref_title_show_self_boosts_description">Någon som knuffar sin toot</string>
|
<string name="pref_title_show_self_boosts_description">Någon som knuffar sin toot</string>
|
||||||
<string name="ui_error_filter_v1_load_fmt">Laddning av filter misslyckades: %1$s</string>
|
<string name="ui_error_filter_v1_load_fmt">Laddning av filter misslyckades: %1$s</string>
|
||||||
|
@ -611,4 +610,4 @@
|
||||||
<string name="action_translate_undo">Ångra översättning</string>
|
<string name="action_translate_undo">Ångra översättning</string>
|
||||||
<string name="server_repository_error">Kunde inte hämta serverinformation för %1$s: %2$s</string>
|
<string name="server_repository_error">Kunde inte hämta serverinformation för %1$s: %2$s</string>
|
||||||
<string name="pref_summary_content_filters">Din server stöder inte filter</string>
|
<string name="pref_summary_content_filters">Din server stöder inte filter</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -568,6 +568,5 @@
|
||||||
<string name="error_missing_edits">Sunucunuz bu gönderinin düzenlendiğini bilir, ancak düzenlemelerin bir kopyası yoktur, bu nedenle bunlar size gösterilemez.
|
<string name="error_missing_edits">Sunucunuz bu gönderinin düzenlendiğini bilir, ancak düzenlemelerin bir kopyası yoktur, bu nedenle bunlar size gösterilemez.
|
||||||
\n
|
\n
|
||||||
\nBu <a href="https://github.com/mastodon/mastodon/issues/25398">Mastodon sorununu #25398</a>.</string>
|
\nBu <a href="https://github.com/mastodon/mastodon/issues/25398">Mastodon sorununu #25398</a>.</string>
|
||||||
<string name="error_media_upload_sending_fmt">Yükleme başarısız oldu: %s</string>
|
|
||||||
<string name="error_media_playback">Oynatma başarısız oldu: %s</string>
|
<string name="error_media_playback">Oynatma başarısız oldu: %s</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -583,5 +583,4 @@
|
||||||
<string name="error_missing_edits">Ваш сервер знає, що цей допис було змінено, але не має копії редагувань, тому вони не можуть бути вам показані.
|
<string name="error_missing_edits">Ваш сервер знає, що цей допис було змінено, але не має копії редагувань, тому вони не можуть бути вам показані.
|
||||||
\n
|
\n
|
||||||
\nЦе <a href="https://github.com/mastodon/mastodon/issues/25398">помилка #25398 у Mastodon</a>.</string>
|
\nЦе <a href="https://github.com/mastodon/mastodon/issues/25398">помилка #25398 у Mastodon</a>.</string>
|
||||||
<string name="error_media_upload_sending_fmt">Не вдалося вивантажити: %s</string>
|
</resources>
|
||||||
</resources>
|
|
||||||
|
|
|
@ -551,8 +551,7 @@
|
||||||
<string name="notification_listenable_worker_description">Thông báo khi Pachli hoạt động ngầm</string>
|
<string name="notification_listenable_worker_description">Thông báo khi Pachli hoạt động ngầm</string>
|
||||||
<string name="notification_notification_worker">Đang nạp thông báo…</string>
|
<string name="notification_notification_worker">Đang nạp thông báo…</string>
|
||||||
<string name="notification_prune_cache">Bảo trì bộ nhớ đệm…</string>
|
<string name="notification_prune_cache">Bảo trì bộ nhớ đệm…</string>
|
||||||
<string name="error_media_upload_sending_fmt">Không thể tải lên: %s</string>
|
|
||||||
<string name="error_media_playback">Không thể phát: %s</string>
|
<string name="error_media_playback">Không thể phát: %s</string>
|
||||||
<string name="dialog_delete_filter_text">Xóa bộ lọc \'%1$s\'\?</string>
|
<string name="dialog_delete_filter_text">Xóa bộ lọc \'%1$s\'\?</string>
|
||||||
<string name="dialog_delete_filter_positive_action">Xóa</string>
|
<string name="dialog_delete_filter_positive_action">Xóa</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -565,8 +565,7 @@
|
||||||
<string name="notification_notification_worker">获取通知中…</string>
|
<string name="notification_notification_worker">获取通知中…</string>
|
||||||
<string name="notification_prune_cache">缓存维护…</string>
|
<string name="notification_prune_cache">缓存维护…</string>
|
||||||
<string name="notification_listenable_worker_name">后台活动</string>
|
<string name="notification_listenable_worker_name">后台活动</string>
|
||||||
<string name="error_media_upload_sending_fmt">上传失败了:%s</string>
|
|
||||||
<string name="error_media_playback">播放失败了:%s</string>
|
<string name="error_media_playback">播放失败了:%s</string>
|
||||||
<string name="dialog_delete_filter_text">删除筛选器\'%1$s\'吗?</string>
|
<string name="dialog_delete_filter_text">删除筛选器\'%1$s\'吗?</string>
|
||||||
<string name="dialog_delete_filter_positive_action">删除</string>
|
<string name="dialog_delete_filter_positive_action">删除</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -27,7 +27,6 @@
|
||||||
<string name="error_media_download_permission">Permission to store media is required.</string>
|
<string name="error_media_download_permission">Permission to store media is required.</string>
|
||||||
<string name="error_media_upload_image_or_video">images and videos cannot both be attached to the same post.</string>
|
<string name="error_media_upload_image_or_video">images and videos cannot both be attached to the same post.</string>
|
||||||
<string name="error_media_upload_sending">The upload failed.</string>
|
<string name="error_media_upload_sending">The upload failed.</string>
|
||||||
<string name="error_media_upload_sending_fmt">The upload failed: %s</string>
|
|
||||||
<string name="error_sender_account_gone">Error sending post.</string>
|
<string name="error_sender_account_gone">Error sending post.</string>
|
||||||
<string name="error_following_hashtag_format">Error following #%s</string>
|
<string name="error_following_hashtag_format">Error following #%s</string>
|
||||||
<string name="error_unfollowing_hashtag_format">Error unfollowing #%s</string>
|
<string name="error_unfollowing_hashtag_format">Error unfollowing #%s</string>
|
||||||
|
@ -397,7 +396,7 @@
|
||||||
<string name="lock_account_label_description">Requires you to manually approve followers</string>
|
<string name="lock_account_label_description">Requires you to manually approve followers</string>
|
||||||
<string name="compose_delete_draft">Delete draft?</string>
|
<string name="compose_delete_draft">Delete draft?</string>
|
||||||
<string name="compose_save_draft">Save draft?</string>
|
<string name="compose_save_draft">Save draft?</string>
|
||||||
<string name="compose_save_draft_loses_media">Save draft? (Attachments will be uploaded again when you restore the draft.)</string>
|
<string name="compose_save_draft_loses_media">Attachments will be uploaded again when you restore the draft.</string>
|
||||||
<string name="unsaved_changes">You have unsaved changes.</string>
|
<string name="unsaved_changes">You have unsaved changes.</string>
|
||||||
<string name="send_post_notification_title">Sending post…</string>
|
<string name="send_post_notification_title">Sending post…</string>
|
||||||
<string name="send_post_notification_error_title">Error sending post</string>
|
<string name="send_post_notification_error_title">Error sending post</string>
|
||||||
|
@ -721,7 +720,6 @@
|
||||||
<string name="compose_warn_language_dialog_accept_and_dont_ask_fmt">Post as-is (%1$s) and don\'t ask again</string>
|
<string name="compose_warn_language_dialog_accept_and_dont_ask_fmt">Post as-is (%1$s) and don\'t ask again</string>
|
||||||
|
|
||||||
<string name="error_media_uploader_upload_not_found_fmt">media upload with ID %1$s not found</string>
|
<string name="error_media_uploader_upload_not_found_fmt">media upload with ID %1$s not found</string>
|
||||||
<string name="error_media_uploader_throwable_fmt">%1$s</string>
|
|
||||||
|
|
||||||
<string name="error_prepare_media_content_resolver_missing_path_fmt">content resolver URI was missing a path: %1$s</string>
|
<string name="error_prepare_media_content_resolver_missing_path_fmt">content resolver URI was missing a path: %1$s</string>
|
||||||
<string name="error_prepare_media_content_resolver_unsupported_scheme_fmt">content resolver URI has unsupported scheme: %1$s</string>
|
<string name="error_prepare_media_content_resolver_unsupported_scheme_fmt">content resolver URI has unsupported scheme: %1$s</string>
|
||||||
|
@ -853,4 +851,7 @@
|
||||||
|
|
||||||
<string name="action_copy_item">Copy item</string>
|
<string name="action_copy_item">Copy item</string>
|
||||||
<string name="item_copied">Text copied</string>
|
<string name="item_copied">Text copied</string>
|
||||||
|
|
||||||
|
<string name="upload_failed_msg_fmt">The upload will be retried when you send the post. If it fails again the post will be saved in your drafts.\n\nThe error was: %1$s</string>
|
||||||
|
<string name="upload_failed_modify_attachment">Modify attachment</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -201,14 +201,14 @@ interface MastodonApi {
|
||||||
@PUT("api/v1/media/{mediaId}")
|
@PUT("api/v1/media/{mediaId}")
|
||||||
suspend fun updateMedia(
|
suspend fun updateMedia(
|
||||||
@Path("mediaId") mediaId: String,
|
@Path("mediaId") mediaId: String,
|
||||||
@Field("description") description: String?,
|
@Field("description") description: String? = null,
|
||||||
@Field("focus") focus: String?,
|
@Field("focus") focus: String? = null,
|
||||||
): NetworkResult<Attachment>
|
): ApiResult<Attachment>
|
||||||
|
|
||||||
@GET("api/v1/media/{mediaId}")
|
@GET("api/v1/media/{mediaId}")
|
||||||
suspend fun getMedia(
|
suspend fun getMedia(
|
||||||
@Path("mediaId") mediaId: String,
|
@Path("mediaId") mediaId: String,
|
||||||
): Response<MediaUploadResult>
|
): ApiResult<MediaUploadResult>
|
||||||
|
|
||||||
@POST("api/v1/statuses")
|
@POST("api/v1/statuses")
|
||||||
suspend fun createStatus(
|
suspend fun createStatus(
|
||||||
|
|
|
@ -21,8 +21,8 @@
|
||||||
<string name="node_info_error_no_software_version">software version is missing, empty, or blank</string>
|
<string name="node_info_error_no_software_version">software version is missing, empty, or blank</string>
|
||||||
|
|
||||||
<string name="server_error_unparseable_version">could not parse \"%1$s\" as a version: %2$s</string>
|
<string name="server_error_unparseable_version">could not parse \"%1$s\" as a version: %2$s</string>
|
||||||
<string name="error_generic_fmt">An error occurred: %s</string>
|
<string name="error_generic_fmt">%1$s</string>
|
||||||
<string name="error_404_not_found_fmt">Your server does not support this feature: %1$s</string>
|
<string name="error_404_not_found_fmt">your server does not support this feature: %1$s</string>
|
||||||
<string name="error_429_rate_limit_fmt">your server is rate-limiting your requests: %1$s</string>
|
<string name="error_429_rate_limit_fmt">your server is rate-limiting your requests: %1$s</string>
|
||||||
<string name="error_json_data_fmt">Your server returned an invalid response: %1$s</string>
|
<string name="error_json_data_fmt">Your server returned an invalid response: %1$s</string>
|
||||||
<string name="error_network_fmt">A network error occurred: %s</string>
|
<string name="error_network_fmt">A network error occurred: %s</string>
|
||||||
|
|
Loading…
Reference in New Issue