添付メディアの説明文の入力時にサムネイル表示を行う。複数行入力に対応する。

This commit is contained in:
tateisu 2023-02-12 02:31:30 +09:00
parent 2ebf63151a
commit b6f30f0097
8 changed files with 379 additions and 374 deletions

View File

@ -1,6 +1,5 @@
package jp.juggler.subwaytooter.action package jp.juggler.subwaytooter.action
import android.app.Dialog
import android.os.Build import android.os.Build
import jp.juggler.subwaytooter.* import jp.juggler.subwaytooter.*
import jp.juggler.subwaytooter.actmain.addColumn import jp.juggler.subwaytooter.actmain.addColumn
@ -71,7 +70,7 @@ fun ActMain.accountAdd() {
val tootInstance = runApiTask2(apiHost) { TootInstance.getOrThrow(it) } val tootInstance = runApiTask2(apiHost) { TootInstance.getOrThrow(it) }
addPseudoAccount(apiHost, tootInstance)?.let { a -> addPseudoAccount(apiHost, tootInstance)?.let { a ->
showToast(false, R.string.server_confirmed) showToast(false, R.string.server_confirmed)
addColumn(defaultInsertPosition, a, ColumnType.LOCAL,protect=true) addColumn(defaultInsertPosition, a, ColumnType.LOCAL, protect = true)
dialogHost.dismissSafe() dialogHost.dismissSafe()
} }
} }
@ -139,56 +138,49 @@ private suspend fun ActMain.createUser(
* @param onComplete 非nullならアカウント認証が終わったタイミングで呼ばれる * @param onComplete 非nullならアカウント認証が終わったタイミングで呼ばれる
*/ */
// アクセストークンの手動入力(更新) // アクセストークンの手動入力(更新)
fun ActMain.accessTokenPrompt( suspend fun ActMain.accessTokenPrompt(
apiHost: Host, apiHost: Host,
onComplete: (() -> Unit)? = null, onComplete: (() -> Unit)? = null,
) { ) {
DlgTextInput.show( showTextInputDialog(
this, title = getString(R.string.access_token_or_api_token),
getString(R.string.access_token_or_api_token), initialText = null,
null, onEmptyText = { showToast(true, R.string.token_not_specified) },
callback = object : DlgTextInput.Callback { ) { text ->
override fun onEmptyError() { try {
showToast(true, R.string.token_not_specified) val accessToken = text.trim()
val auth2Result = runApiTask2(apiHost) { client ->
val ti = TootInstance.getExOrThrow(client, forceAccessToken = accessToken)
val tokenJson = JsonObject()
val userJson = client.verifyAccount(
accessToken,
outTokenInfo = tokenJson, // 更新される
misskeyVersion = ti.misskeyVersionMajor
)
val parser = TootParser(this, linkHelper = LinkHelper.create(ti))
Auth2Result(
tootInstance = ti,
tokenJson = tokenJson,
accountJson = userJson,
tootAccount = parser.account(userJson)
?: error("can't parse user information."),
)
} }
when (afterAccountVerify(auth2Result)) {
override fun onOK(dialog: Dialog, text: String) { false -> false
launchMain { else -> {
try { onComplete?.invoke()
val accessToken = text.trim() true
val auth2Result = runApiTask2(apiHost) { client ->
val ti =
TootInstance.getExOrThrow(client, forceAccessToken = accessToken)
val tokenJson = JsonObject()
val userJson = client.verifyAccount(
accessToken,
outTokenInfo = tokenJson, // 更新される
misskeyVersion = ti.misskeyVersionMajor
)
val parser = TootParser(this, linkHelper = LinkHelper.create(ti))
Auth2Result(
tootInstance = ti,
tokenJson = tokenJson,
accountJson = userJson,
tootAccount = parser.account(userJson)
?: error("can't parse user information."),
)
}
if (afterAccountVerify(auth2Result)) {
dialog.dismissSafe()
onComplete?.invoke()
}
} catch (ex: Throwable) {
showApiError(ex)
}
} }
} }
} catch (ex: Throwable) {
showApiError(ex)
false
} }
) }
} }
// アカウント設定 // アカウント設定

View File

@ -1,6 +1,5 @@
package jp.juggler.subwaytooter.action package jp.juggler.subwaytooter.action
import android.app.Dialog
import jp.juggler.subwaytooter.ActMain import jp.juggler.subwaytooter.ActMain
import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.actmain.addColumn import jp.juggler.subwaytooter.actmain.addColumn
@ -14,8 +13,8 @@ import jp.juggler.subwaytooter.column.ColumnType
import jp.juggler.subwaytooter.column.onListListUpdated import jp.juggler.subwaytooter.column.onListListUpdated
import jp.juggler.subwaytooter.column.onListNameUpdated import jp.juggler.subwaytooter.column.onListNameUpdated
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
import jp.juggler.subwaytooter.dialog.DlgTextInput
import jp.juggler.subwaytooter.dialog.actionsDialog import jp.juggler.subwaytooter.dialog.actionsDialog
import jp.juggler.subwaytooter.dialog.showTextInputDialog
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.coroutine.launchAndShowError import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain import jp.juggler.util.coroutine.launchMain
@ -23,7 +22,6 @@ import jp.juggler.util.data.buildJsonObject
import jp.juggler.util.log.showToast import jp.juggler.util.log.showToast
import jp.juggler.util.network.toPostRequestBuilder import jp.juggler.util.network.toPostRequestBuilder
import jp.juggler.util.network.toPutRequestBuilder import jp.juggler.util.network.toPutRequestBuilder
import jp.juggler.util.ui.dismissSafe
import okhttp3.Request import okhttp3.Request
fun ActMain.clickListTl(pos: Int, accessInfo: SavedAccount, item: TimelineItem?) { fun ActMain.clickListTl(pos: Int, accessInfo: SavedAccount, item: TimelineItem?) {
@ -164,61 +162,54 @@ fun ActMain.listDelete(
} }
} }
fun ActMain.listRename( suspend fun ActMain.listRename(
accessInfo: SavedAccount, accessInfo: SavedAccount,
item: TootList, item: TootList,
) { ) {
showTextInputDialog(
DlgTextInput.show( title = getString(R.string.rename),
this, initialText = item.title,
getString(R.string.rename), onEmptyText = { showToast(false, R.string.list_name_empty) },
item.title, ) { text ->
callback = object : DlgTextInput.Callback { var resultList: TootList? = null
override fun onEmptyError() { val result = runApiTask(accessInfo) { client ->
showToast(false, R.string.list_name_empty) if (accessInfo.isMisskey) {
} client.request(
"/api/users/lists/update",
override fun onOK(dialog: Dialog, text: String) { accessInfo.putMisskeyApiToken().apply {
launchMain { put("listId", item.id)
var resultList: TootList? = null put("title", text)
runApiTask(accessInfo) { client -> }.toPostRequestBuilder()
if (accessInfo.isMisskey) { )
client.request( } else {
"/api/users/lists/update", client.request(
accessInfo.putMisskeyApiToken().apply { "/api/v1/lists/${item.id}",
put("listId", item.id) buildJsonObject {
put("title", text) put("title", text)
}.toPostRequestBuilder() }.toPutRequestBuilder()
) )
} else { }?.also { result ->
client.request( client.publishApiProgress(getString(R.string.parsing_response))
"/api/v1/lists/${item.id}", resultList = parseItem(result.jsonObject) {
buildJsonObject { TootList(
put("title", text) TootParser(this, accessInfo),
}.toPutRequestBuilder() it
) )
}?.also { result ->
client.publishApiProgress(getString(R.string.parsing_response))
resultList = parseItem(result.jsonObject) {
TootList(
TootParser(this, accessInfo),
it
)
}
}
}?.let { result ->
when (val list = resultList) {
null -> showToast(false, result.error)
else -> {
for (column in appState.columnList) {
column.onListNameUpdated(accessInfo, list)
}
dialog.dismissSafe()
}
}
}
} }
} }
} }
) result ?: return@showTextInputDialog true
when (val list = resultList) {
null -> {
showToast(false, result.error)
false
}
else -> {
for (column in appState.columnList) {
column.onListNameUpdated(accessInfo, list)
}
true
}
}
}
} }

