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

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
import android.app.Dialog
import android.os.Build
import jp.juggler.subwaytooter.*
import jp.juggler.subwaytooter.actmain.addColumn
@ -71,7 +70,7 @@ fun ActMain.accountAdd() {
val tootInstance = runApiTask2(apiHost) { TootInstance.getOrThrow(it) }
addPseudoAccount(apiHost, tootInstance)?.let { a ->
showToast(false, R.string.server_confirmed)
addColumn(defaultInsertPosition, a, ColumnType.LOCAL,protect=true)
addColumn(defaultInsertPosition, a, ColumnType.LOCAL, protect = true)
dialogHost.dismissSafe()
}
}
@ -139,56 +138,49 @@ private suspend fun ActMain.createUser(
* @param onComplete 非nullならアカウント認証が終わったタイミングで呼ばれる
*/
// アクセストークンの手動入力(更新)
fun ActMain.accessTokenPrompt(
suspend fun ActMain.accessTokenPrompt(
apiHost: Host,
onComplete: (() -> Unit)? = null,
) {
DlgTextInput.show(
this,
getString(R.string.access_token_or_api_token),
null,
callback = object : DlgTextInput.Callback {
override fun onEmptyError() {
showToast(true, R.string.token_not_specified)
showTextInputDialog(
title = getString(R.string.access_token_or_api_token),
initialText = null,
onEmptyText = { showToast(true, R.string.token_not_specified) },
) { text ->
try {
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."),
)
}
override fun onOK(dialog: Dialog, text: String) {
launchMain {
try {
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."),
)
}
if (afterAccountVerify(auth2Result)) {
dialog.dismissSafe()
onComplete?.invoke()
}
} catch (ex: Throwable) {
showApiError(ex)
}
when (afterAccountVerify(auth2Result)) {
false -> false
else -> {
onComplete?.invoke()
true
}
}
} catch (ex: Throwable) {
showApiError(ex)
false
}
)
}
}
// アカウント設定

View File

@ -1,6 +1,5 @@
package jp.juggler.subwaytooter.action
import android.app.Dialog
import jp.juggler.subwaytooter.ActMain
import jp.juggler.subwaytooter.R
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.onListNameUpdated
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
import jp.juggler.subwaytooter.dialog.DlgTextInput
import jp.juggler.subwaytooter.dialog.actionsDialog
import jp.juggler.subwaytooter.dialog.showTextInputDialog
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.coroutine.launchAndShowError
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.network.toPostRequestBuilder
import jp.juggler.util.network.toPutRequestBuilder
import jp.juggler.util.ui.dismissSafe
import okhttp3.Request
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,
item: TootList,
) {
DlgTextInput.show(
this,
getString(R.string.rename),
item.title,
callback = object : DlgTextInput.Callback {
override fun onEmptyError() {
showToast(false, R.string.list_name_empty)
}
override fun onOK(dialog: Dialog, text: String) {
launchMain {
var resultList: TootList? = null
runApiTask(accessInfo) { client ->
if (accessInfo.isMisskey) {
client.request(
"/api/users/lists/update",
accessInfo.putMisskeyApiToken().apply {
put("listId", item.id)
put("title", text)
}.toPostRequestBuilder()
)
} else {
client.request(
"/api/v1/lists/${item.id}",
buildJsonObject {
put("title", text)
}.toPutRequestBuilder()
)
}?.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()
}
}
}
showTextInputDialog(
title = getString(R.string.rename),
initialText = item.title,
onEmptyText = { showToast(false, R.string.list_name_empty) },
) { text ->
var resultList: TootList? = null
val result = runApiTask(accessInfo) { client ->
if (accessInfo.isMisskey) {
client.request(
"/api/users/lists/update",
accessInfo.putMisskeyApiToken().apply {
put("listId", item.id)
put("title", text)
}.toPostRequestBuilder()
)
} else {
client.request(
"/api/v1/lists/${item.id}",
buildJsonObject {
put("title", text)
}.toPutRequestBuilder()
)
}?.also { result ->
client.publishApiProgress(getString(R.string.parsing_response))
resultList = parseItem(result.jsonObject) {
TootList(
TootParser(this, accessInfo),
it
)
}
}
}
)
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
import android.app.Dialog
import android.graphics.Bitmap
import android.net.Uri
import android.text.InputType
import android.view.View
import android.view.WindowManager
import androidx.appcompat.app.AlertDialog
import jp.juggler.subwaytooter.ActPost
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.runApiTask
import jp.juggler.subwaytooter.calcIconRound
import jp.juggler.subwaytooter.databinding.DlgFocusPointBinding
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.decodeAttachmentBitmap
import jp.juggler.subwaytooter.dialog.focusPointDialog
import jp.juggler.subwaytooter.dialog.showTextInputDialog
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.util.AttachmentRequest
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.network.toPutRequestBuilder
import jp.juggler.util.ui.dismissSafe
import jp.juggler.util.ui.isLiveActivity
import jp.juggler.util.ui.vg
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resumeWithException
private val log = LogCategory("ActPostAttachment")
@ -212,13 +220,10 @@ private fun ActPost.appendArrachmentUrl(a: TootAttachment) {
// 添付した画像をタップ
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 {
val pa = attachmentList.elementAtOrNull(idx)
?: error("can't get attachment item[$idx].")
actionsDialog(getString(R.string.media_attachment)) {
action(getString(R.string.set_description)) {
editAttachmentDescription(pa)
@ -265,11 +270,12 @@ fun ActPost.deleteAttachment(pa: PostAttachment) {
.show()
}
fun ActPost.openFocusPoint(pa: PostAttachment) {
suspend fun ActPost.openFocusPoint(pa: PostAttachment) {
val attachment = pa.attachment ?: return
DlgFocusPoint(this, attachment)
.setCallback { x, y -> sendFocusPoint(pa, attachment, x, y) }
.show()
focusPointDialog(
attachment = attachment,
callback = { x, y -> sendFocusPoint(pa, attachment, x, y) }
)
}
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
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()
}
val attachmentId = pa.attachment?.id ?: return
val account = this.account ?: return
var bitmap: Bitmap? = null
try {
val url = a.preview_url
if (url != null) {
val result = runApiTask { client ->
try {
val (result, data) = client.getHttpBytes(url)
data?.let {
bitmap = decodeAttachmentBitmap(it, 1024)
?: return@runApiTask TootApiResult("image decode failed.")
}
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) {

View File

@ -1,6 +1,5 @@
package jp.juggler.subwaytooter.columnviewholder
import android.app.Dialog
import android.graphics.Color
import android.text.Spannable
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.column.*
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.itemviewholder.DlgContextMenu
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.view.MyLinkMovementMethod
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.intoStringResource
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.network.toPostRequestBuilder
import jp.juggler.util.ui.attrColor
import jp.juggler.util.ui.dismissSafe
import jp.juggler.util.ui.setIconDrawableId
import jp.juggler.util.ui.vg
import org.jetbrains.anko.textColor
@ -444,49 +442,46 @@ internal class ViewHolderHeaderProfile(
val who = whoRef.get()
val relation = this.relation
val lastColumn = column
DlgTextInput.show(
activity,
daoAcctColor.getStringWithNickname(
activity,
R.string.personal_notes_of,
who.acct
),
relation?.note ?: "",
allowEmpty = true,
callback = object : DlgTextInput.Callback {
override fun onEmptyError() {
activity.launchAndShowError {
activity.showTextInputDialog(
title = daoAcctColor.getStringWithNickname(
activity,
R.string.personal_notes_of,
who.acct
),
initialText = relation?.note ?: "",
allowEmpty = true,
onEmptyText = {},
) { 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()
)
}
}
override fun onOK(dialog: Dialog, text: String) {
launchMain {
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()
)
}
}?.let { result ->
when (val error = result.error) {
null -> {
relation?.note = text
dialog.dismissSafe()
if (lastColumn == column) bindData(column)
}
else -> activity.showToast(true, error)
}
}
result ?: return@showTextInputDialog true
when (val error = result.error) {
null -> {
relation?.note = text
if (lastColumn == column) bindData(column)
true
}
else -> {
activity.showToast(true, error)
false
}
}
}
)
}
}
}
}
@ -496,7 +491,6 @@ internal class ViewHolderHeaderProfile(
R.id.btnFollow -> {
activity.followFromAnotherAccount(
activity.nextPosition(column),
accessInfo,
whoRef?.get()
@ -506,7 +500,6 @@ internal class ViewHolderHeaderProfile(
R.id.btnMoved -> {
activity.followFromAnotherAccount(
activity.nextPosition(column),
accessInfo,
movedRef?.get()

View File

@ -1,129 +1,101 @@
package jp.juggler.subwaytooter.dialog
import android.annotation.SuppressLint
import android.app.Dialog
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.view.View
import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.*
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.coroutine.launchMain
import jp.juggler.util.data.*
import jp.juggler.util.log.*
import jp.juggler.util.ui.*
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resumeWithException
@SuppressLint("InflateParams")
class DlgFocusPoint(
val activity: AppCompatActivity,
val attachment: TootAttachment,
) : View.OnClickListener {
private val log = LogCategory("DlgFocusPoint")
companion object {
val log = LogCategory("DlgFocusPoint")
fun decodeAttachmentBitmap(
data: ByteArray,
@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
private val focusPointView: FocusPointView
suspend fun AppCompatActivity.focusPointDialog(
attachment: TootAttachment,
callback: (x: Float, y: Float) -> Unit,
) {
var bitmap: Bitmap? = null
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() {
try {
val url = attachment.preview_url
if (url == null) {
activity.showToast(false, "missing image url")
showToast(false, "missing image url")
return
}
val options = BitmapFactory.Options()
fun decodeBitmap(
data: ByteArray,
@Suppress("SameParameterValue") pixelMax: Int,
): 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()
val result = runApiTask { client ->
try {
val (result, data) = client.getHttpBytes(url)
data?.let {
bitmap = decodeAttachmentBitmap(it, 1024)
?: return@runApiTask TootApiResult("image decode failed.")
}
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.MyNetworkImageView
import jp.juggler.util.*
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.data.*
import jp.juggler.util.log.*
@ -247,30 +248,28 @@ class DlgListMember(
}
private fun openListCreator() {
DlgTextInput.show(
activity,
activity.getString(R.string.list_create),
null,
callback = object : DlgTextInput.Callback {
override fun onEmptyError() {
activity.launchAndShowError {
activity.showTextInputDialog(
title = activity.getString(R.string.list_create),
initialText = null,
onEmptyText = {
activity.showToast(false, R.string.list_name_empty)
}
override fun onOK(dialog: Dialog, text: String) {
val list_owner = this@DlgListMember.listOwner
if (list_owner == null) {
},
) { text ->
when (val listOwner1 = this@DlgListMember.listOwner) {
null -> {
activity.showToast(false, "list owner is not selected.")
return
false
}
activity.listCreate(list_owner, text) {
dialog.dismissSafe()
loadLists()
else -> {
activity.listCreate(listOwner1, text) {
loadLists()
}
true
}
}
})
}
}
}
internal class ErrorItem(val message: String)

View File

@ -1,67 +1,69 @@
package jp.juggler.subwaytooter.dialog
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Dialog
import android.view.View
import android.graphics.Bitmap
import android.view.WindowManager
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
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
object DlgTextInput {
interface Callback {
fun onOK(dialog: Dialog, text: String)
fun onEmptyError()
suspend fun AppCompatActivity.showTextInputDialog(
title: CharSequence,
initialText: CharSequence?,
allowEmpty: Boolean = false,
inputType: Int? = null,
bitmap: Bitmap? = null,
onEmptyText: suspend () -> Unit,
// 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)
}
@SuppressLint("InflateParams")
fun show(
activity: Activity,
caption: CharSequence,
initialText: CharSequence?,
allowEmpty: Boolean = false,
callback: Callback,
) {
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)
// views.llInput.maxHeight = (100f * resources.displayMetrics.density + 0.5f).toInt()
inputType?.let { views.etInput.inputType = it }
bitmap?.let { views.ivBitmap.visible().setImageBitmap(it) }
views.etInput.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
views.btnOk.performClick()
true
} else {
false
}
etInput.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
btnOk.performClick()
true
} else {
false
}
val dialog = Dialog(this)
dialog.setContentView(views.root)
dialog.window?.setLayout(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.WRAP_CONTENT
)
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()
}
}
}
val dialog = Dialog(activity)
dialog.setContentView(view)
btnOk.setOnClickListener {
val token = etInput.text.toString().trim { it <= ' ' }
if (token.isEmpty() && !allowEmpty) {
callback.onEmptyError()
} else {
callback.onOK(dialog, token)
}
views.btnCancel.setOnClickListener { dialog.cancel() }
dialog.setOnDismissListener {
if (cont.isActive) cont.resumeWithException(CancellationException())
}
view.findViewById<View>(R.id.btnCancel).setOnClickListener { dialog.cancel() }
dialog.window?.setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT)
cont.invokeOnCancellation { dialog.dismissSafe() }
dialog.show()
}
}

View File

@ -1,29 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/tvCaption"
<jp.juggler.subwaytooter.view.MaxHeightScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:labelFor="@+id/etInput"
tools:ignore="LabelFor" />
android:fadeScrollbars="false"
android:scrollbarStyle="outsideOverlay"
app:maxHeight="240dp">
<EditText
android:id="@+id/etInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:imeOptions="actionDone"
android:importantForAutofill="no"
android:inputType="text" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="12dp"
android:paddingVertical="6dp">
<ImageView
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
style="?android:attr/buttonBarStyle"