From f3bde24b6d551437edad1cd142570aba7702de05 Mon Sep 17 00:00:00 2001 From: mcc Date: Sat, 16 Jul 2022 01:01:31 -0400 Subject: [PATCH 01/27] Attempt-zero implementation of a "focus" feature for image attachments. Choose "Set focus" in the attachment menu, tap once to select focus point (no visual feedback currently), tap "OK". Works in tests. --- .../components/compose/ComposeActivity.kt | 10 +- .../components/compose/ComposeViewModel.kt | 29 ++++- .../components/compose/MediaPreviewAdapter.kt | 11 +- .../components/compose/dialog/FocusDialog.kt | 110 ++++++++++++++++++ .../tusky/network/MastodonApi.kt | 9 +- app/src/main/res/values/strings.xml | 2 + 6 files changed, 165 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt 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 370f89c355..b6155bdb08 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 @@ -26,6 +26,7 @@ import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.PorterDuff import android.graphics.PorterDuffColorFilter +import android.graphics.PointF import android.net.Uri import android.os.Build import android.os.Bundle @@ -68,6 +69,7 @@ import com.keylesspalace.tusky.R import com.keylesspalace.tusky.adapter.EmojiAdapter import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener import com.keylesspalace.tusky.components.compose.dialog.makeCaptionDialog +import com.keylesspalace.tusky.components.compose.dialog.makeFocusDialog import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog import com.keylesspalace.tusky.components.compose.view.ComposeOptionsListener import com.keylesspalace.tusky.components.compose.view.ComposeScheduleView @@ -216,6 +218,11 @@ class ComposeActivity : viewModel.updateDescription(item.localId, newDescription) } }, + onAddFocus = { item -> + makeFocusDialog(item.focus, item.uri) { newFocus -> + viewModel.updateFocus(item.localId, newFocus) + } + }, onEditImage = this::editImageInQueue, onRemove = this::removeMediaFromQueue ) @@ -1065,7 +1072,8 @@ class ComposeActivity : val mediaSize: Long, val uploadPercent: Int = 0, val id: String? = null, - val description: String? = null + val description: String? = null, + val focus: PointF? = null // Range -1..1 y-up ) { enum class Type { IMAGE, VIDEO, AUDIO; diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt index a7e1779cf2..8f9f8191de 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt @@ -15,6 +15,7 @@ package com.keylesspalace.tusky.components.compose +import android.graphics.PointF import android.net.Uri import android.util.Log import androidx.core.net.toUri @@ -337,7 +338,7 @@ class ComposeViewModel @Inject constructor( val updatedItem = newMediaList.find { it.localId == localId } if (updatedItem?.id != null) { - return api.updateMedia(updatedItem.id, description) + return api.updateMediaDescription(updatedItem.id, description) .fold({ true }, { throwable -> @@ -348,6 +349,32 @@ class ComposeViewModel @Inject constructor( return true } + // TODO: Factor this and updateDescription into a single function? + suspend fun updateFocus(localId: Int, focus: PointF): Boolean { + val newMediaList = media.updateAndGet { mediaValue -> + mediaValue.map { mediaItem -> + if (mediaItem.localId == localId) { + mediaItem.copy(focus = focus) + } else { + mediaItem + } + } + } + + val updatedItem = newMediaList.find { it.localId == localId } + if (updatedItem?.id != null) { + val focusString = "${focus.x},${focus.y}" + return api.updateMediaFocus(updatedItem.id, focusString) + .fold({ + true + }, { throwable -> + Log.w(TAG, "failed to update media focus point", throwable) + false + }) + } + return true + } + fun searchAutocompleteSuggestions(token: String): List { when (token[0]) { '@' -> { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt index be54a1aa99..2460f29e74 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt @@ -32,6 +32,7 @@ import com.keylesspalace.tusky.components.compose.view.ProgressImageView class MediaPreviewAdapter( context: Context, private val onAddCaption: (ComposeActivity.QueuedMedia) -> Unit, + private val onAddFocus: (ComposeActivity.QueuedMedia) -> Unit, private val onEditImage: (ComposeActivity.QueuedMedia) -> Unit, private val onRemove: (ComposeActivity.QueuedMedia) -> Unit ) : RecyclerView.Adapter() { @@ -44,15 +45,19 @@ class MediaPreviewAdapter( val item = differ.currentList[position] val popup = PopupMenu(view.context, view) val addCaptionId = 1 - val editImageId = 2 - val removeId = 3 + val addFocusId = 2 + val editImageId = 3 + val removeId = 4 popup.menu.add(0, addCaptionId, 0, R.string.action_set_caption) - if (item.type == ComposeActivity.QueuedMedia.Type.IMAGE) + if (item.type == ComposeActivity.QueuedMedia.Type.IMAGE) { + popup.menu.add(0, addFocusId, 0, R.string.action_set_focus) popup.menu.add(0, editImageId, 0, R.string.action_edit_image) + } popup.menu.add(0, removeId, 0, R.string.action_remove) popup.setOnMenuItemClickListener { menuItem -> when (menuItem.itemId) { addCaptionId -> onAddCaption(item) + addFocusId -> onAddFocus(item) editImageId -> onEditImage(item) removeId -> onRemove(item) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt new file mode 100644 index 0000000000..1be85e9402 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt @@ -0,0 +1,110 @@ +/* Copyright 2019 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.compose.dialog + +import android.app.Activity +import android.content.DialogInterface +import android.graphics.PointF +import android.graphics.drawable.Drawable +import android.net.Uri +import android.text.InputFilter +import android.text.InputType +import android.util.Log // ANDI SHOULD NOT CHECK THIS LINE IN TO GIT +import android.view.WindowManager +import android.widget.EditText +import android.widget.ImageView; +import android.widget.LinearLayout +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import at.connyduck.sparkbutton.helpers.Utils +import com.bumptech.glide.Glide +import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy +import com.bumptech.glide.request.target.CustomTarget +import com.bumptech.glide.request.transition.Transition +import com.github.chrisbanes.photoview.OnPhotoTapListener +import com.github.chrisbanes.photoview.PhotoView +import com.keylesspalace.tusky.R +import kotlinx.coroutines.launch + +fun T.makeFocusDialog( + existingFocus: PointF?, + previewUri: Uri, + onUpdateFocus: suspend (PointF) -> Boolean +) where T : Activity, T : LifecycleOwner { + var focus = existingFocus ?: PointF(0.0f, 0.0f) // Default to center + + val dialogLayout = LinearLayout(this) + val padding = Utils.dpToPx(this, 8) + dialogLayout.setPadding(padding, padding, padding, padding) + + dialogLayout.orientation = LinearLayout.VERTICAL + val imageView = PhotoView(this).apply { + maximumScale = 6f + setOnPhotoTapListener(object : OnPhotoTapListener { + override fun onPhotoTap(view: ImageView, x:Float, y:Float) { + focus = PointF(x*2-1, 1-y*2) // PhotoView range is 0..1 Y-down but Mastodon API range is -1..1 Y-up + } + }) + } + + 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) + + val okListener = { dialog: DialogInterface, _: Int -> + lifecycleScope.launch { + if (!onUpdateFocus(focus)) { + showFailedFocusMessage() + } + } + dialog.dismiss() + } + + val dialog = AlertDialog.Builder(this) + .setView(dialogLayout) + .setPositiveButton(android.R.string.ok, okListener) + .setNegativeButton(android.R.string.cancel, null) + .create() + + val window = dialog.window + window?.setSoftInputMode( + WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE + ) + + dialog.show() + + // 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) + } + }) +} + +private fun Activity.showFailedFocusMessage() { + Toast.makeText(this, R.string.error_failed_set_focus, Toast.LENGTH_SHORT).show() +} diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt index e1d18e9f65..4b947572cd 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -143,11 +143,18 @@ interface MastodonApi { @FormUrlEncoded @PUT("api/v1/media/{mediaId}") - suspend fun updateMedia( + suspend fun updateMediaDescription( @Path("mediaId") mediaId: String, @Field("description") description: String ): NetworkResult + @FormUrlEncoded + @PUT("api/v1/media/{mediaId}") + suspend fun updateMediaFocus( + @Path("mediaId") mediaId: String, + @Field("focus") focus: String + ): NetworkResult + @GET("api/v1/media/{mediaId}") suspend fun getMedia( @Path("mediaId") mediaId: String diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1a7ef29833..08de98ac4b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -402,10 +402,12 @@ Posting with account %1$s Failed to set caption + Failed to set focus Describe for visually impaired\n(%d character limit) Set caption + Set focus Edit image Remove Lock account From 613358ef83e5065e66a2e5c2b5e4871cb43efc4c Mon Sep 17 00:00:00 2001 From: mcc Date: Sun, 17 Jul 2022 13:04:37 -0400 Subject: [PATCH 02/27] Remove code duplication between 'update description' and 'update focus' --- .../components/compose/ComposeViewModel.kt | 41 +++++++------------ .../tusky/network/MastodonApi.kt | 12 ++---- 2 files changed, 18 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt index 8f9f8191de..0ff7ece203 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt @@ -325,11 +325,12 @@ class ComposeViewModel @Inject constructor( return combineLiveData(deletionObservable, sendFlow.asLiveData()) { _, _ -> } } - suspend fun updateDescription(localId: Int, description: String): Boolean { + // Updates a QueuedMedia item arbitrarily, then sends description and focus to server + suspend fun updateMediaItem(localId: Int, mutator: (QueuedMedia) -> QueuedMedia): Boolean { val newMediaList = media.updateAndGet { mediaValue -> mediaValue.map { mediaItem -> if (mediaItem.localId == localId) { - mediaItem.copy(description = description) + mutator(mediaItem) } else { mediaItem } @@ -338,7 +339,9 @@ class ComposeViewModel @Inject constructor( val updatedItem = newMediaList.find { it.localId == localId } if (updatedItem?.id != null) { - return api.updateMediaDescription(updatedItem.id, description) + val focus = updatedItem.focus + val focusString = if (focus != null) "${focus.x},${focus.y}" else null + return api.updateMedia(updatedItem.id, updatedItem.description, focusString) .fold({ true }, { throwable -> @@ -349,30 +352,16 @@ class ComposeViewModel @Inject constructor( return true } - // TODO: Factor this and updateDescription into a single function? - suspend fun updateFocus(localId: Int, focus: PointF): Boolean { - val newMediaList = media.updateAndGet { mediaValue -> - mediaValue.map { mediaItem -> - if (mediaItem.localId == localId) { - mediaItem.copy(focus = focus) - } else { - mediaItem - } - } - } + suspend fun updateDescription(localId: Int, description: String): Boolean { + return updateMediaItem(localId, { mediaItem -> + mediaItem.copy(description = description) + }) + } - val updatedItem = newMediaList.find { it.localId == localId } - if (updatedItem?.id != null) { - val focusString = "${focus.x},${focus.y}" - return api.updateMediaFocus(updatedItem.id, focusString) - .fold({ - true - }, { throwable -> - Log.w(TAG, "failed to update media focus point", throwable) - false - }) - } - return true + suspend fun updateFocus(localId: Int, focus: PointF): Boolean { + return updateMediaItem(localId, { mediaItem -> + mediaItem.copy(focus = focus) + }) } fun searchAutocompleteSuggestions(token: String): List { diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt index 4b947572cd..0913d1dce7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -143,16 +143,10 @@ interface MastodonApi { @FormUrlEncoded @PUT("api/v1/media/{mediaId}") - suspend fun updateMediaDescription( + suspend fun updateMedia( @Path("mediaId") mediaId: String, - @Field("description") description: String - ): NetworkResult - - @FormUrlEncoded - @PUT("api/v1/media/{mediaId}") - suspend fun updateMediaFocus( - @Path("mediaId") mediaId: String, - @Field("focus") focus: String + @Field("description") description: String?, + @Field("focus") focus: String? ): NetworkResult @GET("api/v1/media/{mediaId}") From 7d860157c58b863b858f083f10d41c3dabd7ac74 Mon Sep 17 00:00:00 2001 From: mcc Date: Sun, 17 Jul 2022 21:27:01 -0400 Subject: [PATCH 03/27] Fix ktlint/bitrise failures --- .../tusky/components/compose/ComposeActivity.kt | 2 +- .../tusky/components/compose/dialog/FocusDialog.kt | 10 +++------- 2 files changed, 4 insertions(+), 8 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 b6155bdb08..c59ece0072 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 @@ -24,9 +24,9 @@ import android.content.Intent import android.content.SharedPreferences import android.content.pm.PackageManager import android.graphics.Bitmap +import android.graphics.PointF import android.graphics.PorterDuff import android.graphics.PorterDuffColorFilter -import android.graphics.PointF import android.net.Uri import android.os.Build import android.os.Bundle diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt index 1be85e9402..8ce23b906d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt @@ -20,12 +20,8 @@ import android.content.DialogInterface import android.graphics.PointF import android.graphics.drawable.Drawable import android.net.Uri -import android.text.InputFilter -import android.text.InputType -import android.util.Log // ANDI SHOULD NOT CHECK THIS LINE IN TO GIT import android.view.WindowManager -import android.widget.EditText -import android.widget.ImageView; +import android.widget.ImageView import android.widget.LinearLayout import android.widget.Toast import androidx.appcompat.app.AlertDialog @@ -56,8 +52,8 @@ fun T.makeFocusDialog( val imageView = PhotoView(this).apply { maximumScale = 6f setOnPhotoTapListener(object : OnPhotoTapListener { - override fun onPhotoTap(view: ImageView, x:Float, y:Float) { - focus = PointF(x*2-1, 1-y*2) // PhotoView range is 0..1 Y-down but Mastodon API range is -1..1 Y-up + override fun onPhotoTap(view: ImageView, x: Float, y: Float) { + focus = PointF(x * 2 - 1, 1 - y * 2) // PhotoView range is 0..1 Y-down but Mastodon API range is -1..1 Y-up } }) } From c283a4cf8b34a3512ea04b9f4ecf2a48a0b2a628 Mon Sep 17 00:00:00 2001 From: mcc Date: Fri, 22 Jul 2022 14:07:34 -0400 Subject: [PATCH 04/27] Make updateMediaItem private --- .../keylesspalace/tusky/components/compose/ComposeViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt index 0ff7ece203..c60bd73040 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt @@ -326,7 +326,7 @@ class ComposeViewModel @Inject constructor( } // Updates a QueuedMedia item arbitrarily, then sends description and focus to server - suspend fun updateMediaItem(localId: Int, mutator: (QueuedMedia) -> QueuedMedia): Boolean { + private suspend fun updateMediaItem(localId: Int, mutator: (QueuedMedia) -> QueuedMedia): Boolean { val newMediaList = media.updateAndGet { mediaValue -> mediaValue.map { mediaItem -> if (mediaItem.localId == localId) { From 7b3fd743aee2907625a298079bde7d76f4be7809 Mon Sep 17 00:00:00 2001 From: mcc Date: Fri, 22 Jul 2022 20:15:14 -0400 Subject: [PATCH 05/27] When focus is set on a post attachment the preview focuses correctly. ProgressImageView now inherits from MediaPreviewImageView. --- .../components/compose/MediaPreviewAdapter.kt | 18 ++++++++++++++++-- .../compose/view/ProgressImageView.java | 3 ++- .../tusky/view/MediaPreviewImageView.kt | 2 +- instance-build.gradle | 4 ++-- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt index 2460f29e74..0ba9732d17 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt @@ -28,6 +28,7 @@ import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.compose.view.ProgressImageView +import com.keylesspalace.tusky.entity.Attachment.Focus class MediaPreviewAdapter( context: Context, @@ -83,11 +84,24 @@ class MediaPreviewAdapter( // TODO: Fancy waveform display? holder.progressImageView.setImageResource(R.drawable.ic_music_box_preview_24dp) } else { - Glide.with(holder.itemView.context) + val imageView = holder.progressImageView; + val focus = item.focus + + if (focus != null) + imageView.setFocalPoint(Focus(item.focus.x, item.focus.y)) + else + imageView.removeFocalPoint(); // Probably unnecessary since we have no UI for removal once added. + + var glide = Glide.with(holder.itemView.context) .load(item.uri) .diskCacheStrategy(DiskCacheStrategy.NONE) .dontAnimate() - .into(holder.progressImageView) + .centerInside() + + if (focus != null) + glide = glide.addListener(imageView) + + glide.into(imageView) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ProgressImageView.java b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ProgressImageView.java index fde993d1e1..f737536ec8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ProgressImageView.java +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ProgressImageView.java @@ -30,9 +30,10 @@ import android.util.AttributeSet; import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.view.MediaPreviewImageView; import at.connyduck.sparkbutton.helpers.Utils; -public final class ProgressImageView extends AppCompatImageView { +public final class ProgressImageView extends MediaPreviewImageView { private int progress = -1; private final RectF progressRect = new RectF(); diff --git a/app/src/main/java/com/keylesspalace/tusky/view/MediaPreviewImageView.kt b/app/src/main/java/com/keylesspalace/tusky/view/MediaPreviewImageView.kt index 8922fafd56..bd6d3adb2e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/view/MediaPreviewImageView.kt +++ b/app/src/main/java/com/keylesspalace/tusky/view/MediaPreviewImageView.kt @@ -37,7 +37,7 @@ import com.keylesspalace.tusky.util.FocalPointUtil * However if there is no focal point set (e.g. it is null), then this view should simply * act exactly the same as an ordinary android ImageView. */ -class MediaPreviewImageView +open class MediaPreviewImageView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, diff --git a/instance-build.gradle b/instance-build.gradle index eec6fcac12..50aff7e46c 100644 --- a/instance-build.gradle +++ b/instance-build.gradle @@ -4,10 +4,10 @@ Note: Publishing a custom build on Google Play may violate the Google Play devel */ // The app name -ext.APP_NAME = "Tusky" +ext.APP_NAME = "Tusky-DEV" // The application id. Must be unique, e.g. based on your domain -ext.APP_ID = "com.keylesspalace.tusky" +ext.APP_ID = "com.keylesspalace.tusky_DEV" // url of a custom app logo. Recommended size at least 600x600. Keep empty to use the Tusky elephant friend. ext.CUSTOM_LOGO_URL = "" From 02b5e68895ea484546149b59f11df5f7b0dabf06 Mon Sep 17 00:00:00 2001 From: mcc Date: Fri, 22 Jul 2022 20:30:25 -0400 Subject: [PATCH 06/27] Replace use of PointF for Focus where focus is represented, fix ktlint --- .../tusky/components/compose/ComposeActivity.kt | 3 +-- .../tusky/components/compose/ComposeViewModel.kt | 3 +-- .../tusky/components/compose/MediaPreviewAdapter.kt | 5 ++--- .../tusky/components/compose/dialog/FocusDialog.kt | 10 +++++----- 4 files changed, 9 insertions(+), 12 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 c59ece0072..e872ff47d7 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 @@ -24,7 +24,6 @@ import android.content.Intent import android.content.SharedPreferences import android.content.pm.PackageManager import android.graphics.Bitmap -import android.graphics.PointF import android.graphics.PorterDuff import android.graphics.PorterDuffColorFilter import android.net.Uri @@ -1073,7 +1072,7 @@ class ComposeActivity : val uploadPercent: Int = 0, val id: String? = null, val description: String? = null, - val focus: PointF? = null // Range -1..1 y-up + val focus: Attachment.Focus? = null ) { enum class Type { IMAGE, VIDEO, AUDIO; diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt index c60bd73040..0d7f47043f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt @@ -15,7 +15,6 @@ package com.keylesspalace.tusky.components.compose -import android.graphics.PointF import android.net.Uri import android.util.Log import androidx.core.net.toUri @@ -358,7 +357,7 @@ class ComposeViewModel @Inject constructor( }) } - suspend fun updateFocus(localId: Int, focus: PointF): Boolean { + suspend fun updateFocus(localId: Int, focus: Attachment.Focus): Boolean { return updateMediaItem(localId, { mediaItem -> mediaItem.copy(focus = focus) }) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt index 0ba9732d17..41afde5115 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt @@ -28,7 +28,6 @@ import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.compose.view.ProgressImageView -import com.keylesspalace.tusky.entity.Attachment.Focus class MediaPreviewAdapter( context: Context, @@ -84,11 +83,11 @@ class MediaPreviewAdapter( // TODO: Fancy waveform display? holder.progressImageView.setImageResource(R.drawable.ic_music_box_preview_24dp) } else { - val imageView = holder.progressImageView; + val imageView = holder.progressImageView val focus = item.focus if (focus != null) - imageView.setFocalPoint(Focus(item.focus.x, item.focus.y)) + imageView.setFocalPoint(focus) else imageView.removeFocalPoint(); // Probably unnecessary since we have no UI for removal once added. diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt index 8ce23b906d..edec7e8a19 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt @@ -17,7 +17,6 @@ package com.keylesspalace.tusky.components.compose.dialog import android.app.Activity import android.content.DialogInterface -import android.graphics.PointF import android.graphics.drawable.Drawable import android.net.Uri import android.view.WindowManager @@ -35,14 +34,15 @@ import com.bumptech.glide.request.transition.Transition import com.github.chrisbanes.photoview.OnPhotoTapListener import com.github.chrisbanes.photoview.PhotoView import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.entity.Attachment.Focus import kotlinx.coroutines.launch fun T.makeFocusDialog( - existingFocus: PointF?, + existingFocus: Focus?, previewUri: Uri, - onUpdateFocus: suspend (PointF) -> Boolean + onUpdateFocus: suspend (Focus) -> Boolean ) where T : Activity, T : LifecycleOwner { - var focus = existingFocus ?: PointF(0.0f, 0.0f) // Default to center + var focus = existingFocus ?: Focus(0.0f, 0.0f) // Default to center val dialogLayout = LinearLayout(this) val padding = Utils.dpToPx(this, 8) @@ -53,7 +53,7 @@ fun T.makeFocusDialog( maximumScale = 6f setOnPhotoTapListener(object : OnPhotoTapListener { override fun onPhotoTap(view: ImageView, x: Float, y: Float) { - focus = PointF(x * 2 - 1, 1 - y * 2) // PhotoView range is 0..1 Y-down but Mastodon API range is -1..1 Y-up + focus = Focus(x * 2 - 1, 1 - y * 2) // PhotoView range is 0..1 Y-down but Mastodon API range is -1..1 Y-up } }) } From 84926d97217efc42b509b1af92854fa2780c016b Mon Sep 17 00:00:00 2001 From: mcc Date: Fri, 22 Jul 2022 23:22:48 -0400 Subject: [PATCH 07/27] Substitute 'focus' for 'focus point' in strings --- app/src/main/res/values/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 08de98ac4b..997a7f3fe9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -402,12 +402,12 @@ Posting with account %1$s Failed to set caption - Failed to set focus + Failed to set focus point Describe for visually impaired\n(%d character limit) Set caption - Set focus + Set focus point Edit image Remove Lock account From a439ec5b8fe13d774bc5e95d5b8ac016d835ad45 Mon Sep 17 00:00:00 2001 From: mcc Date: Sat, 3 Sep 2022 17:36:17 -0400 Subject: [PATCH 08/27] First attempt draw focus point. Only updates on initial load. Modeled on code from RoundedCorners builtin from Glide --- .../components/compose/dialog/FocusDialog.kt | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt index edec7e8a19..491dd998a5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt @@ -17,8 +17,11 @@ package com.keylesspalace.tusky.components.compose.dialog import android.app.Activity import android.content.DialogInterface +import android.graphics.* import android.graphics.drawable.Drawable import android.net.Uri +import android.os.Build +import android.util.Log import android.view.WindowManager import android.widget.ImageView import android.widget.LinearLayout @@ -28,14 +31,115 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import at.connyduck.sparkbutton.helpers.Utils import com.bumptech.glide.Glide +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool +import com.bumptech.glide.load.resource.bitmap.BitmapTransformation import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy +import com.bumptech.glide.load.resource.bitmap.TransformationUtils import com.bumptech.glide.request.target.CustomTarget import com.bumptech.glide.request.transition.Transition +import com.bumptech.glide.util.Util import com.github.chrisbanes.photoview.OnPhotoTapListener import com.github.chrisbanes.photoview.PhotoView import com.keylesspalace.tusky.R import com.keylesspalace.tusky.entity.Attachment.Focus import kotlinx.coroutines.launch +import java.nio.ByteBuffer +import java.security.MessageDigest +import java.util.concurrent.locks.Lock + +// Private, but necessary to implement BitmapTransformation, function extracted from Glide +private fun getAlphaSafeBitmap( + pool: BitmapPool, maybeAlphaSafe: Bitmap +): Bitmap { + val safeConfig: Bitmap.Config = getAlphaSafeConfig(maybeAlphaSafe) + if (safeConfig == maybeAlphaSafe.config) { + return maybeAlphaSafe + } + val argbBitmap = pool[maybeAlphaSafe.width, maybeAlphaSafe.height, safeConfig] + Canvas(argbBitmap).drawBitmap(maybeAlphaSafe, 0.0f /*left*/, 0.0f /*top*/, null /*paint*/) + + // From Glide: "We now own this Bitmap. It's our responsibility to replace it in the pool outside this method + // when we're finished with it." + return argbBitmap +} + +// Private, but necessary to implement BitmapTransformation, function extracted from Glide +private fun getAlphaSafeConfig(inBitmap: Bitmap): Bitmap.Config { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // Avoid short circuiting the sdk check. + if (Bitmap.Config.RGBA_F16 == inBitmap.config) { // NOPMD + return Bitmap.Config.RGBA_F16 + } + } + return Bitmap.Config.ARGB_8888 +} + +/** Glide BitmapTransformation which overlays a highlight on a focus point. */ +class HighlightFocus(val focus: Focus) : BitmapTransformation() { + override fun transform( + pool: BitmapPool, inBitmap: Bitmap, outWidth: Int, outHeight: Int + ): Bitmap { + // Draw overlaid target + val bitmapDrawableLock: Lock = TransformationUtils.getBitmapDrawableLock() + val safeConfig: Bitmap.Config = getAlphaSafeConfig(inBitmap) + val toTransform: Bitmap = getAlphaSafeBitmap(pool, inBitmap) + val result = pool[toTransform.width, toTransform.height, safeConfig] + + val plainPaint = Paint() + val strokePaint = Paint() + strokePaint.setAntiAlias(true) + strokePaint.setStyle(Paint.Style.STROKE) + val strokeWidth = 8.0f; + strokePaint.setStrokeWidth(strokeWidth) + strokePaint.setColor(Color.RED) + + bitmapDrawableLock.lock() + try { + val canvas: Canvas = Canvas(result) + + canvas.drawBitmap(toTransform, Matrix.IDENTITY_MATRIX, plainPaint) + + // Canvas range is 0..size Y-down but Mastodon API range is -1..1 Y-up + val x = (focus.x+1.0f)/2.0f*result.width.toFloat(); + val y = (1.0f-focus.y)/2.0f*result.height.toFloat(); + canvas.drawCircle(x, y, Math.min(result.width, result.height).toFloat()/4.0f, strokePaint) + canvas.drawCircle(x, y, strokeWidth/2.0f, strokePaint) + + canvas.setBitmap(null) + } finally { + bitmapDrawableLock.unlock() + } + + if (!toTransform.equals(inBitmap)) { + pool.put(toTransform) + } + + return result + } + + // Remaining methods are boilerplate for BitmapTransformations to work with image caching. + override fun equals(other: Any?): Boolean { + if (other is HighlightFocus) { + return focus == other.focus + } + return false + } + + override fun hashCode(): Int { + return Util.hashCode(ID.hashCode(), Util.hashCode(Util.hashCode(focus.x), Util.hashCode(focus.y))) + } + + override fun updateDiskCacheKey(messageDigest: MessageDigest) { + messageDigest.update(ID_BYTES) + val radiusData: ByteArray = ByteBuffer.allocate(8).putFloat(focus.x).putFloat(focus.y).array() + messageDigest.update(radiusData) + } + + companion object { + private const val ID = "com.keylesspalace.tusky.components.compose.dialog.HighlightFocus" + private val ID_BYTES = ID.toByteArray(CHARSET) + } +} fun T.makeFocusDialog( existingFocus: Focus?, @@ -90,6 +194,7 @@ fun T.makeFocusDialog( Glide.with(this) .load(previewUri) .downsample(DownsampleStrategy.CENTER_INSIDE) + .transform(HighlightFocus(focus)) .into(object : CustomTarget(4096, 4096) { override fun onLoadCleared(placeholder: Drawable?) { imageView.setImageDrawable(placeholder) From 5e920da0e3f786b60eb7ea0f1294f6f41fe1b3bd Mon Sep 17 00:00:00 2001 From: mcc Date: Sat, 3 Sep 2022 17:57:05 -0400 Subject: [PATCH 09/27] Redraw focus after each tap --- .../components/compose/dialog/FocusDialog.kt | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt index 491dd998a5..9999853430 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt @@ -151,13 +151,34 @@ fun T.makeFocusDialog( val dialogLayout = LinearLayout(this) val padding = Utils.dpToPx(this, 8) dialogLayout.setPadding(padding, padding, padding, padding) - dialogLayout.orientation = LinearLayout.VERTICAL - val imageView = PhotoView(this).apply { + + val baseImageRequest = Glide.with(this) + .load(previewUri) + .downsample(DownsampleStrategy.CENTER_INSIDE) + + var imageView:PhotoView? = null + + // Note all calls of this function are after imageView goes non-null + fun imageRequest() { + baseImageRequest.transform(HighlightFocus(focus)) + .into(object : CustomTarget(4096, 4096) { + override fun onLoadCleared(placeholder: Drawable?) { + imageView!!.setImageDrawable(placeholder) + } + + override fun onResourceReady(resource: Drawable, transition: Transition?) { + imageView!!.setImageDrawable(resource) + } + }) + } + + imageView = PhotoView(this).apply { maximumScale = 6f setOnPhotoTapListener(object : OnPhotoTapListener { override fun onPhotoTap(view: ImageView, x: Float, y: Float) { focus = Focus(x * 2 - 1, 1 - y * 2) // PhotoView range is 0..1 Y-down but Mastodon API range is -1..1 Y-up + imageRequest() } }) } @@ -191,19 +212,7 @@ fun T.makeFocusDialog( dialog.show() // 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) - .transform(HighlightFocus(focus)) - .into(object : CustomTarget(4096, 4096) { - override fun onLoadCleared(placeholder: Drawable?) { - imageView.setImageDrawable(placeholder) - } - - override fun onResourceReady(resource: Drawable, transition: Transition?) { - imageView.setImageDrawable(resource) - } - }) + imageRequest() } private fun Activity.showFailedFocusMessage() { From 09ab141d97952e20d24d9a9679d7b24a381bcda1 Mon Sep 17 00:00:00 2001 From: mcc Date: Sat, 3 Sep 2022 18:12:22 -0400 Subject: [PATCH 10/27] Dark curtain where focus isn't (now looks like mastosoc) --- .../components/compose/dialog/FocusDialog.kt | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt index 9999853430..165e945149 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt @@ -21,7 +21,6 @@ import android.graphics.* import android.graphics.drawable.Drawable import android.net.Uri import android.os.Build -import android.util.Log import android.view.WindowManager import android.widget.ImageView import android.widget.LinearLayout @@ -74,6 +73,8 @@ private fun getAlphaSafeConfig(inBitmap: Bitmap): Bitmap.Config { return Bitmap.Config.ARGB_8888 } +private val transparentDarkGray = 0x40000000 + /** Glide BitmapTransformation which overlays a highlight on a focus point. */ class HighlightFocus(val focus: Focus) : BitmapTransformation() { override fun transform( @@ -86,12 +87,15 @@ class HighlightFocus(val focus: Focus) : BitmapTransformation() { val result = pool[toTransform.width, toTransform.height, safeConfig] val plainPaint = Paint() - val strokePaint = Paint() + val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG) strokePaint.setAntiAlias(true) strokePaint.setStyle(Paint.Style.STROKE) - val strokeWidth = 8.0f; + val strokeWidth = 10.0f; strokePaint.setStrokeWidth(strokeWidth) - strokePaint.setColor(Color.RED) + strokePaint.setColor(Color.WHITE) + val curtainPaint = Paint(Paint.ANTI_ALIAS_FLAG) + curtainPaint.style = Paint.Style.FILL + curtainPaint.color = transparentDarkGray bitmapDrawableLock.lock() try { @@ -100,9 +104,19 @@ class HighlightFocus(val focus: Focus) : BitmapTransformation() { canvas.drawBitmap(toTransform, Matrix.IDENTITY_MATRIX, plainPaint) // Canvas range is 0..size Y-down but Mastodon API range is -1..1 Y-up - val x = (focus.x+1.0f)/2.0f*result.width.toFloat(); - val y = (1.0f-focus.y)/2.0f*result.height.toFloat(); - canvas.drawCircle(x, y, Math.min(result.width, result.height).toFloat()/4.0f, strokePaint) + val width = result.width.toFloat() + val height = result.height.toFloat() + val x = (focus.x+1.0f)/2.0f*width; + val y = (1.0f-focus.y)/2.0f*height + val circleRadius = Math.min(width, height).toFloat()/4.0f + + val curtainPath = Path() // Draw a flood fill with a hole cut out of it + curtainPath.setFillType(Path.FillType.WINDING) + curtainPath.addRect(0.0f, 0.0f, width, height, Path.Direction.CW) + curtainPath.addCircle(x, y, circleRadius, Path.Direction.CCW) + canvas.drawPath(curtainPath, curtainPaint) + + canvas.drawCircle(x, y, circleRadius, strokePaint) canvas.drawCircle(x, y, strokeWidth/2.0f, strokePaint) canvas.setBitmap(null) From 8f08b1b6781ed21c02aaaf00c32bdc43a4aae40d Mon Sep 17 00:00:00 2001 From: mcc Date: Sat, 3 Sep 2022 19:23:50 -0400 Subject: [PATCH 11/27] Correct ktlint for FocusDialog --- .../components/compose/dialog/FocusDialog.kt | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt index 165e945149..37a87102c4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt @@ -17,7 +17,12 @@ package com.keylesspalace.tusky.components.compose.dialog import android.app.Activity import android.content.DialogInterface -import android.graphics.* +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.Path import android.graphics.drawable.Drawable import android.net.Uri import android.os.Build @@ -47,9 +52,7 @@ import java.security.MessageDigest import java.util.concurrent.locks.Lock // Private, but necessary to implement BitmapTransformation, function extracted from Glide -private fun getAlphaSafeBitmap( - pool: BitmapPool, maybeAlphaSafe: Bitmap -): Bitmap { +private fun getAlphaSafeBitmap(pool: BitmapPool, maybeAlphaSafe: Bitmap): Bitmap { val safeConfig: Bitmap.Config = getAlphaSafeConfig(maybeAlphaSafe) if (safeConfig == maybeAlphaSafe.config) { return maybeAlphaSafe @@ -77,9 +80,7 @@ private val transparentDarkGray = 0x40000000 /** Glide BitmapTransformation which overlays a highlight on a focus point. */ class HighlightFocus(val focus: Focus) : BitmapTransformation() { - override fun transform( - pool: BitmapPool, inBitmap: Bitmap, outWidth: Int, outHeight: Int - ): Bitmap { + override fun transform(pool: BitmapPool, inBitmap: Bitmap, outWidth: Int, outHeight: Int): Bitmap { // Draw overlaid target val bitmapDrawableLock: Lock = TransformationUtils.getBitmapDrawableLock() val safeConfig: Bitmap.Config = getAlphaSafeConfig(inBitmap) @@ -90,7 +91,7 @@ class HighlightFocus(val focus: Focus) : BitmapTransformation() { val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG) strokePaint.setAntiAlias(true) strokePaint.setStyle(Paint.Style.STROKE) - val strokeWidth = 10.0f; + val strokeWidth = 10.0f strokePaint.setStrokeWidth(strokeWidth) strokePaint.setColor(Color.WHITE) val curtainPaint = Paint(Paint.ANTI_ALIAS_FLAG) @@ -106,9 +107,9 @@ class HighlightFocus(val focus: Focus) : BitmapTransformation() { // Canvas range is 0..size Y-down but Mastodon API range is -1..1 Y-up val width = result.width.toFloat() val height = result.height.toFloat() - val x = (focus.x+1.0f)/2.0f*width; - val y = (1.0f-focus.y)/2.0f*height - val circleRadius = Math.min(width, height).toFloat()/4.0f + val x = (focus.x + 1.0f) / 2.0f * width + val y = (1.0f - focus.y) / 2.0f * height + val circleRadius = Math.min(width, height).toFloat() / 4.0f val curtainPath = Path() // Draw a flood fill with a hole cut out of it curtainPath.setFillType(Path.FillType.WINDING) @@ -117,7 +118,7 @@ class HighlightFocus(val focus: Focus) : BitmapTransformation() { canvas.drawPath(curtainPath, curtainPaint) canvas.drawCircle(x, y, circleRadius, strokePaint) - canvas.drawCircle(x, y, strokeWidth/2.0f, strokePaint) + canvas.drawCircle(x, y, strokeWidth / 2.0f, strokePaint) canvas.setBitmap(null) } finally { @@ -171,7 +172,7 @@ fun T.makeFocusDialog( .load(previewUri) .downsample(DownsampleStrategy.CENTER_INSIDE) - var imageView:PhotoView? = null + var imageView: PhotoView? = null // Note all calls of this function are after imageView goes non-null fun imageRequest() { From d345e62e9f8e5822691ad1968850a16142bb756b Mon Sep 17 00:00:00 2001 From: Conny Duck Date: Mon, 5 Sep 2022 12:56:54 +0200 Subject: [PATCH 12/27] draft: switch to overlay for focus indicator --- .../components/compose/dialog/FocusDialog.kt | 47 +++------------- .../compose/view/FocusIndicatorView.kt | 54 +++++++++++++++++++ app/src/main/res/layout/dialog_focus.xml | 17 ++++++ 3 files changed, 78 insertions(+), 40 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt create mode 100644 app/src/main/res/layout/dialog_focus.xml diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt index 37a87102c4..a9363dd700 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt @@ -45,6 +45,7 @@ import com.bumptech.glide.util.Util import com.github.chrisbanes.photoview.OnPhotoTapListener import com.github.chrisbanes.photoview.PhotoView import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.DialogFocusBinding import com.keylesspalace.tusky.entity.Attachment.Focus import kotlinx.coroutines.launch import java.nio.ByteBuffer @@ -161,48 +162,15 @@ fun T.makeFocusDialog( previewUri: Uri, onUpdateFocus: suspend (Focus) -> Boolean ) where T : Activity, T : LifecycleOwner { - var focus = existingFocus ?: Focus(0.0f, 0.0f) // Default to center + val focus = existingFocus ?: Focus(0.0f, 0.0f) // Default to center - val dialogLayout = LinearLayout(this) - val padding = Utils.dpToPx(this, 8) - dialogLayout.setPadding(padding, padding, padding, padding) - dialogLayout.orientation = LinearLayout.VERTICAL + val dialogBinding = DialogFocusBinding.inflate(layoutInflater) - val baseImageRequest = Glide.with(this) + Glide.with(this) .load(previewUri) .downsample(DownsampleStrategy.CENTER_INSIDE) + .into(dialogBinding.imageView) - var imageView: PhotoView? = null - - // Note all calls of this function are after imageView goes non-null - fun imageRequest() { - baseImageRequest.transform(HighlightFocus(focus)) - .into(object : CustomTarget(4096, 4096) { - override fun onLoadCleared(placeholder: Drawable?) { - imageView!!.setImageDrawable(placeholder) - } - - override fun onResourceReady(resource: Drawable, transition: Transition?) { - imageView!!.setImageDrawable(resource) - } - }) - } - - imageView = PhotoView(this).apply { - maximumScale = 6f - setOnPhotoTapListener(object : OnPhotoTapListener { - override fun onPhotoTap(view: ImageView, x: Float, y: Float) { - focus = Focus(x * 2 - 1, 1 - y * 2) // PhotoView range is 0..1 Y-down but Mastodon API range is -1..1 Y-up - imageRequest() - } - }) - } - - 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) val okListener = { dialog: DialogInterface, _: Int -> lifecycleScope.launch { @@ -214,7 +182,7 @@ fun T.makeFocusDialog( } val dialog = AlertDialog.Builder(this) - .setView(dialogLayout) + .setView(dialogBinding.root) .setPositiveButton(android.R.string.ok, okListener) .setNegativeButton(android.R.string.cancel, null) .create() @@ -226,8 +194,7 @@ fun T.makeFocusDialog( dialog.show() - // Load the image and manually set it into the ImageView because it doesn't have a fixed size. - imageRequest() + } private fun Activity.showFailedFocusMessage() { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt new file mode 100644 index 0000000000..b65dc5e03f --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt @@ -0,0 +1,54 @@ +package com.keylesspalace.tusky.components.compose.view + +import android.content.Context +import android.graphics.Canvas +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import androidx.appcompat.content.res.AppCompatResources +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.entity.Attachment + +class FocusIndicatorView +@JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + + private var focusDrawable: Drawable = AppCompatResources.getDrawable(context, R.drawable.spellcheck)!! // TODO: use an actual drawable suited as indicator + private var posX = 0f + private var posY = 0f + + fun setFocus(focus: Attachment.Focus) { + // TODO + invalidate() + } + + fun getFocus() { + // TODO + invalidate() + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + // TODO: only handle events if they are on top of the image below + // TODO: don't handle all event actions + + posX = event.x + posY = event.y + invalidate() + return true + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + focusDrawable.setBounds( + posX.toInt() - focusDrawable.intrinsicWidth / 2, + posY.toInt() - focusDrawable.intrinsicHeight / 2, + posX.toInt() + focusDrawable.intrinsicWidth / 2, + posY.toInt() + focusDrawable.intrinsicHeight / 2 + ) + focusDrawable.draw(canvas) + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_focus.xml b/app/src/main/res/layout/dialog_focus.xml new file mode 100644 index 0000000000..a509289200 --- /dev/null +++ b/app/src/main/res/layout/dialog_focus.xml @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file From c89cdcf07cee8418182aa818a371fb590925086b Mon Sep 17 00:00:00 2001 From: mcc Date: Mon, 5 Sep 2022 13:42:51 -0400 Subject: [PATCH 13/27] Draw focus circle, but ImageView and FocusIndicatorView seem to share a single canvas --- .../components/compose/dialog/FocusDialog.kt | 110 +----------------- .../compose/view/FocusIndicatorView.kt | 34 ++++-- 2 files changed, 27 insertions(+), 117 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt index a9363dd700..e5a8f24041 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt @@ -52,111 +52,6 @@ import java.nio.ByteBuffer import java.security.MessageDigest import java.util.concurrent.locks.Lock -// Private, but necessary to implement BitmapTransformation, function extracted from Glide -private fun getAlphaSafeBitmap(pool: BitmapPool, maybeAlphaSafe: Bitmap): Bitmap { - val safeConfig: Bitmap.Config = getAlphaSafeConfig(maybeAlphaSafe) - if (safeConfig == maybeAlphaSafe.config) { - return maybeAlphaSafe - } - val argbBitmap = pool[maybeAlphaSafe.width, maybeAlphaSafe.height, safeConfig] - Canvas(argbBitmap).drawBitmap(maybeAlphaSafe, 0.0f /*left*/, 0.0f /*top*/, null /*paint*/) - - // From Glide: "We now own this Bitmap. It's our responsibility to replace it in the pool outside this method - // when we're finished with it." - return argbBitmap -} - -// Private, but necessary to implement BitmapTransformation, function extracted from Glide -private fun getAlphaSafeConfig(inBitmap: Bitmap): Bitmap.Config { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // Avoid short circuiting the sdk check. - if (Bitmap.Config.RGBA_F16 == inBitmap.config) { // NOPMD - return Bitmap.Config.RGBA_F16 - } - } - return Bitmap.Config.ARGB_8888 -} - -private val transparentDarkGray = 0x40000000 - -/** Glide BitmapTransformation which overlays a highlight on a focus point. */ -class HighlightFocus(val focus: Focus) : BitmapTransformation() { - override fun transform(pool: BitmapPool, inBitmap: Bitmap, outWidth: Int, outHeight: Int): Bitmap { - // Draw overlaid target - val bitmapDrawableLock: Lock = TransformationUtils.getBitmapDrawableLock() - val safeConfig: Bitmap.Config = getAlphaSafeConfig(inBitmap) - val toTransform: Bitmap = getAlphaSafeBitmap(pool, inBitmap) - val result = pool[toTransform.width, toTransform.height, safeConfig] - - val plainPaint = Paint() - val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG) - strokePaint.setAntiAlias(true) - strokePaint.setStyle(Paint.Style.STROKE) - val strokeWidth = 10.0f - strokePaint.setStrokeWidth(strokeWidth) - strokePaint.setColor(Color.WHITE) - val curtainPaint = Paint(Paint.ANTI_ALIAS_FLAG) - curtainPaint.style = Paint.Style.FILL - curtainPaint.color = transparentDarkGray - - bitmapDrawableLock.lock() - try { - val canvas: Canvas = Canvas(result) - - canvas.drawBitmap(toTransform, Matrix.IDENTITY_MATRIX, plainPaint) - - // Canvas range is 0..size Y-down but Mastodon API range is -1..1 Y-up - val width = result.width.toFloat() - val height = result.height.toFloat() - val x = (focus.x + 1.0f) / 2.0f * width - val y = (1.0f - focus.y) / 2.0f * height - val circleRadius = Math.min(width, height).toFloat() / 4.0f - - val curtainPath = Path() // Draw a flood fill with a hole cut out of it - curtainPath.setFillType(Path.FillType.WINDING) - curtainPath.addRect(0.0f, 0.0f, width, height, Path.Direction.CW) - curtainPath.addCircle(x, y, circleRadius, Path.Direction.CCW) - canvas.drawPath(curtainPath, curtainPaint) - - canvas.drawCircle(x, y, circleRadius, strokePaint) - canvas.drawCircle(x, y, strokeWidth / 2.0f, strokePaint) - - canvas.setBitmap(null) - } finally { - bitmapDrawableLock.unlock() - } - - if (!toTransform.equals(inBitmap)) { - pool.put(toTransform) - } - - return result - } - - // Remaining methods are boilerplate for BitmapTransformations to work with image caching. - override fun equals(other: Any?): Boolean { - if (other is HighlightFocus) { - return focus == other.focus - } - return false - } - - override fun hashCode(): Int { - return Util.hashCode(ID.hashCode(), Util.hashCode(Util.hashCode(focus.x), Util.hashCode(focus.y))) - } - - override fun updateDiskCacheKey(messageDigest: MessageDigest) { - messageDigest.update(ID_BYTES) - val radiusData: ByteArray = ByteBuffer.allocate(8).putFloat(focus.x).putFloat(focus.y).array() - messageDigest.update(radiusData) - } - - companion object { - private const val ID = "com.keylesspalace.tusky.components.compose.dialog.HighlightFocus" - private val ID_BYTES = ID.toByteArray(CHARSET) - } -} - fun T.makeFocusDialog( existingFocus: Focus?, previewUri: Uri, @@ -166,12 +61,13 @@ fun T.makeFocusDialog( val dialogBinding = DialogFocusBinding.inflate(layoutInflater) + dialogBinding.focusIndicator.setFocus(focus) + Glide.with(this) .load(previewUri) .downsample(DownsampleStrategy.CENTER_INSIDE) .into(dialogBinding.imageView) - val okListener = { dialog: DialogInterface, _: Int -> lifecycleScope.launch { if (!onUpdateFocus(focus)) { @@ -193,8 +89,6 @@ fun T.makeFocusDialog( ) dialog.show() - - } private fun Activity.showFailedFocusMessage() { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt index b65dc5e03f..af411eb563 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt @@ -1,7 +1,10 @@ package com.keylesspalace.tusky.components.compose.view import android.content.Context +import android.graphics.BlendMode import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint import android.graphics.drawable.Drawable import android.util.AttributeSet import android.view.MotionEvent @@ -16,8 +19,6 @@ class FocusIndicatorView attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { - - private var focusDrawable: Drawable = AppCompatResources.getDrawable(context, R.drawable.spellcheck)!! // TODO: use an actual drawable suited as indicator private var posX = 0f private var posY = 0f @@ -41,14 +42,29 @@ class FocusIndicatorView return true } + private val transparentDarkGray = 0x40000000 + private val strokeWidth = 10.0f + + private val erasePaint = Paint(Paint.ANTI_ALIAS_FLAG) + private val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG) + + init { + erasePaint.setBlendMode(BlendMode.CLEAR) + + strokePaint.setStyle(Paint.Style.STROKE) + strokePaint.setStrokeWidth(strokeWidth) + strokePaint.setColor(Color.WHITE) + } + override fun onDraw(canvas: Canvas) { super.onDraw(canvas) - focusDrawable.setBounds( - posX.toInt() - focusDrawable.intrinsicWidth / 2, - posY.toInt() - focusDrawable.intrinsicHeight / 2, - posX.toInt() + focusDrawable.intrinsicWidth / 2, - posY.toInt() + focusDrawable.intrinsicHeight / 2 - ) - focusDrawable.draw(canvas) + + canvas.drawColor(transparentDarkGray, BlendMode.SRC_OUT) // Blank canvas + + val circleRadius = Math.min(getWidth(), getHeight()).toFloat() / 4.0f + + canvas.drawCircle(posX, posY, circleRadius, erasePaint) // Erase hole in curtain + canvas.drawCircle(posX, posY, circleRadius, strokePaint) // Draw white circle + canvas.drawCircle(posX, posY, strokeWidth / 2.0f, strokePaint) // Draw white dot } } \ No newline at end of file From 3437fa8ee8c79b24062a0eb7a01f47d67b6197f0 Mon Sep 17 00:00:00 2001 From: mcc Date: Mon, 5 Sep 2022 15:17:50 -0400 Subject: [PATCH 14/27] Switch focus circle to path approach --- .../compose/view/FocusIndicatorView.kt | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt index af411eb563..fb263a7337 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt @@ -5,6 +5,7 @@ import android.graphics.BlendMode import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint +import android.graphics.Path import android.graphics.drawable.Drawable import android.util.AttributeSet import android.view.MotionEvent @@ -45,11 +46,12 @@ class FocusIndicatorView private val transparentDarkGray = 0x40000000 private val strokeWidth = 10.0f - private val erasePaint = Paint(Paint.ANTI_ALIAS_FLAG) + private val curtainPaint = Paint(Paint.ANTI_ALIAS_FLAG) private val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG) init { - erasePaint.setBlendMode(BlendMode.CLEAR) + curtainPaint.setColor(transparentDarkGray) + curtainPaint.style = Paint.Style.FILL strokePaint.setStyle(Paint.Style.STROKE) strokePaint.setStrokeWidth(strokeWidth) @@ -59,11 +61,16 @@ class FocusIndicatorView override fun onDraw(canvas: Canvas) { super.onDraw(canvas) - canvas.drawColor(transparentDarkGray, BlendMode.SRC_OUT) // Blank canvas + val width = getWidth().toFloat() + val height = getHeight().toFloat() + val circleRadius = Math.min(width, height) / 4.0f - val circleRadius = Math.min(getWidth(), getHeight()).toFloat() / 4.0f + val curtainPath = Path() // Draw a flood fill with a hole cut out of it + curtainPath.setFillType(Path.FillType.WINDING) + curtainPath.addRect(0.0f, 0.0f, width, height, Path.Direction.CW) + curtainPath.addCircle(posX, posY, circleRadius, Path.Direction.CCW) + canvas.drawPath(curtainPath, curtainPaint) - canvas.drawCircle(posX, posY, circleRadius, erasePaint) // Erase hole in curtain canvas.drawCircle(posX, posY, circleRadius, strokePaint) // Draw white circle canvas.drawCircle(posX, posY, strokeWidth / 2.0f, strokePaint) // Draw white dot } From 480b07bce5fa35e9943e292b13963b7088ec9ddb Mon Sep 17 00:00:00 2001 From: mcc Date: Mon, 5 Sep 2022 18:06:34 -0400 Subject: [PATCH 15/27] Correctly scale, save and load focuses. Clamp to visible area. Focus editor looks and feels right --- .../components/compose/dialog/FocusDialog.kt | 17 +++- .../compose/view/FocusIndicatorView.kt | 79 ++++++++++++++----- 2 files changed, 74 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt index e5a8f24041..5430e6f38a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt @@ -35,11 +35,16 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import at.connyduck.sparkbutton.helpers.Utils import com.bumptech.glide.Glide +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool import com.bumptech.glide.load.resource.bitmap.BitmapTransformation import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy import com.bumptech.glide.load.resource.bitmap.TransformationUtils +import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.target.CustomTarget +import com.bumptech.glide.request.target.SizeReadyCallback +import com.bumptech.glide.request.target.Target import com.bumptech.glide.request.transition.Transition import com.bumptech.glide.util.Util import com.github.chrisbanes.photoview.OnPhotoTapListener @@ -66,11 +71,21 @@ fun T.makeFocusDialog( Glide.with(this) .load(previewUri) .downsample(DownsampleStrategy.CENTER_INSIDE) + .listener(object: RequestListener { + override fun onLoadFailed(p0: GlideException?, p1:Any?, p2:Target?, p3: Boolean): Boolean { + return false + } + + override fun onResourceReady(resource: Drawable?, model: Any?, target: Target?, dataSource: DataSource?, isFirstResource: Boolean): Boolean { + dialogBinding.focusIndicator.setImageSize(resource!!.getIntrinsicWidth(), resource.getIntrinsicHeight()) + return false + } + }) .into(dialogBinding.imageView) val okListener = { dialog: DialogInterface, _: Int -> lifecycleScope.launch { - if (!onUpdateFocus(focus)) { + if (!onUpdateFocus(dialogBinding.focusIndicator.getFocus())) { showFailedFocusMessage() } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt index fb263a7337..441193d6bf 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt @@ -6,8 +6,10 @@ import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import android.graphics.Path +import android.graphics.Point import android.graphics.drawable.Drawable import android.util.AttributeSet +import android.util.Log import android.view.MotionEvent import android.view.View import androidx.appcompat.content.res.AppCompatResources @@ -20,25 +22,48 @@ class FocusIndicatorView attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { - private var posX = 0f - private var posY = 0f + private var focus: Attachment.Focus? = null + private var imageSize: Point? = null + + fun setImageSize(width: Int, height: Int) { + this.imageSize = Point(width, height) + if (focus != null) + invalidate() + } fun setFocus(focus: Attachment.Focus) { - // TODO - invalidate() + this.focus = focus + if (imageSize != null) + invalidate() } - fun getFocus() { - // TODO - invalidate() + // Assumes setFocus called first + fun getFocus(): Attachment.Focus { + return focus!! + } + + // Remember focus uses -1..1 y-down coordinates + private fun axisToFocus(value: Float, innerLimit: Int, outerLimit: Int) : Float { + val offset = (outerLimit-innerLimit)/2 + val result = (value-offset).toFloat()/innerLimit.toFloat() * -2.0f + 1.0f // To range -1..1 + return Math.min(1.0f, Math.max(-1.0f, result)) // Clamp + } + + private fun axisFromFocus(value:Float, innerLimit: Int, outerLimit: Int) : Float { + val offset = (outerLimit-innerLimit)/2 + return offset.toFloat() + ((-value+1.0f)/2.0f)*innerLimit.toFloat() // From range -1..1 } override fun onTouchEvent(event: MotionEvent): Boolean { - // TODO: only handle events if they are on top of the image below - // TODO: don't handle all event actions + if (event.getActionMasked() == MotionEvent.ACTION_CANCEL) + return false - posX = event.x - posY = event.y + val imageSize = this.imageSize + if (imageSize == null) + return false + + // Convert touch xy to point inside image + focus = Attachment.Focus(axisToFocus(event.x, imageSize.x, getWidth()), axisToFocus(event.y, imageSize.y, getHeight())) invalidate() return true } @@ -61,17 +86,29 @@ class FocusIndicatorView override fun onDraw(canvas: Canvas) { super.onDraw(canvas) - val width = getWidth().toFloat() - val height = getHeight().toFloat() - val circleRadius = Math.min(width, height) / 4.0f + val imageSize = this.imageSize + val focus = this.focus + + if (imageSize != null && focus != null) { + val x = axisFromFocus(focus.x, imageSize.x, getWidth()) + val y = axisFromFocus(focus.y, imageSize.y, getHeight()) + val width = getWidth().toFloat() + val height = getHeight().toFloat() + val circleRadius = Math.min(width, height) / 4.0f - val curtainPath = Path() // Draw a flood fill with a hole cut out of it - curtainPath.setFillType(Path.FillType.WINDING) - curtainPath.addRect(0.0f, 0.0f, width, height, Path.Direction.CW) - curtainPath.addCircle(posX, posY, circleRadius, Path.Direction.CCW) - canvas.drawPath(curtainPath, curtainPaint) + val curtainPath = Path() // Draw a flood fill with a hole cut out of it + curtainPath.setFillType(Path.FillType.WINDING) + curtainPath.addRect(0.0f, 0.0f, width, height, Path.Direction.CW) + curtainPath.addCircle(x, y, circleRadius, Path.Direction.CCW) + canvas.drawPath(curtainPath, curtainPaint) - canvas.drawCircle(posX, posY, circleRadius, strokePaint) // Draw white circle - canvas.drawCircle(posX, posY, strokeWidth / 2.0f, strokePaint) // Draw white dot + canvas.drawCircle( + x, + y, + circleRadius, + strokePaint + ) // Draw white circle + canvas.drawCircle(x, y, strokeWidth / 2.0f, strokePaint) // Draw white dot + } } } \ No newline at end of file From 0e6c9427dbfd9bbad75a191f66b581bfc0870cd5 Mon Sep 17 00:00:00 2001 From: mcc Date: Mon, 5 Sep 2022 18:18:02 -0400 Subject: [PATCH 16/27] ktlint fixes and comments --- .../components/compose/dialog/FocusDialog.kt | 28 ++----------------- .../compose/view/FocusIndicatorView.kt | 26 ++++++----------- 2 files changed, 11 insertions(+), 43 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt index 5430e6f38a..0f1edbb213 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt @@ -17,45 +17,23 @@ package com.keylesspalace.tusky.components.compose.dialog import android.app.Activity import android.content.DialogInterface -import android.graphics.Bitmap -import android.graphics.Canvas -import android.graphics.Color -import android.graphics.Matrix -import android.graphics.Paint -import android.graphics.Path import android.graphics.drawable.Drawable import android.net.Uri -import android.os.Build import android.view.WindowManager -import android.widget.ImageView -import android.widget.LinearLayout import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope -import at.connyduck.sparkbutton.helpers.Utils import com.bumptech.glide.Glide import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.engine.GlideException -import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool -import com.bumptech.glide.load.resource.bitmap.BitmapTransformation import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy -import com.bumptech.glide.load.resource.bitmap.TransformationUtils import com.bumptech.glide.request.RequestListener -import com.bumptech.glide.request.target.CustomTarget -import com.bumptech.glide.request.target.SizeReadyCallback import com.bumptech.glide.request.target.Target -import com.bumptech.glide.request.transition.Transition -import com.bumptech.glide.util.Util -import com.github.chrisbanes.photoview.OnPhotoTapListener -import com.github.chrisbanes.photoview.PhotoView import com.keylesspalace.tusky.R import com.keylesspalace.tusky.databinding.DialogFocusBinding import com.keylesspalace.tusky.entity.Attachment.Focus import kotlinx.coroutines.launch -import java.nio.ByteBuffer -import java.security.MessageDigest -import java.util.concurrent.locks.Lock fun T.makeFocusDialog( existingFocus: Focus?, @@ -71,14 +49,14 @@ fun T.makeFocusDialog( Glide.with(this) .load(previewUri) .downsample(DownsampleStrategy.CENTER_INSIDE) - .listener(object: RequestListener { - override fun onLoadFailed(p0: GlideException?, p1:Any?, p2:Target?, p3: Boolean): Boolean { + .listener(object : RequestListener { + override fun onLoadFailed(p0: GlideException?, p1: Any?, p2: Target?, p3: Boolean): Boolean { return false } override fun onResourceReady(resource: Drawable?, model: Any?, target: Target?, dataSource: DataSource?, isFirstResource: Boolean): Boolean { dialogBinding.focusIndicator.setImageSize(resource!!.getIntrinsicWidth(), resource.getIntrinsicHeight()) - return false + return false // Pass through } }) .into(dialogBinding.imageView) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt index 441193d6bf..ca0becb72b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt @@ -1,19 +1,14 @@ package com.keylesspalace.tusky.components.compose.view import android.content.Context -import android.graphics.BlendMode import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import android.graphics.Path import android.graphics.Point -import android.graphics.drawable.Drawable import android.util.AttributeSet -import android.util.Log import android.view.MotionEvent import android.view.View -import androidx.appcompat.content.res.AppCompatResources -import com.keylesspalace.tusky.R import com.keylesspalace.tusky.entity.Attachment class FocusIndicatorView @@ -43,15 +38,15 @@ class FocusIndicatorView } // Remember focus uses -1..1 y-down coordinates - private fun axisToFocus(value: Float, innerLimit: Int, outerLimit: Int) : Float { - val offset = (outerLimit-innerLimit)/2 - val result = (value-offset).toFloat()/innerLimit.toFloat() * -2.0f + 1.0f // To range -1..1 + private fun axisToFocus(value: Float, innerLimit: Int, outerLimit: Int): Float { + val offset = (outerLimit - innerLimit) / 2 // Assume image is centered in widget frame + val result = (value - offset).toFloat() / innerLimit.toFloat() * -2.0f + 1.0f // To range -1..1 return Math.min(1.0f, Math.max(-1.0f, result)) // Clamp } - private fun axisFromFocus(value:Float, innerLimit: Int, outerLimit: Int) : Float { - val offset = (outerLimit-innerLimit)/2 - return offset.toFloat() + ((-value+1.0f)/2.0f)*innerLimit.toFloat() // From range -1..1 + private fun axisFromFocus(value: Float, innerLimit: Int, outerLimit: Int): Float { + val offset = (outerLimit - innerLimit) / 2 + return offset.toFloat() + ((-value + 1.0f) / 2.0f) * innerLimit.toFloat() // From range -1..1 } override fun onTouchEvent(event: MotionEvent): Boolean { @@ -102,13 +97,8 @@ class FocusIndicatorView curtainPath.addCircle(x, y, circleRadius, Path.Direction.CCW) canvas.drawPath(curtainPath, curtainPaint) - canvas.drawCircle( - x, - y, - circleRadius, - strokePaint - ) // Draw white circle + canvas.drawCircle(x, y, circleRadius, strokePaint) // Draw white circle canvas.drawCircle(x, y, strokeWidth / 2.0f, strokePaint) // Draw white dot } } -} \ No newline at end of file +} From f4e4f8b2b99b1f1a089ed024eaf98145f9b65144 Mon Sep 17 00:00:00 2001 From: mcc Date: Mon, 5 Sep 2022 18:37:28 -0400 Subject: [PATCH 17/27] Focus indicator drawing should use device-independent pixels --- .../tusky/components/compose/view/FocusIndicatorView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt index ca0becb72b..40a73132ab 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt @@ -64,7 +64,7 @@ class FocusIndicatorView } private val transparentDarkGray = 0x40000000 - private val strokeWidth = 10.0f + private val strokeWidth = 4.0f * getResources().getDisplayMetrics().density private val curtainPaint = Paint(Paint.ANTI_ALIAS_FLAG) private val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG) From d929b980cabfbad4c561589d91af1ef88bac8423 Mon Sep 17 00:00:00 2001 From: mcc Date: Sun, 18 Sep 2022 19:26:04 -0400 Subject: [PATCH 18/27] Shrink focus window when it gets unattractively tall (no linting, misbehaves on wide aspect ratio screens) --- .../components/compose/dialog/FocusDialog.kt | 23 ++++++++++++++++++- .../compose/view/FocusIndicatorView.kt | 10 +++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt index 0f1edbb213..3714c697a4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt @@ -19,7 +19,10 @@ import android.app.Activity import android.content.DialogInterface import android.graphics.drawable.Drawable import android.net.Uri +import android.util.Log import android.view.WindowManager +import android.widget.FrameLayout +import android.widget.LinearLayout import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.lifecycle.LifecycleOwner @@ -55,7 +58,25 @@ fun T.makeFocusDialog( } override fun onResourceReady(resource: Drawable?, model: Any?, target: Target?, dataSource: DataSource?, isFirstResource: Boolean): Boolean { - dialogBinding.focusIndicator.setImageSize(resource!!.getIntrinsicWidth(), resource.getIntrinsicHeight()) + val width = resource!!.getIntrinsicWidth() + val height = resource.getIntrinsicHeight() + + dialogBinding.focusIndicator.setImageSize(width, height) + + // We want the dialog to be a little taller than the image, so you can slide your thumb past the image border, + // but if it's *too* much taller that looks weird. See if a threshold has been crossed: + if (width > height) { + val maxHeight = dialogBinding.focusIndicator.maxAttractiveHeight() + Log.w( + "TUSKYFOCUS", + "Resource $width x $height View ${dialogBinding.imageView.getWidth()} x ${dialogBinding.imageView.getHeight()} Max $maxHeight" + ) + if (dialogBinding.imageView.getHeight() > maxHeight) { + val verticalShrinkLayout = FrameLayout.LayoutParams(width, maxHeight) + dialogBinding.imageView.setLayoutParams(verticalShrinkLayout) + dialogBinding.focusIndicator.setLayoutParams(verticalShrinkLayout) + } + } return false // Pass through } }) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt index 40a73132ab..eb6cf38090 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt @@ -89,7 +89,7 @@ class FocusIndicatorView val y = axisFromFocus(focus.y, imageSize.y, getHeight()) val width = getWidth().toFloat() val height = getHeight().toFloat() - val circleRadius = Math.min(width, height) / 4.0f + val circleRadius = width / 4.0f val curtainPath = Path() // Draw a flood fill with a hole cut out of it curtainPath.setFillType(Path.FillType.WINDING) @@ -101,4 +101,12 @@ class FocusIndicatorView canvas.drawCircle(x, y, strokeWidth / 2.0f, strokePaint) // Draw white dot } } + + // Give a "safe" height based on currently set image size. Assume imageSize is set and height>width already checked + fun maxAttractiveHeight(): Int { + val height = this.imageSize!!.y + // As hardcoded above, the focus indicator circle is radius 1/4 of the width + // So give us enough space for the image, plus on each size a focus indicator circle plus a strokeWidth + return Math.ceil(height.toDouble() + getWidth()/2.0 + strokeWidth).toInt() + } } From dab638157609432b0f60143853916c6ecbc40579 Mon Sep 17 00:00:00 2001 From: mcc Date: Sun, 18 Sep 2022 19:39:04 -0400 Subject: [PATCH 19/27] Correct max-height behavior for screens in landscape mode --- .../compose/view/FocusIndicatorView.kt | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt index eb6cf38090..1cfedb6841 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt @@ -19,6 +19,7 @@ class FocusIndicatorView ) : View(context, attrs, defStyleAttr) { private var focus: Attachment.Focus? = null private var imageSize: Point? = null + private var circleRadius: Float? = null fun setImageSize(width: Int, height: Int) { this.imageSize = Point(width, height) @@ -37,6 +38,17 @@ class FocusIndicatorView return focus!! } + // This needs to be consistent every time it is consulted over the lifetime of the object, + // so base it on the view width/height whenever the first access occurs. + fun getCirleRadius(): Float { + val circleRadius = this.circleRadius + if (circleRadius != null) + return circleRadius + val newCircleRadius = Math.min(getWidth(), getHeight()).toFloat() / 4.0f + this.circleRadius = newCircleRadius + return newCircleRadius + } + // Remember focus uses -1..1 y-down coordinates private fun axisToFocus(value: Float, innerLimit: Int, outerLimit: Int): Float { val offset = (outerLimit - innerLimit) / 2 // Assume image is centered in widget frame @@ -87,13 +99,11 @@ class FocusIndicatorView if (imageSize != null && focus != null) { val x = axisFromFocus(focus.x, imageSize.x, getWidth()) val y = axisFromFocus(focus.y, imageSize.y, getHeight()) - val width = getWidth().toFloat() - val height = getHeight().toFloat() - val circleRadius = width / 4.0f + val circleRadius = getCirleRadius() val curtainPath = Path() // Draw a flood fill with a hole cut out of it curtainPath.setFillType(Path.FillType.WINDING) - curtainPath.addRect(0.0f, 0.0f, width, height, Path.Direction.CW) + curtainPath.addRect(0.0f, 0.0f, getWidth().toFloat(), getHeight().toFloat(), Path.Direction.CW) curtainPath.addCircle(x, y, circleRadius, Path.Direction.CCW) canvas.drawPath(curtainPath, curtainPaint) @@ -105,8 +115,9 @@ class FocusIndicatorView // Give a "safe" height based on currently set image size. Assume imageSize is set and height>width already checked fun maxAttractiveHeight(): Int { val height = this.imageSize!!.y - // As hardcoded above, the focus indicator circle is radius 1/4 of the width - // So give us enough space for the image, plus on each size a focus indicator circle plus a strokeWidth - return Math.ceil(height.toDouble() + getWidth()/2.0 + strokeWidth).toInt() + val circleRadius = getCirleRadius() + + // Give us enough space for the image, plus on each side half a focus indicator circle, plus a strokeWidth + return Math.ceil((height.toFloat() + circleRadius*2.0f + strokeWidth).toDouble()).toInt() } } From af5c995488bd10dd5f971eae77ff5cccfdabf02a Mon Sep 17 00:00:00 2001 From: mcc Date: Mon, 19 Sep 2022 10:50:46 -0400 Subject: [PATCH 20/27] Focus attachment result is are flipped on x axis; fix this --- .../components/compose/view/FocusIndicatorView.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt index 1cfedb6841..2cdb8de55a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt @@ -49,16 +49,16 @@ class FocusIndicatorView return newCircleRadius } - // Remember focus uses -1..1 y-down coordinates + // Remember focus uses -1..1 y-down coordinates (so focus value should be negated for y) private fun axisToFocus(value: Float, innerLimit: Int, outerLimit: Int): Float { val offset = (outerLimit - innerLimit) / 2 // Assume image is centered in widget frame - val result = (value - offset).toFloat() / innerLimit.toFloat() * -2.0f + 1.0f // To range -1..1 + val result = (value - offset).toFloat() / innerLimit.toFloat() * 2.0f - 1.0f // To range -1..1 return Math.min(1.0f, Math.max(-1.0f, result)) // Clamp } private fun axisFromFocus(value: Float, innerLimit: Int, outerLimit: Int): Float { val offset = (outerLimit - innerLimit) / 2 - return offset.toFloat() + ((-value + 1.0f) / 2.0f) * innerLimit.toFloat() // From range -1..1 + return offset.toFloat() + ((value + 1.0f) / 2.0f) * innerLimit.toFloat() // From range -1..1 } override fun onTouchEvent(event: MotionEvent): Boolean { @@ -70,7 +70,7 @@ class FocusIndicatorView return false // Convert touch xy to point inside image - focus = Attachment.Focus(axisToFocus(event.x, imageSize.x, getWidth()), axisToFocus(event.y, imageSize.y, getHeight())) + focus = Attachment.Focus(axisToFocus(event.x, imageSize.x, getWidth()), -axisToFocus(event.y, imageSize.y, getHeight())) invalidate() return true } @@ -98,7 +98,7 @@ class FocusIndicatorView if (imageSize != null && focus != null) { val x = axisFromFocus(focus.x, imageSize.x, getWidth()) - val y = axisFromFocus(focus.y, imageSize.y, getHeight()) + val y = axisFromFocus(-focus.y, imageSize.y, getHeight()) val circleRadius = getCirleRadius() val curtainPath = Path() // Draw a flood fill with a hole cut out of it From 829c15138d1d7803a550c9f5cd5a7a0a5928c15e Mon Sep 17 00:00:00 2001 From: mcc Date: Mon, 19 Sep 2022 14:33:39 -0400 Subject: [PATCH 21/27] Correctly thread focus through on scheduled posts, redrafted posts, and drafts (but draft focus is lost on post) --- .../components/compose/ComposeActivity.kt | 1 + .../components/compose/ComposeViewModel.kt | 23 +++++++++++++------ .../tusky/components/drafts/DraftHelper.kt | 3 +++ .../components/drafts/DraftMediaAdapter.kt | 19 +++++++++++---- .../com/keylesspalace/tusky/db/DraftEntity.kt | 2 ++ .../receiver/SendStatusBroadcastReceiver.kt | 1 + .../tusky/service/SendStatusService.kt | 3 +++ 7 files changed, 41 insertions(+), 11 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 e872ff47d7..a202bdaa35 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 @@ -170,6 +170,7 @@ class ComposeActivity : uriNew, size, itemOld.description, + null, // Intentionally reset focus when cropping itemOld ) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt index 0d7f47043f..16074a9fac 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt @@ -107,7 +107,7 @@ class ComposeViewModel @Inject constructor( } } - suspend fun pickMedia(mediaUri: Uri, description: String? = null): Result = withContext(Dispatchers.IO) { + suspend fun pickMedia(mediaUri: Uri, description: String? = null, focus: Attachment.Focus? = null): Result = withContext(Dispatchers.IO) { try { val (type, uri, size) = mediaUploader.prepareMedia(mediaUri) val mediaItems = media.value @@ -117,7 +117,7 @@ class ComposeViewModel @Inject constructor( ) { Result.failure(VideoOrImageException()) } else { - val queuedMedia = addMediaToQueue(type, uri, size, description) + val queuedMedia = addMediaToQueue(type, uri, size, description, focus) Result.success(queuedMedia) } } catch (e: Exception) { @@ -130,6 +130,7 @@ class ComposeViewModel @Inject constructor( uri: Uri, mediaSize: Long, description: String? = null, + focus: Attachment.Focus? = null, replaceItem: QueuedMedia? = null ): QueuedMedia { var stashMediaItem: QueuedMedia? = null @@ -140,7 +141,8 @@ class ComposeViewModel @Inject constructor( uri = uri, type = type, mediaSize = mediaSize, - description = description + description = description, + focus = focus ) stashMediaItem = mediaItem @@ -185,7 +187,7 @@ class ComposeViewModel @Inject constructor( return mediaItem } - private fun addUploadedMedia(id: String, type: QueuedMedia.Type, uri: Uri, description: String?) { + private fun addUploadedMedia(id: String, type: QueuedMedia.Type, uri: Uri, description: String?, focus: Attachment.Focus?) { media.update { mediaValue -> val mediaItem = QueuedMedia( localId = (mediaValue.maxOfOrNull { it.localId } ?: 0) + 1, @@ -194,7 +196,8 @@ class ComposeViewModel @Inject constructor( mediaSize = 0, uploadPercent = -1, id = id, - description = description + description = description, + focus = focus ) mediaValue + mediaItem } @@ -248,9 +251,11 @@ class ComposeViewModel @Inject constructor( suspend fun saveDraft(content: String, contentWarning: String) { val mediaUris: MutableList = mutableListOf() val mediaDescriptions: MutableList = mutableListOf() + val mediaFocus: MutableList = mutableListOf() media.value.forEach { item -> mediaUris.add(item.uri.toString()) mediaDescriptions.add(item.description) + mediaFocus.add(item.focus) } draftHelper.saveDraft( @@ -263,6 +268,7 @@ class ComposeViewModel @Inject constructor( visibility = statusVisibility.value!!, mediaUris = mediaUris, mediaDescriptions = mediaDescriptions, + mediaFocus = mediaFocus, poll = poll.value, failedToSend = false ) @@ -290,11 +296,13 @@ class ComposeViewModel @Inject constructor( val mediaIds: MutableList = mutableListOf() val mediaUris: MutableList = mutableListOf() val mediaDescriptions: MutableList = mutableListOf() + val mediaFocus: MutableList = mutableListOf() val mediaProcessed: MutableList = mutableListOf() for (item in media.value) { mediaIds.add(item.id!!) mediaUris.add(item.uri) mediaDescriptions.add(item.description ?: "") + mediaFocus.add(item.focus) mediaProcessed.add(false) } @@ -306,6 +314,7 @@ class ComposeViewModel @Inject constructor( mediaIds = mediaIds, mediaUris = mediaUris.map { it.toString() }, mediaDescriptions = mediaDescriptions, + mediaFocus = mediaFocus, scheduledAt = scheduledAt.value, inReplyToId = inReplyToId, poll = poll.value, @@ -433,7 +442,7 @@ class ComposeViewModel @Inject constructor( // when coming from DraftActivity viewModelScope.launch { draftAttachments.forEach { attachment -> - pickMedia(attachment.uri, attachment.description) + pickMedia(attachment.uri, attachment.description, attachment.focus) } } } else composeOptions?.mediaAttachments?.forEach { a -> @@ -443,7 +452,7 @@ class ComposeViewModel @Inject constructor( Attachment.Type.UNKNOWN, Attachment.Type.IMAGE -> QueuedMedia.Type.IMAGE Attachment.Type.AUDIO -> QueuedMedia.Type.AUDIO } - addUploadedMedia(a.id, mediaType, a.url.toUri(), a.description) + addUploadedMedia(a.id, mediaType, a.url.toUri(), a.description, a.meta?.focus) } draftId = composeOptions?.draftId ?: 0 diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt index a6cd3fcd70..1816fd1ab6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt @@ -25,6 +25,7 @@ import com.keylesspalace.tusky.BuildConfig import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.db.DraftAttachment import com.keylesspalace.tusky.db.DraftEntity +import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.NewPoll import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.util.IOUtils @@ -59,6 +60,7 @@ class DraftHelper @Inject constructor( visibility: Status.Visibility, mediaUris: List, mediaDescriptions: List, + mediaFocus: List, poll: NewPoll?, failedToSend: Boolean ) = withContext(Dispatchers.IO) { @@ -101,6 +103,7 @@ class DraftHelper @Inject constructor( DraftAttachment( uriString = uris[i].toString(), description = mediaDescriptions[i], + focus = mediaFocus[i], type = types[i] ) ) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftMediaAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftMediaAdapter.kt index acee683b61..40c61f9bb7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftMediaAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftMediaAdapter.kt @@ -26,6 +26,7 @@ import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy import com.keylesspalace.tusky.R import com.keylesspalace.tusky.db.DraftAttachment +import com.keylesspalace.tusky.view.MediaPreviewImageView class DraftMediaAdapter( private val attachmentClick: () -> Unit @@ -42,24 +43,34 @@ class DraftMediaAdapter( ) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DraftMediaViewHolder { - return DraftMediaViewHolder(AppCompatImageView(parent.context)) + return DraftMediaViewHolder(MediaPreviewImageView(parent.context)) } override fun onBindViewHolder(holder: DraftMediaViewHolder, position: Int) { getItem(position)?.let { attachment -> if (attachment.type == DraftAttachment.Type.AUDIO) { + holder.imageView.clearFocus() holder.imageView.setImageResource(R.drawable.ic_music_box_preview_24dp) } else { - Glide.with(holder.itemView.context) + if (attachment.focus != null) + holder.imageView.setFocalPoint(attachment.focus) + else + holder.imageView.clearFocus() + var glide = Glide.with(holder.itemView.context) .load(attachment.uri) .diskCacheStrategy(DiskCacheStrategy.NONE) .dontAnimate() - .into(holder.imageView) + .centerInside() + + if (attachment.focus != null) + glide = glide.addListener(holder.imageView) + + glide.into(holder.imageView) } } } - inner class DraftMediaViewHolder(val imageView: ImageView) : + inner class DraftMediaViewHolder(val imageView: MediaPreviewImageView) : RecyclerView.ViewHolder(imageView) { init { val thumbnailViewSize = diff --git a/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt index a1e19c75c4..41341ae305 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt @@ -22,6 +22,7 @@ import androidx.room.Entity import androidx.room.PrimaryKey import androidx.room.TypeConverters import com.google.gson.annotations.SerializedName +import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.NewPoll import com.keylesspalace.tusky.entity.Status import kotlinx.parcelize.Parcelize @@ -50,6 +51,7 @@ data class DraftEntity( data class DraftAttachment( @SerializedName(value = "uriString", alternate = ["e", "i"]) val uriString: String, @SerializedName(value = "description", alternate = ["f", "j"]) val description: String?, + @SerializedName(value = "focus") val focus: Attachment.Focus?, @SerializedName(value = "type", alternate = ["g", "k"]) val type: Type ) : Parcelable { val uri: Uri diff --git a/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt index a0eac8334f..116f062d20 100644 --- a/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt +++ b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt @@ -92,6 +92,7 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() { mediaIds = emptyList(), mediaUris = emptyList(), mediaDescriptions = emptyList(), + mediaFocus = emptyList(), scheduledAt = null, inReplyToId = citedStatusId, poll = null, diff --git a/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt b/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt index 20ad8de8b7..d8a372ca8e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt +++ b/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt @@ -24,6 +24,7 @@ import com.keylesspalace.tusky.components.drafts.DraftHelper import com.keylesspalace.tusky.components.notifications.NotificationHelper import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.di.Injectable +import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.NewPoll import com.keylesspalace.tusky.entity.NewStatus import com.keylesspalace.tusky.entity.Status @@ -256,6 +257,7 @@ class SendStatusService : Service(), Injectable { visibility = Status.Visibility.byString(status.visibility), mediaUris = status.mediaUris, mediaDescriptions = status.mediaDescriptions, + mediaFocus = status.mediaFocus, poll = status.poll, failedToSend = true ) @@ -322,6 +324,7 @@ data class StatusToSend( val mediaIds: List, val mediaUris: List, val mediaDescriptions: List, + val mediaFocus: List, val scheduledAt: String?, val inReplyToId: String?, val poll: NewPoll?, From cde5d5e384d893ff727d3fa692b0bfa27e495695 Mon Sep 17 00:00:00 2001 From: mcc Date: Mon, 19 Sep 2022 14:40:38 -0400 Subject: [PATCH 22/27] More focus ktlint fixes --- .../tusky/components/compose/dialog/FocusDialog.kt | 7 +------ .../tusky/components/compose/view/FocusIndicatorView.kt | 2 +- .../tusky/components/drafts/DraftMediaAdapter.kt | 1 - 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt index 3714c697a4..a486d09d74 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt @@ -19,10 +19,8 @@ import android.app.Activity import android.content.DialogInterface import android.graphics.drawable.Drawable import android.net.Uri -import android.util.Log import android.view.WindowManager import android.widget.FrameLayout -import android.widget.LinearLayout import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.lifecycle.LifecycleOwner @@ -67,10 +65,7 @@ fun T.makeFocusDialog( // but if it's *too* much taller that looks weird. See if a threshold has been crossed: if (width > height) { val maxHeight = dialogBinding.focusIndicator.maxAttractiveHeight() - Log.w( - "TUSKYFOCUS", - "Resource $width x $height View ${dialogBinding.imageView.getWidth()} x ${dialogBinding.imageView.getHeight()} Max $maxHeight" - ) + if (dialogBinding.imageView.getHeight() > maxHeight) { val verticalShrinkLayout = FrameLayout.LayoutParams(width, maxHeight) dialogBinding.imageView.setLayoutParams(verticalShrinkLayout) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt index 2cdb8de55a..dcdf88f1fa 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt @@ -118,6 +118,6 @@ class FocusIndicatorView val circleRadius = getCirleRadius() // Give us enough space for the image, plus on each side half a focus indicator circle, plus a strokeWidth - return Math.ceil((height.toFloat() + circleRadius*2.0f + strokeWidth).toDouble()).toInt() + return Math.ceil((height.toFloat() + circleRadius * 2.0f + strokeWidth).toDouble()).toInt() } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftMediaAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftMediaAdapter.kt index 40c61f9bb7..98a288b426 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftMediaAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftMediaAdapter.kt @@ -17,7 +17,6 @@ package com.keylesspalace.tusky.components.drafts import android.view.ViewGroup import android.widget.ImageView -import androidx.appcompat.widget.AppCompatImageView import androidx.constraintlayout.widget.ConstraintLayout import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter From a93369ba7567b0c8d9ca9852196131800c31a0bd Mon Sep 17 00:00:00 2001 From: mcc Date: Mon, 19 Sep 2022 14:53:43 -0400 Subject: [PATCH 23/27] Fix specific case where a draft is given a focus, then deleted, then posted in that order --- .../tusky/components/compose/MediaUploader.kt | 8 +++++++- .../com/keylesspalace/tusky/network/MediaUploadApi.kt | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt index 324540d127..51e745156c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt @@ -225,7 +225,13 @@ class MediaUploader @Inject constructor( null } - mediaUploadApi.uploadMedia(body, description).fold({ result -> + val focus = if (media.focus != null) { + MultipartBody.Part.createFormData("focus", "${media.focus.x},${media.focus.y}") + } else { + null + } + + mediaUploadApi.uploadMedia(body, description, focus).fold({ result -> send(UploadEvent.FinishedEvent(result.id)) }, { throwable -> val errorMessage = throwable.getServerErrorMessage() diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MediaUploadApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MediaUploadApi.kt index a179e71d2d..24636a641a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MediaUploadApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MediaUploadApi.kt @@ -15,6 +15,7 @@ interface MediaUploadApi { @POST("api/v2/media") suspend fun uploadMedia( @Part file: MultipartBody.Part, - @Part description: MultipartBody.Part? = null + @Part description: MultipartBody.Part? = null, + @Part focus: MultipartBody.Part? = null ): NetworkResult } From 400683d24bcfb3788dbb930e3e8de966a6a5cdab Mon Sep 17 00:00:00 2001 From: mcc Date: Mon, 19 Sep 2022 15:04:34 -0400 Subject: [PATCH 24/27] Fix accidental file change in focus PR --- instance-build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/instance-build.gradle b/instance-build.gradle index 50aff7e46c..eec6fcac12 100644 --- a/instance-build.gradle +++ b/instance-build.gradle @@ -4,10 +4,10 @@ Note: Publishing a custom build on Google Play may violate the Google Play devel */ // The app name -ext.APP_NAME = "Tusky-DEV" +ext.APP_NAME = "Tusky" // The application id. Must be unique, e.g. based on your domain -ext.APP_ID = "com.keylesspalace.tusky_DEV" +ext.APP_ID = "com.keylesspalace.tusky" // url of a custom app logo. Recommended size at least 600x600. Keep empty to use the Tusky elephant friend. ext.CUSTOM_LOGO_URL = "" From 829a0ed45b24a5d5ab51385129186011e0a869e0 Mon Sep 17 00:00:00 2001 From: mcc Date: Mon, 19 Sep 2022 16:04:24 -0400 Subject: [PATCH 25/27] ktLint fix --- .../keylesspalace/tusky/components/compose/ComposeActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f331f8a2be..cd4b57ddcf 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,8 +68,8 @@ 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.makeFocusDialog import com.keylesspalace.tusky.components.compose.dialog.CaptionDialog +import com.keylesspalace.tusky.components.compose.dialog.makeFocusDialog import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog import com.keylesspalace.tusky.components.compose.view.ComposeOptionsListener import com.keylesspalace.tusky.components.compose.view.ComposeScheduleView From fc771f3094a7e6c8f2fddcafc0bf044625b0d9aa Mon Sep 17 00:00:00 2001 From: mcc Date: Tue, 20 Sep 2022 17:51:06 -0400 Subject: [PATCH 26/27] Fix property style warnings in focus --- .../tusky/components/compose/dialog/FocusDialog.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt index a486d09d74..4764ec544a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt @@ -56,8 +56,8 @@ fun T.makeFocusDialog( } override fun onResourceReady(resource: Drawable?, model: Any?, target: Target?, dataSource: DataSource?, isFirstResource: Boolean): Boolean { - val width = resource!!.getIntrinsicWidth() - val height = resource.getIntrinsicHeight() + val width = resource!!.intrinsicWidth + val height = resource.intrinsicHeight dialogBinding.focusIndicator.setImageSize(width, height) @@ -66,10 +66,10 @@ fun T.makeFocusDialog( if (width > height) { val maxHeight = dialogBinding.focusIndicator.maxAttractiveHeight() - if (dialogBinding.imageView.getHeight() > maxHeight) { + if (dialogBinding.imageView.height > maxHeight) { val verticalShrinkLayout = FrameLayout.LayoutParams(width, maxHeight) - dialogBinding.imageView.setLayoutParams(verticalShrinkLayout) - dialogBinding.focusIndicator.setLayoutParams(verticalShrinkLayout) + dialogBinding.imageView.layoutParams = verticalShrinkLayout + dialogBinding.focusIndicator.layoutParams = verticalShrinkLayout } } return false // Pass through From 33d5745a6f66d345694a87dd5005be062909d725 Mon Sep 17 00:00:00 2001 From: mcc Date: Tue, 20 Sep 2022 18:26:41 -0400 Subject: [PATCH 27/27] Fix remaining style warnings from focus PR --- .../components/compose/MediaPreviewAdapter.kt | 2 +- .../compose/view/FocusIndicatorView.kt | 45 +++++++++++-------- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt index 41afde5115..2855e69697 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt @@ -89,7 +89,7 @@ class MediaPreviewAdapter( if (focus != null) imageView.setFocalPoint(focus) else - imageView.removeFocalPoint(); // Probably unnecessary since we have no UI for removal once added. + imageView.removeFocalPoint() // Probably unnecessary since we have no UI for removal once added. var glide = Glide.with(holder.itemView.context) .load(item.uri) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt index dcdf88f1fa..9a3e4b00a0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt @@ -1,5 +1,6 @@ package com.keylesspalace.tusky.components.compose.view +import android.annotation.SuppressLint import android.content.Context import android.graphics.Canvas import android.graphics.Color @@ -10,6 +11,9 @@ import android.util.AttributeSet import android.view.MotionEvent import android.view.View import com.keylesspalace.tusky.entity.Attachment +import kotlin.math.ceil +import kotlin.math.max +import kotlin.math.min class FocusIndicatorView @JvmOverloads constructor( @@ -40,11 +44,11 @@ class FocusIndicatorView // This needs to be consistent every time it is consulted over the lifetime of the object, // so base it on the view width/height whenever the first access occurs. - fun getCirleRadius(): Float { + private fun getCircleRadius(): Float { val circleRadius = this.circleRadius if (circleRadius != null) return circleRadius - val newCircleRadius = Math.min(getWidth(), getHeight()).toFloat() / 4.0f + val newCircleRadius = min(this.width, this.height).toFloat() / 4.0f this.circleRadius = newCircleRadius return newCircleRadius } @@ -52,8 +56,8 @@ class FocusIndicatorView // Remember focus uses -1..1 y-down coordinates (so focus value should be negated for y) private fun axisToFocus(value: Float, innerLimit: Int, outerLimit: Int): Float { val offset = (outerLimit - innerLimit) / 2 // Assume image is centered in widget frame - val result = (value - offset).toFloat() / innerLimit.toFloat() * 2.0f - 1.0f // To range -1..1 - return Math.min(1.0f, Math.max(-1.0f, result)) // Clamp + val result = (value - offset) / innerLimit.toFloat() * 2.0f - 1.0f // To range -1..1 + return min(1.0f, max(-1.0f, result)) // Clamp } private fun axisFromFocus(value: Float, innerLimit: Int, outerLimit: Int): Float { @@ -61,8 +65,9 @@ class FocusIndicatorView return offset.toFloat() + ((value + 1.0f) / 2.0f) * innerLimit.toFloat() // From range -1..1 } + @SuppressLint("ClickableViewAccessibility") // Android Studio wants us to implement PerformClick for accessibility, but that unfortunately cannot be made meaningful for this widget. override fun onTouchEvent(event: MotionEvent): Boolean { - if (event.getActionMasked() == MotionEvent.ACTION_CANCEL) + if (event.actionMasked == MotionEvent.ACTION_CANCEL) return false val imageSize = this.imageSize @@ -70,24 +75,26 @@ class FocusIndicatorView return false // Convert touch xy to point inside image - focus = Attachment.Focus(axisToFocus(event.x, imageSize.x, getWidth()), -axisToFocus(event.y, imageSize.y, getHeight())) + focus = Attachment.Focus(axisToFocus(event.x, imageSize.x, this.width), -axisToFocus(event.y, imageSize.y, this.height)) invalidate() return true } private val transparentDarkGray = 0x40000000 - private val strokeWidth = 4.0f * getResources().getDisplayMetrics().density + private val strokeWidth = 4.0f * this.resources.displayMetrics.density private val curtainPaint = Paint(Paint.ANTI_ALIAS_FLAG) private val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG) + private val curtainPath = Path() + init { - curtainPaint.setColor(transparentDarkGray) + curtainPaint.color = transparentDarkGray curtainPaint.style = Paint.Style.FILL - strokePaint.setStyle(Paint.Style.STROKE) - strokePaint.setStrokeWidth(strokeWidth) - strokePaint.setColor(Color.WHITE) + strokePaint.style = Paint.Style.STROKE + strokePaint.strokeWidth = strokeWidth + strokePaint.color = Color.WHITE } override fun onDraw(canvas: Canvas) { @@ -97,13 +104,13 @@ class FocusIndicatorView val focus = this.focus if (imageSize != null && focus != null) { - val x = axisFromFocus(focus.x, imageSize.x, getWidth()) - val y = axisFromFocus(-focus.y, imageSize.y, getHeight()) - val circleRadius = getCirleRadius() + val x = axisFromFocus(focus.x, imageSize.x, this.width) + val y = axisFromFocus(-focus.y, imageSize.y, this.height) + val circleRadius = getCircleRadius() - val curtainPath = Path() // Draw a flood fill with a hole cut out of it - curtainPath.setFillType(Path.FillType.WINDING) - curtainPath.addRect(0.0f, 0.0f, getWidth().toFloat(), getHeight().toFloat(), Path.Direction.CW) + curtainPath.reset() // Draw a flood fill with a hole cut out of it + curtainPath.fillType = Path.FillType.WINDING + curtainPath.addRect(0.0f, 0.0f, this.width.toFloat(), this.height.toFloat(), Path.Direction.CW) curtainPath.addCircle(x, y, circleRadius, Path.Direction.CCW) canvas.drawPath(curtainPath, curtainPaint) @@ -115,9 +122,9 @@ class FocusIndicatorView // Give a "safe" height based on currently set image size. Assume imageSize is set and height>width already checked fun maxAttractiveHeight(): Int { val height = this.imageSize!!.y - val circleRadius = getCirleRadius() + val circleRadius = getCircleRadius() // Give us enough space for the image, plus on each side half a focus indicator circle, plus a strokeWidth - return Math.ceil((height.toFloat() + circleRadius * 2.0f + strokeWidth).toDouble()).toInt() + return ceil(height.toFloat() + circleRadius * 2.0f + strokeWidth).toInt() } }