View File

@ -1,8 +1,11 @@
package jp.juggler.subwaytooter.actpost package jp.juggler.subwaytooter.actpost
import android.app.Dialog import android.app.Dialog
import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import android.text.InputType
import android.view.View import android.view.View
import android.view.WindowManager
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import jp.juggler.subwaytooter.ActPost import jp.juggler.subwaytooter.ActPost
import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.R
@ -13,10 +16,12 @@ import jp.juggler.subwaytooter.api.entity.TootAttachment.Companion.tootAttachmen
import jp.juggler.subwaytooter.api.entity.TootAttachment.Companion.tootAttachmentJson import jp.juggler.subwaytooter.api.entity.TootAttachment.Companion.tootAttachmentJson
import jp.juggler.subwaytooter.api.runApiTask import jp.juggler.subwaytooter.api.runApiTask
import jp.juggler.subwaytooter.calcIconRound import jp.juggler.subwaytooter.calcIconRound
import jp.juggler.subwaytooter.databinding.DlgFocusPointBinding
import jp.juggler.subwaytooter.defaultColorIcon 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.dialog.actionsDialog
import jp.juggler.subwaytooter.dialog.decodeAttachmentBitmap
import jp.juggler.subwaytooter.dialog.focusPointDialog
import jp.juggler.subwaytooter.dialog.showTextInputDialog
import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.util.AttachmentRequest import jp.juggler.subwaytooter.util.AttachmentRequest
import jp.juggler.subwaytooter.util.PostAttachment import jp.juggler.subwaytooter.util.PostAttachment
@ -29,7 +34,10 @@ import jp.juggler.util.log.showToast
import jp.juggler.util.log.withCaption import jp.juggler.util.log.withCaption
import jp.juggler.util.network.toPutRequestBuilder import jp.juggler.util.network.toPutRequestBuilder
import jp.juggler.util.ui.dismissSafe import jp.juggler.util.ui.dismissSafe
import jp.juggler.util.ui.isLiveActivity
import jp.juggler.util.ui.vg import jp.juggler.util.ui.vg
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resumeWithException
private val log = LogCategory("ActPostAttachment") private val log = LogCategory("ActPostAttachment")
@ -212,13 +220,10 @@ private fun ActPost.appendArrachmentUrl(a: TootAttachment) {
// 添付した画像をタップ // 添付した画像をタップ
fun ActPost.performAttachmentClick(idx: Int) { 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 { launchAndShowError {
val pa = attachmentList.elementAtOrNull(idx)
?: error("can't get attachment item[$idx].")
actionsDialog(getString(R.string.media_attachment)) { actionsDialog(getString(R.string.media_attachment)) {
action(getString(R.string.set_description)) { action(getString(R.string.set_description)) {
editAttachmentDescription(pa) editAttachmentDescription(pa)
@ -265,11 +270,12 @@ fun ActPost.deleteAttachment(pa: PostAttachment) {
.show() .show()
} }
fun ActPost.openFocusPoint(pa: PostAttachment) { suspend fun ActPost.openFocusPoint(pa: PostAttachment) {
val attachment = pa.attachment ?: return val attachment = pa.attachment ?: return
DlgFocusPoint(this, attachment) focusPointDialog(
.setCallback { x, y -> sendFocusPoint(pa, attachment, x, y) } attachment = attachment,
.show() callback = { x, y -> sendFocusPoint(pa, attachment, x, y) }
)
} }
fun ActPost.sendFocusPoint(pa: PostAttachment, attachment: TootAttachment, x: Float, y: Float) { fun ActPost.sendFocusPoint(pa: PostAttachment, attachment: TootAttachment, x: Float, y: Float) {
@ -300,42 +306,65 @@ fun ActPost.sendFocusPoint(pa: PostAttachment, attachment: TootAttachment, x: Fl
} }
} }
fun ActPost.editAttachmentDescription(pa: PostAttachment) { suspend fun ActPost.editAttachmentDescription(pa: PostAttachment) {
val a = pa.attachment val a = pa.attachment
if (a == null) { if (a == null) {
showToast(true, R.string.attachment_description_cant_edit_while_uploading) showToast(true, R.string.attachment_description_cant_edit_while_uploading)
return return
} }
val attachmentId = pa.attachment?.id ?: return
DlgTextInput.show( val account = this.account ?: return
this, var bitmap: Bitmap? = null
getString(R.string.attachment_description), try {
a.description, val url = a.preview_url
callback = object : DlgTextInput.Callback { if (url != null) {
override fun onEmptyError() { val result = runApiTask { client ->
showToast(true, R.string.description_empty) try {
} val (result, data) = client.getHttpBytes(url)
data?.let {
override fun onOK(dialog: Dialog, text: String) { bitmap = decodeAttachmentBitmap(it, 1024)
val attachmentId = pa.attachment?.id ?: return ?: return@runApiTask TootApiResult("image decode failed.")
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()
}
} }
result
} catch (ex: Throwable) {
TootApiResult(ex.withCaption("preview loading failed."))
} }
} }
}) result ?: return
if (!isLiveActivity) return
result.error?.let{
showToast(true, result.error ?: "error")
// not exit
}
}
showTextInputDialog(
title = getString(R.string.attachment_description),
bitmap = bitmap,
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE,
initialText = a.description,
onEmptyText = { showToast(true, R.string.description_empty) },
) { text ->
val (result, newAttachment) = attachmentUploader.setAttachmentDescription(
account,
attachmentId,
text
)
result ?: return@showTextInputDialog true
when (newAttachment) {
null -> {
result.error?.let { showToast(true, it) }
false
}
else -> {
pa.attachment = newAttachment
showMediaAttachment()
true
}
}
}
} finally {
bitmap?.recycle()
}
} }
fun ActPost.onPickCustomThumbnailImpl(pa: PostAttachment, src: GetContentResultEntry) { fun ActPost.onPickCustomThumbnailImpl(pa: PostAttachment, src: GetContentResultEntry) {

View File

@ -1,6 +1,5 @@
package jp.juggler.subwaytooter.columnviewholder package jp.juggler.subwaytooter.columnviewholder
import android.app.Dialog
import android.graphics.Color import android.graphics.Color
import android.text.Spannable import android.text.Spannable
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
@ -22,7 +21,7 @@ import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.api.runApiTask import jp.juggler.subwaytooter.api.runApiTask
import jp.juggler.subwaytooter.column.* import jp.juggler.subwaytooter.column.*
import jp.juggler.subwaytooter.databinding.LvHeaderProfileBinding import jp.juggler.subwaytooter.databinding.LvHeaderProfileBinding
import jp.juggler.subwaytooter.dialog.DlgTextInput import jp.juggler.subwaytooter.dialog.showTextInputDialog
import jp.juggler.subwaytooter.emoji.EmojiMap import jp.juggler.subwaytooter.emoji.EmojiMap
import jp.juggler.subwaytooter.itemviewholder.DlgContextMenu import jp.juggler.subwaytooter.itemviewholder.DlgContextMenu
import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.pref.PrefB
@ -41,7 +40,7 @@ import jp.juggler.subwaytooter.util.openCustomTab
import jp.juggler.subwaytooter.util.startMargin import jp.juggler.subwaytooter.util.startMargin
import jp.juggler.subwaytooter.view.MyLinkMovementMethod import jp.juggler.subwaytooter.view.MyLinkMovementMethod
import jp.juggler.subwaytooter.view.MyTextView import jp.juggler.subwaytooter.view.MyTextView
import jp.juggler.util.coroutine.launchMain import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.data.buildJsonObject import jp.juggler.util.data.buildJsonObject
import jp.juggler.util.data.intoStringResource import jp.juggler.util.data.intoStringResource
import jp.juggler.util.data.notEmpty import jp.juggler.util.data.notEmpty
@ -49,7 +48,6 @@ import jp.juggler.util.data.notZero
import jp.juggler.util.log.showToast import jp.juggler.util.log.showToast
import jp.juggler.util.network.toPostRequestBuilder import jp.juggler.util.network.toPostRequestBuilder
import jp.juggler.util.ui.attrColor import jp.juggler.util.ui.attrColor
import jp.juggler.util.ui.dismissSafe
import jp.juggler.util.ui.setIconDrawableId import jp.juggler.util.ui.setIconDrawableId
import jp.juggler.util.ui.vg import jp.juggler.util.ui.vg
import org.jetbrains.anko.textColor import org.jetbrains.anko.textColor
@ -444,49 +442,46 @@ internal class ViewHolderHeaderProfile(
val who = whoRef.get() val who = whoRef.get()
val relation = this.relation val relation = this.relation
val lastColumn = column val lastColumn = column
DlgTextInput.show( activity.launchAndShowError {
activity, activity.showTextInputDialog(
daoAcctColor.getStringWithNickname( title = daoAcctColor.getStringWithNickname(
activity, activity,
R.string.personal_notes_of, R.string.personal_notes_of,
who.acct who.acct
), ),
relation?.note ?: "", initialText = relation?.note ?: "",
allowEmpty = true, allowEmpty = true,
callback = object : DlgTextInput.Callback { onEmptyText = {},
override fun onEmptyError() { ) { text ->
val result = activity.runApiTask(column.accessInfo) { client ->
when {
accessInfo.isPseudo ->
TootApiResult("Personal notes is not supported on pseudo account.")
accessInfo.isMisskey ->
TootApiResult("Personal notes is not supported on Misskey account.")
else ->
client.request(
"/api/v1/accounts/${who.id}/note",
buildJsonObject {
put("comment", text)
}.toPostRequestBuilder()
)
}
} }
result ?: return@showTextInputDialog true
override fun onOK(dialog: Dialog, text: String) { when (val error = result.error) {
launchMain { null -> {
activity.runApiTask(column.accessInfo) { client -> relation?.note = text
when { if (lastColumn == column) bindData(column)
accessInfo.isPseudo -> true
TootApiResult("Personal notes is not supported on pseudo account.") }
accessInfo.isMisskey -> else -> {
TootApiResult("Personal notes is not supported on Misskey account.") activity.showToast(true, error)
else -> false
client.request(
"/api/v1/accounts/${who.id}/note",
buildJsonObject {
put("comment", text)
}.toPostRequestBuilder()
)
}
}?.let { result ->
when (val error = result.error) {
null -> {
relation?.note = text
dialog.dismissSafe()
if (lastColumn == column) bindData(column)
}
else -> activity.showToast(true, error)
}
}
} }
} }
} }
) }
} }
} }
} }
@ -496,7 +491,6 @@ internal class ViewHolderHeaderProfile(
R.id.btnFollow -> { R.id.btnFollow -> {
activity.followFromAnotherAccount( activity.followFromAnotherAccount(
activity.nextPosition(column), activity.nextPosition(column),
accessInfo, accessInfo,
whoRef?.get() whoRef?.get()
@ -506,7 +500,6 @@ internal class ViewHolderHeaderProfile(
R.id.btnMoved -> { R.id.btnMoved -> {
activity.followFromAnotherAccount( activity.followFromAnotherAccount(
activity.nextPosition(column), activity.nextPosition(column),
accessInfo, accessInfo,
movedRef?.get() movedRef?.get()

View File

@ -1,129 +1,101 @@
package jp.juggler.subwaytooter.dialog package jp.juggler.subwaytooter.dialog
import android.annotation.SuppressLint
import android.app.Dialog import android.app.Dialog
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.view.View
import android.view.WindowManager import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.* import jp.juggler.subwaytooter.api.*
import jp.juggler.subwaytooter.api.entity.TootAttachment import jp.juggler.subwaytooter.api.entity.TootAttachment
import jp.juggler.subwaytooter.view.FocusPointView import jp.juggler.subwaytooter.databinding.DlgFocusPointBinding
import jp.juggler.util.* import jp.juggler.util.*
import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.data.* import jp.juggler.util.data.*
import jp.juggler.util.log.* import jp.juggler.util.log.*
import jp.juggler.util.ui.* import jp.juggler.util.ui.*
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resumeWithException
@SuppressLint("InflateParams") private val log = LogCategory("DlgFocusPoint")
class DlgFocusPoint(
val activity: AppCompatActivity,
val attachment: TootAttachment,
) : View.OnClickListener {
companion object { fun decodeAttachmentBitmap(
data: ByteArray,
val log = LogCategory("DlgFocusPoint") @Suppress("SameParameterValue") pixelMax: Int,
): Bitmap? {
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
options.inScaled = false
options.outWidth = 0
options.outHeight = 0
BitmapFactory.decodeByteArray(data, 0, data.size, options)
var w = options.outWidth
var h = options.outHeight
if (w <= 0 || h <= 0) {
log.e("can't decode bounds.")
return null
} }
var bits = 0
while (w > pixelMax || h > pixelMax) {
++bits
w = w shr 1
h = h shr 1
}
options.inJustDecodeBounds = false
options.inSampleSize = 1 shl bits
return BitmapFactory.decodeByteArray(data, 0, data.size, options)
}
val dialog: Dialog suspend fun AppCompatActivity.focusPointDialog(
private val focusPointView: FocusPointView attachment: TootAttachment,
callback: (x: Float, y: Float) -> Unit,
) {
var bitmap: Bitmap? = null var bitmap: Bitmap? = null
try {
init {
val viewRoot = activity.layoutInflater.inflate(R.layout.dlg_focus_point, null, false)
focusPointView = viewRoot.findViewById(R.id.ivFocus)
viewRoot.findViewById<View>(R.id.btnClose).setOnClickListener(this)
this.dialog = Dialog(activity)
dialog.setContentView(viewRoot)
dialog.setOnDismissListener {
bitmap?.recycle()
}
}
override fun onClick(v: View) {
when (v.id) {
R.id.btnClose -> dialog.dismissSafe()
}
}
fun setCallback(callback: (x: Float, y: Float) -> Unit): DlgFocusPoint {
focusPointView.callback = callback
return this
}
fun show() {
val url = attachment.preview_url val url = attachment.preview_url
if (url == null) { if (url == null) {
activity.showToast(false, "missing image url") showToast(false, "missing image url")
return return
} }
val result = runApiTask { client ->
val options = BitmapFactory.Options() try {
val (result, data) = client.getHttpBytes(url)
fun decodeBitmap( data?.let {
data: ByteArray, bitmap = decodeAttachmentBitmap(it, 1024)
@Suppress("SameParameterValue") pixelMax: Int, ?: return@runApiTask TootApiResult("image decode failed.")
): Bitmap? {
options.inJustDecodeBounds = true
options.inScaled = false
options.outWidth = 0
options.outHeight = 0
BitmapFactory.decodeByteArray(data, 0, data.size, options)
var w = options.outWidth
var h = options.outHeight
if (w <= 0 || h <= 0) {
log.e("can't decode bounds.")
return null
}
var bits = 0
while (w > pixelMax || h > pixelMax) {
++bits
w = w shr 1
h = h shr 1
}
options.inJustDecodeBounds = false
options.inSampleSize = 1 shl bits
return BitmapFactory.decodeByteArray(data, 0, data.size, options)
}
launchMain {
var resultBitmap: Bitmap? = null
val result = activity.runApiTask { client ->
try {
val (result, data) = client.getHttpBytes(url)
data?.let {
resultBitmap = decodeBitmap(it, 1024)
?: return@runApiTask TootApiResult("image decode failed.")
}
result
} catch (ex: Throwable) {
TootApiResult(ex.withCaption("preview loading failed."))
}
}
val bitmap = resultBitmap
when {
bitmap == null -> {
activity.showToast(true, result?.error ?: "?")
dialog.dismissSafe()
}
activity.isFinishing -> {
bitmap.recycle()
dialog.dismissSafe()
}
else -> {
this@DlgFocusPoint.bitmap = bitmap
focusPointView.setAttachment(attachment, bitmap)
dialog.window?.setLayout(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.MATCH_PARENT
)
dialog.show()
} }
result
} catch (ex: Throwable) {
TootApiResult(ex.withCaption("preview loading failed."))
} }
} }
result ?: return
if (bitmap == null) {
showToast(true, result.error ?: "error")
return
} else if (!isLiveActivity) {
return
}
val dialog = Dialog(this)
val views = DlgFocusPointBinding.inflate(layoutInflater)
dialog.setContentView(views.root)
views.ivFocus.setAttachment(attachment, bitmap!!)
views.ivFocus.callback = callback
dialog.window?.setLayout(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.MATCH_PARENT
)
suspendCancellableCoroutine { cont ->
views.btnClose.setOnClickListener {
if (cont.isActive) cont.resume(Unit) {}
dialog.dismissSafe()
}
dialog.setOnDismissListener {
if (cont.isActive) cont.resumeWithException(CancellationException())
}
cont.invokeOnCancellation { dialog.dismissSafe() }
dialog.show()
}
} finally {
bitmap?.recycle()
} }
} }

View File

@ -20,6 +20,7 @@ import jp.juggler.subwaytooter.util.NetworkEmojiInvalidator
import jp.juggler.subwaytooter.view.MyListView import jp.juggler.subwaytooter.view.MyListView
import jp.juggler.subwaytooter.view.MyNetworkImageView import jp.juggler.subwaytooter.view.MyNetworkImageView
import jp.juggler.util.* import jp.juggler.util.*
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.data.* import jp.juggler.util.data.*
import jp.juggler.util.log.* import jp.juggler.util.log.*
@ -247,30 +248,28 @@ class DlgListMember(
} }
private fun openListCreator() { private fun openListCreator() {
DlgTextInput.show( activity.launchAndShowError {
activity, activity.showTextInputDialog(
activity.getString(R.string.list_create), title = activity.getString(R.string.list_create),
null, initialText = null,
callback = object : DlgTextInput.Callback { onEmptyText = {
override fun onEmptyError() {
activity.showToast(false, R.string.list_name_empty) activity.showToast(false, R.string.list_name_empty)
} },
) { text ->
override fun onOK(dialog: Dialog, text: String) { when (val listOwner1 = this@DlgListMember.listOwner) {
val list_owner = this@DlgListMember.listOwner null -> {
if (list_owner == null) {
activity.showToast(false, "list owner is not selected.") activity.showToast(false, "list owner is not selected.")
return false
} }
else -> {
activity.listCreate(list_owner, text) { activity.listCreate(listOwner1, text) {
dialog.dismissSafe() loadLists()
loadLists() }
true
} }
} }
}) }
}
} }
internal class ErrorItem(val message: String) internal class ErrorItem(val message: String)

View File

@ -1,67 +1,69 @@
package jp.juggler.subwaytooter.dialog package jp.juggler.subwaytooter.dialog
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Dialog import android.app.Dialog
import android.view.View import android.graphics.Bitmap
import android.view.WindowManager import android.view.WindowManager
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.widget.EditText import androidx.appcompat.app.AppCompatActivity
import android.widget.TextView import jp.juggler.subwaytooter.databinding.DlgTextInputBinding
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.data.notEmpty
import jp.juggler.util.ui.dismissSafe
import jp.juggler.util.ui.visible
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resumeWithException
import jp.juggler.subwaytooter.R suspend fun AppCompatActivity.showTextInputDialog(
title: CharSequence,
object DlgTextInput { initialText: CharSequence?,
allowEmpty: Boolean = false,
interface Callback { inputType: Int? = null,
fun onOK(dialog: Dialog, text: String) bitmap: Bitmap? = null,
onEmptyText: suspend () -> Unit,
fun onEmptyError() // returns true if we can close dialog
onOk: suspend (String) -> Boolean,
) {
val views = DlgTextInputBinding.inflate(layoutInflater)
views.tvCaption.text = title
initialText?.notEmpty()?.let {
views.etInput.setText(it)
views.etInput.setSelection(it.length)
} }
// views.llInput.maxHeight = (100f * resources.displayMetrics.density + 0.5f).toInt()
@SuppressLint("InflateParams") inputType?.let { views.etInput.inputType = it }
fun show( bitmap?.let { views.ivBitmap.visible().setImageBitmap(it) }
activity: Activity, views.etInput.setOnEditorActionListener { _, actionId, _ ->
caption: CharSequence, if (actionId == EditorInfo.IME_ACTION_DONE) {
initialText: CharSequence?, views.btnOk.performClick()
allowEmpty: Boolean = false, true
callback: Callback, } else {
) { false
val view = activity.layoutInflater.inflate(R.layout.dlg_text_input, null, false)
val etInput = view.findViewById<EditText>(R.id.etInput)
val btnOk = view.findViewById<View>(R.id.btnOk)
val tvCaption = view.findViewById<TextView>(R.id.tvCaption)
tvCaption.text = caption
if (initialText != null && initialText.isNotEmpty()) {
etInput.setText(initialText)
etInput.setSelection(initialText.length)
} }
}
etInput.setOnEditorActionListener { _, actionId, _ -> val dialog = Dialog(this)
if (actionId == EditorInfo.IME_ACTION_DONE) { dialog.setContentView(views.root)
btnOk.performClick() dialog.window?.setLayout(
true WindowManager.LayoutParams.MATCH_PARENT,
} else { WindowManager.LayoutParams.WRAP_CONTENT
false )
suspendCancellableCoroutine { cont ->
views.btnOk.setOnClickListener {
launchAndShowError {
val text = views.etInput.text.toString().trim { it <= ' ' }
if (text.isEmpty() && !allowEmpty) {
onEmptyText()
} else if (onOk(text)) {
if (cont.isActive) cont.resume(Unit) {}
dialog.dismissSafe()
}
} }
} }
views.btnCancel.setOnClickListener { dialog.cancel() }
val dialog = Dialog(activity) dialog.setOnDismissListener {
dialog.setContentView(view) if (cont.isActive) cont.resumeWithException(CancellationException())
btnOk.setOnClickListener {
val token = etInput.text.toString().trim { it <= ' ' }
if (token.isEmpty() && !allowEmpty) {
callback.onEmptyError()
} else {
callback.onOK(dialog, token)
}
} }
cont.invokeOnCancellation { dialog.dismissSafe() }
view.findViewById<View>(R.id.btnCancel).setOnClickListener { dialog.cancel() }
dialog.window?.setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT)
dialog.show() dialog.show()
} }
} }

View File

@ -1,29 +1,56 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
<TextView <jp.juggler.subwaytooter.view.MaxHeightScrollView
android:id="@+id/tvCaption"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="12dp" android:fadeScrollbars="false"
android:layout_marginTop="12dp" android:scrollbarStyle="outsideOverlay"
android:layout_marginEnd="12dp" app:maxHeight="240dp">
android:labelFor="@+id/etInput"
tools:ignore="LabelFor" />
<EditText <LinearLayout
android:id="@+id/etInput" android:layout_width="match_parent"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:orientation="vertical"
android:layout_marginStart="12dp" android:paddingHorizontal="12dp"
android:layout_marginEnd="12dp" android:paddingVertical="6dp">
android:imeOptions="actionDone"
android:importantForAutofill="no" <ImageView
android:inputType="text" /> android:id="@+id/ivBitmap"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginBottom="4dp"
android:importantForAccessibility="no"
android:scaleType="fitCenter"
android:visibility="gone"
tools:src="@drawable/ic_face"
tools:visibility="visible" />
<TextView
android:id="@+id/tvCaption"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:labelFor="@+id/etInput"
tools:ignore="LabelFor"
tools:text="title title title title title title title title title title title title title title title title title title title title title title title title title title title title title title title title title title title " />
<EditText
android:id="@+id/etInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionDone"
android:importantForAutofill="no"
android:inputType="text"
tools:inputType="textMultiLine"
tools:text="text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text " />
</LinearLayout>
</jp.juggler.subwaytooter.view.MaxHeightScrollView>
<LinearLayout <LinearLayout
style="?android:attr/buttonBarStyle" style="?android:attr/buttonBarStyle"