From 5bc680ab78ea6a4f8cd1af1a80b769c55914143d Mon Sep 17 00:00:00 2001 From: Prat <616399+pt2121@users.noreply.github.com> Date: Mon, 12 Sep 2022 09:21:00 -0700 Subject: [PATCH] Refactor Caption Dialog to handle screen rotation (#2626) (#2693) Co-authored-by: Prat T --- .../components/compose/ComposeActivity.kt | 18 +- .../compose/dialog/CaptionDialog.kt | 186 +++++++++++------- 2 files changed, 127 insertions(+), 77 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt index c7e6f4a2..c2e3eba3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt @@ -68,7 +68,7 @@ import com.keylesspalace.tusky.R import com.keylesspalace.tusky.adapter.EmojiAdapter import com.keylesspalace.tusky.adapter.LocaleAdapter import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener -import com.keylesspalace.tusky.components.compose.dialog.makeCaptionDialog +import com.keylesspalace.tusky.components.compose.dialog.CaptionDialog import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog import com.keylesspalace.tusky.components.compose.view.ComposeOptionsListener import com.keylesspalace.tusky.components.compose.view.ComposeScheduleView @@ -118,7 +118,8 @@ class ComposeActivity : OnEmojiSelectedListener, Injectable, OnReceiveContentListener, - ComposeScheduleView.OnTimeSetListener { + ComposeScheduleView.OnTimeSetListener, + CaptionDialog.Listener { @Inject lateinit var viewModelFactory: ViewModelFactory @@ -213,9 +214,8 @@ class ComposeActivity : val mediaAdapter = MediaPreviewAdapter( this, onAddCaption = { item -> - makeCaptionDialog(item.description, item.uri) { newDescription -> - viewModel.updateDescription(item.localId, newDescription) - } + CaptionDialog.newInstance(item.localId, item.description, item.uri) + .show(supportFragmentManager, "caption_dialog") }, onEditImage = this::editImageInQueue, onRemove = this::removeMediaFromQueue @@ -1149,6 +1149,14 @@ class ComposeActivity : scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN } + override fun onUpdateDescription(localId: Int, description: String) { + lifecycleScope.launch { + if (!viewModel.updateDescription(localId, description)) { + Toast.makeText(this@ComposeActivity, R.string.error_failed_set_caption, Toast.LENGTH_SHORT).show() + } + } + } + @Parcelize data class ComposeOptions( // Let's keep fields var until all consumers are Kotlin diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt index 25b1d260..614b87ee 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt @@ -15,19 +15,22 @@ package com.keylesspalace.tusky.components.compose.dialog -import android.app.Activity -import android.content.DialogInterface +import android.app.Dialog +import android.content.Context import android.graphics.drawable.Drawable import android.net.Uri +import android.os.Bundle import android.text.InputFilter import android.text.InputType +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup import android.view.WindowManager import android.widget.EditText import android.widget.LinearLayout -import android.widget.Toast import androidx.appcompat.app.AlertDialog -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.lifecycleScope +import androidx.core.os.bundleOf +import androidx.fragment.app.DialogFragment import at.connyduck.sparkbutton.helpers.Utils import com.bumptech.glide.Glide import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy @@ -35,85 +38,124 @@ import com.bumptech.glide.request.target.CustomTarget import com.bumptech.glide.request.transition.Transition import com.github.chrisbanes.photoview.PhotoView import com.keylesspalace.tusky.R -import kotlinx.coroutines.launch // https://github.com/tootsuite/mastodon/blob/c6904c0d3766a2ea8a81ab025c127169ecb51373/app/models/media_attachment.rb#L32 private const val MEDIA_DESCRIPTION_CHARACTER_LIMIT = 1500 -fun T.makeCaptionDialog( - existingDescription: String?, - previewUri: Uri, - onUpdateDescription: suspend (String) -> Boolean -) where T : Activity, T : LifecycleOwner { - val dialogLayout = LinearLayout(this) - val padding = Utils.dpToPx(this, 8) - dialogLayout.setPadding(padding, padding, padding, padding) +class CaptionDialog : DialogFragment() { - dialogLayout.orientation = LinearLayout.VERTICAL - val imageView = PhotoView(this).apply { - maximumScale = 6f - } + private lateinit var listener: Listener + private lateinit var input: EditText - val margin = Utils.dpToPx(this, 4) - dialogLayout.addView(imageView) - (imageView.layoutParams as LinearLayout.LayoutParams).weight = 1f - imageView.layoutParams.height = 0 - (imageView.layoutParams as LinearLayout.LayoutParams).setMargins(0, margin, 0, 0) + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val context = requireContext() + val dialogLayout = LinearLayout(context) + val padding = Utils.dpToPx(context, 8) + dialogLayout.setPadding(padding, padding, padding, padding) - val input = EditText(this) - input.hint = resources.getQuantityString( - R.plurals.hint_describe_for_visually_impaired, - MEDIA_DESCRIPTION_CHARACTER_LIMIT, MEDIA_DESCRIPTION_CHARACTER_LIMIT - ) - dialogLayout.addView(input) - (input.layoutParams as LinearLayout.LayoutParams).setMargins(margin, margin, margin, margin) - input.setLines(2) - input.inputType = ( - InputType.TYPE_CLASS_TEXT - or InputType.TYPE_TEXT_FLAG_MULTI_LINE - or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES - ) - input.setText(existingDescription) - input.filters = arrayOf(InputFilter.LengthFilter(MEDIA_DESCRIPTION_CHARACTER_LIMIT)) - - val okListener = { dialog: DialogInterface, _: Int -> - lifecycleScope.launch { - if (!onUpdateDescription(input.text.toString())) { - showFailedCaptionMessage() - } + dialogLayout.orientation = LinearLayout.VERTICAL + val imageView = PhotoView(context).apply { + maximumScale = 6f } - dialog.dismiss() + + val margin = Utils.dpToPx(context, 4) + dialogLayout.addView(imageView) + (imageView.layoutParams as LinearLayout.LayoutParams).weight = 1f + imageView.layoutParams.height = 0 + (imageView.layoutParams as LinearLayout.LayoutParams).setMargins(0, margin, 0, 0) + + input = EditText(context) + input.hint = resources.getQuantityString( + R.plurals.hint_describe_for_visually_impaired, + MEDIA_DESCRIPTION_CHARACTER_LIMIT, MEDIA_DESCRIPTION_CHARACTER_LIMIT + ) + dialogLayout.addView(input) + (input.layoutParams as LinearLayout.LayoutParams).setMargins(margin, margin, margin, margin) + input.setLines(2) + input.inputType = ( + InputType.TYPE_CLASS_TEXT + or InputType.TYPE_TEXT_FLAG_MULTI_LINE + or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES + ) + input.filters = arrayOf(InputFilter.LengthFilter(MEDIA_DESCRIPTION_CHARACTER_LIMIT)) + input.setText(arguments?.getString(EXISTING_DESCRIPTION_ARG)) + + val localId = arguments?.getInt(LOCAL_ID_ARG) ?: error("Missing localId") + val dialog = AlertDialog.Builder(context) + .setView(dialogLayout) + .setPositiveButton(android.R.string.ok) { _, _ -> + listener.onUpdateDescription(localId, input.text.toString()) + } + .setNegativeButton(android.R.string.cancel, null) + .create() + + isCancelable = false + val window = dialog.window + window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) + + val previewUri = + arguments?.getParcelable(PREVIEW_URI_ARG) ?: error("Preview Uri is null") + // Load the image and manually set it into the ImageView because it doesn't have a fixed size. + Glide.with(this) + .load(previewUri) + .downsample(DownsampleStrategy.CENTER_INSIDE) + .into(object : CustomTarget(4096, 4096) { + override fun onLoadCleared(placeholder: Drawable?) { + imageView.setImageDrawable(placeholder) + } + + override fun onResourceReady( + resource: Drawable, + transition: Transition?, + ) { + imageView.setImageDrawable(resource) + } + }) + + return dialog } - val dialog = AlertDialog.Builder(this) - .setView(dialogLayout) - .setPositiveButton(android.R.string.ok, okListener) - .setNegativeButton(android.R.string.cancel, null) - .setCancelable(false) - .create() + override fun onSaveInstanceState(outState: Bundle) { + outState.putString(DESCRIPTION_KEY, input.text.toString()) + super.onSaveInstanceState(outState) + } - val window = dialog.window - window?.setSoftInputMode( - WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE - ) + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View? { + savedInstanceState?.getString(DESCRIPTION_KEY)?.let { + input.setText(it) + } + return super.onCreateView(inflater, container, savedInstanceState) + } - dialog.show() + override fun onAttach(context: Context) { + super.onAttach(context) + listener = context as? Listener ?: error("Activity is not ComposeCaptionDialog.Listener") + } - // Load the image and manually set it into the ImageView because it doesn't have a fixed size. - Glide.with(this) - .load(previewUri) - .downsample(DownsampleStrategy.CENTER_INSIDE) - .into(object : CustomTarget(4096, 4096) { - override fun onLoadCleared(placeholder: Drawable?) { - imageView.setImageDrawable(placeholder) - } + interface Listener { + fun onUpdateDescription(localId: Int, description: String) + } - override fun onResourceReady(resource: Drawable, transition: Transition?) { - imageView.setImageDrawable(resource) - } - }) -} - -private fun Activity.showFailedCaptionMessage() { - Toast.makeText(this, R.string.error_failed_set_caption, Toast.LENGTH_SHORT).show() + companion object { + fun newInstance( + localId: Int, + existingDescription: String?, + previewUri: Uri, + ) = CaptionDialog().apply { + arguments = bundleOf( + LOCAL_ID_ARG to localId, + EXISTING_DESCRIPTION_ARG to existingDescription, + PREVIEW_URI_ARG to previewUri, + ) + } + + private const val DESCRIPTION_KEY = "description" + private const val EXISTING_DESCRIPTION_ARG = "existing_description" + private const val PREVIEW_URI_ARG = "preview_uri" + private const val LOCAL_ID_ARG = "local_id" + } }