SubwayTooter-Android-App/app/src/main/java/jp/juggler/subwaytooter/actpost/ActPostAttachment.kt

348 lines
12 KiB
Kotlin

package jp.juggler.subwaytooter.actpost
import android.app.Dialog
import android.net.Uri
import android.view.View
import androidx.appcompat.app.AlertDialog
import jp.juggler.subwaytooter.ActPost
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.ApiTask
import jp.juggler.subwaytooter.api.TootApiResult
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.runApiTask
import jp.juggler.subwaytooter.calcIconRound
import jp.juggler.subwaytooter.defaultColorIcon
import jp.juggler.subwaytooter.dialog.DlgFocusPoint
import jp.juggler.subwaytooter.dialog.DlgTextInput
import jp.juggler.subwaytooter.dialog.actionsDialog
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.util.AttachmentRequest
import jp.juggler.subwaytooter.util.PostAttachment
import jp.juggler.subwaytooter.view.MyNetworkImageView
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.data.*
import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.showToast
import jp.juggler.util.log.withCaption
import jp.juggler.util.network.toPutRequestBuilder
import jp.juggler.util.ui.dismissSafe
import jp.juggler.util.ui.vg
private val log = LogCategory("ActPostAttachment")
// AppStateに保存する
fun ActPost.saveAttachmentList() {
if (!isMultiWindowPost) appState.attachmentList = this.attachmentList
}
fun ActPost.decodeAttachments(sv: String) {
attachmentList.clear()
try {
sv.decodeJsonArray().objectList().forEach {
try {
attachmentList.add(PostAttachment(TootAttachment.decodeJson(it)))
} catch (ex: Throwable) {
log.e(ex, "can't parse TootAttachment.")
}
}
} catch (ex: Throwable) {
log.e(ex, "decodeAttachments failed.")
}
}
fun ActPost.showMediaAttachment() {
if (isFinishing) return
views.llAttachment.vg(attachmentList.isNotEmpty())
ivMedia.forEachIndexed { i, v -> showMediaAttachmentOne(v, i) }
}
fun ActPost.showMedisAttachmentProgress() {
val mergedProgress = attachmentList
.mapNotNull { it.progress.notEmpty() }
.joinToString("\n")
views.tvAttachmentProgress
.vg(mergedProgress.isNotEmpty())
?.text = mergedProgress
}
fun ActPost.showMediaAttachmentOne(iv: MyNetworkImageView, idx: Int) {
if (idx >= attachmentList.size) {
iv.visibility = View.GONE
} else {
iv.visibility = View.VISIBLE
val pa = attachmentList[idx]
val a = pa.attachment
when {
a == null || pa.status != PostAttachment.Status.Ok -> {
iv.setDefaultImage(defaultColorIcon(this, R.drawable.ic_upload))
iv.setErrorImage(defaultColorIcon(this, R.drawable.ic_clip))
iv.setImageUrl(calcIconRound(iv.layoutParams.width), null)
}
else -> {
val defaultIconId = when (a.type) {
TootAttachmentType.Image -> R.drawable.ic_image
TootAttachmentType.Video,
TootAttachmentType.GIFV,
-> R.drawable.ic_videocam
TootAttachmentType.Audio -> R.drawable.ic_music_note
else -> R.drawable.ic_clip
}
iv.setDefaultImage(defaultColorIcon(this, defaultIconId))
iv.setErrorImage(defaultColorIcon(this, defaultIconId))
iv.setImageUrl(calcIconRound(iv.layoutParams.width), a.preview_url)
}
}
}
}
fun ActPost.openAttachment() {
when {
attachmentList.size >= 4 -> showToast(false, R.string.attachment_too_many)
account == null -> showToast(false, R.string.account_select_please)
else -> attachmentPicker.openPicker()
}
}
fun ActPost.addAttachment(
uri: Uri,
mimeTypeArg: String? = null,
// onUploadEnd: () -> Unit = {},
) {
val account = this.account
val mimeType = attachmentUploader.getMimeType(uri, mimeTypeArg)
val isReply = states.inReplyToId != null
val instance = account?.let { TootInstance.getCached(it) }
when {
attachmentList.size >= 4 -> showToast(false, R.string.attachment_too_many)
account == null -> showToast(false, R.string.account_select_please)
mimeType?.isEmpty() != false -> showToast(false, R.string.mime_type_missing)
!attachmentUploader.isAcceptableMimeType(
instance,
mimeType,
isReply
) -> Unit // エラーメッセージ出力済み
else -> {
saveAttachmentList()
val pa = PostAttachment(this)
attachmentList.add(pa)
showMediaAttachment()
attachmentUploader.addRequest(
AttachmentRequest(
account,
pa,
uri,
mimeType,
isReply = isReply,
// onUploadEnd = onUploadEnd
)
)
}
}
}
fun ActPost.onPostAttachmentCompleteImpl(pa: PostAttachment) {
// この添付メディアはリストにない
if (!attachmentList.contains(pa)) {
log.w("onPostAttachmentComplete: not in attachment list.")
return
}
when (pa.status) {
PostAttachment.Status.Error -> {
log.w("onPostAttachmentComplete: upload failed.")
attachmentList.remove(pa)
showMediaAttachment()
}
PostAttachment.Status.Progress -> {
// アップロード中…?
log.w("onPostAttachmentComplete: ?? status=${pa.status}")
}
PostAttachment.Status.Ok -> {
when (val a = pa.attachment) {
null -> log.w("onPostAttachmentComplete: upload complete, but missing attachment entity.")
else -> {
// アップロード完了
log.i("onPostAttachmentComplete: upload complete.")
// 投稿欄の末尾に追記する
if (PrefB.bpAppendAttachmentUrlToContent.value) {
appendArrachmentUrl(a)
}
}
}
showMediaAttachment()
}
}
}
/**
* 添付メディアのURLを編集中テキストに挿入する
*/
private fun ActPost.appendArrachmentUrl(a: TootAttachment) {
// text_url is not provided on recent mastodon.
val textUrl = a.text_url ?: a.url
if (textUrl == null) {
log.w("missing attachment.textUrl")
return
}
val e = views.etContent.editableText
if (e == null) {
// 編注中テキストにアクセスできないなら先頭に空白とURLを置いて、先頭を選択する
val appendText = " $textUrl"
views.etContent.setText(appendText)
views.etContent.setSelection(0, 0)
} else {
// 末尾に空白とURLを置く。選択位置は変わらない。
val selStart = views.etContent.selectionStart
val selEnd = views.etContent.selectionEnd
if (e.lastOrNull()?.let { CharacterGroup.isWhitespace(it.code) } != true) {
e.append(" ")
}
e.append(textUrl)
views.etContent.setSelection(selStart, selEnd)
}
}
// 添付した画像をタップ
fun ActPost.performAttachmentClick(idx: Int) {
val pa = try {
attachmentList[idx]
} catch (ex: Throwable) {
showToast(false, ex.withCaption("can't get attachment item[$idx]."))
return
}
launchAndShowError {
actionsDialog(getString(R.string.media_attachment)) {
action(getString(R.string.set_description)) {
editAttachmentDescription(pa)
}
if (pa.attachment?.canFocus == true) {
action(getString(R.string.set_focus_point)) {
openFocusPoint(pa)
}
}
if (account?.isMastodon == true) {
when (pa.attachment?.type) {
TootAttachmentType.Audio,
TootAttachmentType.GIFV,
TootAttachmentType.Video,
-> action(getString(R.string.custom_thumbnail)) {
attachmentPicker.openCustomThumbnail(pa)
}
else -> Unit
}
}
action(getString(R.string.delete)) {
deleteAttachment(pa)
}
}
}
}
fun ActPost.deleteAttachment(pa: PostAttachment) {
AlertDialog.Builder(this)
.setTitle(R.string.confirm_delete_attachment)
.setPositiveButton(R.string.ok) { _, _ ->
try {
pa.isCancelled = true
pa.status = PostAttachment.Status.Error
pa.job.cancel()
attachmentList.remove(pa)
} catch (ignored: Throwable) {
}
showMediaAttachment()
}
.setNegativeButton(R.string.cancel, null)
.show()
}
fun ActPost.openFocusPoint(pa: PostAttachment) {
val attachment = pa.attachment ?: return
DlgFocusPoint(this, attachment)
.setCallback { x, y -> sendFocusPoint(pa, attachment, x, y) }
.show()
}
fun ActPost.sendFocusPoint(pa: PostAttachment, attachment: TootAttachment, x: Float, y: Float) {
val account = this.account ?: return
launchMain {
var resultAttachment: TootAttachment? = null
runApiTask(account, progressStyle = ApiTask.PROGRESS_NONE) { client ->
try {
client.request(
"/api/v1/media/${attachment.id}",
buildJsonObject {
put("focus", "%.2f,%.2f".format(x, y))
}.toPutRequestBuilder()
)?.also { result ->
resultAttachment =
parseItem(::TootAttachment, ServiceType.MASTODON, result.jsonObject)
}
} catch (ex: Throwable) {
TootApiResult(ex.withCaption("set focus point failed."))
}
}?.let { result ->
when (val newAttachment = resultAttachment) {
null -> showToast(true, result.error)
else -> pa.attachment = newAttachment
}
}
}
}
fun ActPost.editAttachmentDescription(pa: PostAttachment) {
val a = pa.attachment
if (a == null) {
showToast(true, R.string.attachment_description_cant_edit_while_uploading)
return
}
DlgTextInput.show(
this,
getString(R.string.attachment_description),
a.description,
callback = object : DlgTextInput.Callback {
override fun onEmptyError() {
showToast(true, R.string.description_empty)
}
override fun onOK(dialog: Dialog, text: String) {
val attachmentId = pa.attachment?.id ?: return
val account = this@editAttachmentDescription.account ?: return
launchMain {
val (result, newAttachment) = attachmentUploader.setAttachmentDescription(
account,
attachmentId,
text
)
when (newAttachment) {
null -> result?.error?.let { showToast(true, it) }
else -> {
pa.attachment = newAttachment
showMediaAttachment()
dialog.dismissSafe()
}
}
}
}
})
}
fun ActPost.onPickCustomThumbnailImpl(pa: PostAttachment, src: GetContentResultEntry) {
when (val account = this.account) {
null -> showToast(false, R.string.account_select_please)
else -> launchMain {
val result = attachmentUploader.uploadCustomThumbnail(account, src, pa)
result?.error?.let { showToast(true, it) }
showMediaAttachment()
}
}
}