From 729b10143fb9fa2a6abff8193ca66a5d55bde5b1 Mon Sep 17 00:00:00 2001 From: tateisu Date: Sun, 26 Nov 2023 09:47:24 +0900 Subject: [PATCH] =?UTF-8?q?=E6=8A=95=E7=A8=BF=E7=94=BB=E9=9D=A2=E3=81=A7?= =?UTF-8?q?=E6=B7=BB=E4=BB=98=E3=83=A1=E3=83=87=E3=82=A3=E3=82=A2=E3=82=92?= =?UTF-8?q?=E4=B8=A6=E3=81=B9=E6=9B=BF=E3=81=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/jp/juggler/subwaytooter/ActPost.kt | 4 +- .../subwaytooter/actpost/ActPostAttachment.kt | 43 ++++ .../dialog/DlgAttachmentRearrange.kt | 205 ++++++++++++++++++ app/src/main/res/drawable/swap_horiz_24px.xml | 10 + app/src/main/res/drawable/swap_vert_24px.xml | 10 + app/src/main/res/layout/act_post.xml | 24 +- .../layout/attachment_rearrange_dialog.xml | 55 +++++ .../res/layout/attachments_rearrange_item.xml | 33 +++ app/src/main/res/values-ja/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + .../main/java/jp/juggler/util/ui/UiUtils.kt | 18 ++ 11 files changed, 399 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/jp/juggler/subwaytooter/dialog/DlgAttachmentRearrange.kt create mode 100644 app/src/main/res/drawable/swap_horiz_24px.xml create mode 100644 app/src/main/res/drawable/swap_vert_24px.xml create mode 100644 app/src/main/res/layout/attachment_rearrange_dialog.xml create mode 100644 app/src/main/res/layout/attachments_rearrange_item.xml diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt b/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt index 0ff88556..86d89ea2 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt @@ -22,6 +22,7 @@ import jp.juggler.subwaytooter.actpost.CompletionHelper import jp.juggler.subwaytooter.actpost.FeaturedTagCache import jp.juggler.subwaytooter.actpost.addAttachment import jp.juggler.subwaytooter.actpost.applyMushroomText +import jp.juggler.subwaytooter.actpost.rearrangeAttachments import jp.juggler.subwaytooter.actpost.onPickCustomThumbnailImpl import jp.juggler.subwaytooter.actpost.onPostAttachmentCompleteImpl import jp.juggler.subwaytooter.actpost.openAttachment @@ -314,7 +315,7 @@ class ActPost : AppCompatActivity(), R.id.btnFeaturedTag -> completionHelper.openFeaturedTagList( featuredTagCache[account?.acct?.ascii ?: ""]?.list ) - + R.id.btnAttachmentsRearrange -> rearrangeAttachments() R.id.ibSchedule -> performSchedule() R.id.ibScheduleReset -> resetSchedule() } @@ -445,6 +446,7 @@ class ActPost : AppCompatActivity(), views.btnEmojiPicker, views.btnMore, views.ivAccount, + views.btnAttachmentsRearrange, ).forEach { it.setOnClickListener(this) } ivMedia.forEach { it.setOnClickListener(this) } diff --git a/app/src/main/java/jp/juggler/subwaytooter/actpost/ActPostAttachment.kt b/app/src/main/java/jp/juggler/subwaytooter/actpost/ActPostAttachment.kt index 969ae07d..f373ffd3 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/actpost/ActPostAttachment.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/actpost/ActPostAttachment.kt @@ -5,6 +5,7 @@ import android.net.Uri import android.text.InputType import android.view.View import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.lifecycleScope import jp.juggler.subwaytooter.ActPost import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.api.ApiTask @@ -20,6 +21,7 @@ import jp.juggler.subwaytooter.calcIconRound import jp.juggler.subwaytooter.defaultColorIcon import jp.juggler.subwaytooter.dialog.actionsDialog import jp.juggler.subwaytooter.dialog.decodeAttachmentBitmap +import jp.juggler.subwaytooter.dialog.dialogArrachmentRearrange import jp.juggler.subwaytooter.dialog.focusPointDialog import jp.juggler.subwaytooter.dialog.showTextInputDialog import jp.juggler.subwaytooter.pref.PrefB @@ -40,6 +42,8 @@ import jp.juggler.util.log.withCaption import jp.juggler.util.network.toPutRequestBuilder import jp.juggler.util.ui.isLiveActivity import jp.juggler.util.ui.vg +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.launch import kotlin.math.min private val log = LogCategory("ActPostAttachment") @@ -64,13 +68,23 @@ fun ActPost.decodeAttachments(sv: String) { } } +fun ActPost.showAttachmentRearrangeButton() { + views.btnAttachmentsRearrange.vg( + attachmentList.size >= 2 && + attachmentList.none { it.status == PostAttachment.Status.Progress } + ) +} + fun ActPost.showMediaAttachment() { if (isFinishing) return views.llAttachment.vg(attachmentList.isNotEmpty()) ivMedia.forEachIndexed { i, v -> showMediaAttachmentOne(v, i) } + showAttachmentRearrangeButton() } fun ActPost.showMediaAttachmentProgress() { + if (isFinishing) return + showAttachmentRearrangeButton() val mergedProgress = attachmentList .mapNotNull { it.progress.notEmpty() } .joinToString("\n") @@ -436,3 +450,32 @@ fun ActPost.onPickCustomThumbnailImpl(pa: PostAttachment, src: GetContentResultE } } } + +fun ActPost.rearrangeAttachments() = lifecycleScope.launch { + try { + val rearranged = dialogArrachmentRearrange(attachmentList) + // 入れ替え中にアップロード失敗などで要素が消えることがあるので + // 最新のattachmentListを指定順に並べ替える + val remain = ArrayList(attachmentList) + val newList = buildList { + rearranged.map { a -> + val pa = remain.find { it === a } + if (pa != null) { + add(pa) + remain.remove(pa) + } + } + addAll(remain) + } + // attachmentListを更新して表示し直す + attachmentList.clear() + attachmentList.addAll(newList) + showMediaAttachment() + showMediaAttachmentProgress() + } catch (ex: Throwable) { + log.e(ex, "attachmentRearrange failed.") + if (ex !is CancellationException) { + dialogOrToast(ex.withCaption("attachmentRearrange failed.")) + } + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/dialog/DlgAttachmentRearrange.kt b/app/src/main/java/jp/juggler/subwaytooter/dialog/DlgAttachmentRearrange.kt new file mode 100644 index 00000000..21900caf --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/dialog/DlgAttachmentRearrange.kt @@ -0,0 +1,205 @@ +package jp.juggler.subwaytooter.dialog + +import android.annotation.SuppressLint +import android.app.Dialog +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.ViewGroup +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import jp.juggler.subwaytooter.ActPost +import jp.juggler.subwaytooter.R +import jp.juggler.subwaytooter.databinding.AttachmentRearrangeDialogBinding +import jp.juggler.subwaytooter.databinding.AttachmentsRearrangeItemBinding +import jp.juggler.subwaytooter.defaultColorIcon +import jp.juggler.subwaytooter.util.PostAttachment +import jp.juggler.util.data.ellipsizeDot3 +import jp.juggler.util.ui.attrColor +import jp.juggler.util.ui.dismissSafe +import jp.juggler.util.ui.dp +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.suspendCancellableCoroutine +import org.jetbrains.anko.backgroundColor +import kotlin.coroutines.resumeWithException + +suspend fun ActPost.dialogArrachmentRearrange( + initialList: List, +): List = suspendCancellableCoroutine { cont -> + val views = AttachmentRearrangeDialogBinding.inflate(layoutInflater) + val dialog = Dialog(this) + dialog.setContentView(views.root) + + cont.invokeOnCancellation { dialog.dismissSafe() } + + dialog.setOnDismissListener { + if (cont.isActive) cont.resumeWithException(CancellationException()) + } + + val rearrangeAdapter = RearrangeAdapter(layoutInflater, initialList) + + views.btnCancel.setOnClickListener { + dialog.dismissSafe() + } + + views.btnOk.setOnClickListener { + if (cont.isActive) { + cont.resume(rearrangeAdapter.list) {} + } + dialog.dismissSafe() + } + + views.listView.apply { + layoutManager = LinearLayoutManager(context) + adapter = rearrangeAdapter + rearrangeAdapter.itemTouchHelper.attachToRecyclerView(this) + } + + dialog.window?.setLayout(dp(300), dp(440)) + dialog.show() +} + +private class RearrangeAdapter( + private val inflater: LayoutInflater, + initialList: List, +) : RecyclerView.Adapter(), + MyDragCallback.Changer { + + val list = ArrayList(initialList) + + private var lastStateViewHolder: MyViewHolder? = null + private var draggingItem: PostAttachment? = null + + val itemTouchHelper by lazy { + ItemTouchHelper(MyDragCallback(this)) + } + + override fun getItemCount() = list.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = + MyViewHolder(parent) + + override fun onBindViewHolder(holder: MyViewHolder, position: Int) { + holder.bind(list.elementAtOrNull(position)) + } + + override fun onMove(posFrom: Int, posTo: Int): Boolean { + val item = list.removeAt(posFrom) + list.add(posTo, item) + notifyItemMoved(posFrom, posTo) + return true + } + + override fun onState(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { + val holder = (viewHolder as? MyViewHolder) + holder?.let { lastStateViewHolder = it } + val pa = holder?.lastItem + draggingItem = when { + pa != null && actionState == ItemTouchHelper.ACTION_STATE_DRAG -> pa + else -> null + } + holder?.bind() + lastStateViewHolder?.takeIf { it != holder }?.bind() + } + + @SuppressLint("ClickableViewAccessibility") + inner class MyViewHolder( + parent: ViewGroup, + val views: AttachmentsRearrangeItemBinding = + AttachmentsRearrangeItemBinding.inflate(inflater, parent, false), + ) : RecyclerView.ViewHolder(views.root) { + + var lastItem: PostAttachment? = null + + init { + views.root.setOnTouchListener { _, event -> + if (event.actionMasked == MotionEvent.ACTION_DOWN) { + itemTouchHelper.startDrag(this) + } + false + } + } + + fun bind(pa: PostAttachment? = lastItem) { + pa ?: return + lastItem = pa + + val context = views.root.context + + views.root.apply { + when { + draggingItem === pa -> backgroundColor = + context.attrColor(R.attr.colorSearchFormBackground) + + else -> background = null + } + } + + views.ivThumbnail.apply { + val imageUrl = pa.attachment?.preview_url + if (imageUrl.isNullOrEmpty()) { + val imageId = when (pa.status) { + PostAttachment.Status.Progress -> R.drawable.ic_upload + PostAttachment.Status.Error -> R.drawable.ic_error + else -> R.drawable.ic_clip + } + Glide.with(context).clear(this) + setImageDrawable(defaultColorIcon(context, imageId)) + } else { + Glide.with(context) + .load(imageUrl) + .placeholder(defaultColorIcon(context, R.drawable.ic_hourglass)) + .error(defaultColorIcon(context, R.drawable.ic_error)) + .fallback(defaultColorIcon(context, R.drawable.ic_clip)) + .into(this) + } + } + views.tvText.text = pa.attachment?.run { + "$type ${description?.ellipsizeDot3(40) ?: ""}" + } ?: context.getString(R.string.attachment_uploading) + } + } +} + +private class MyDragCallback( + private val changer: Changer, +) : ItemTouchHelper.SimpleCallback( + ItemTouchHelper.UP or ItemTouchHelper.DOWN, + 0 // no swipe +) { + interface Changer { + fun onMove(posFrom: Int, posTo: Int): Boolean + fun onState(viewHolder: RecyclerView.ViewHolder?, actionState: Int) + } + + override fun isLongPressDragEnabled() = false + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) = Unit + + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder, + ): Boolean = changer.onMove( + // position of drag from + viewHolder.bindingAdapterPosition, + // position of drag to + target.bindingAdapterPosition, + ) + + override fun onSelectedChanged( + viewHolder: RecyclerView.ViewHolder?, + actionState: Int, + ) { + super.onSelectedChanged(viewHolder, actionState) + changer.onState(viewHolder, actionState) + } + + override fun clearView( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + ) { + super.clearView(recyclerView, viewHolder) + changer.onState(null, ItemTouchHelper.ACTION_STATE_IDLE) + } +} diff --git a/app/src/main/res/drawable/swap_horiz_24px.xml b/app/src/main/res/drawable/swap_horiz_24px.xml new file mode 100644 index 00000000..0364c8e0 --- /dev/null +++ b/app/src/main/res/drawable/swap_horiz_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/swap_vert_24px.xml b/app/src/main/res/drawable/swap_vert_24px.xml new file mode 100644 index 00000000..69e91207 --- /dev/null +++ b/app/src/main/res/drawable/swap_vert_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/act_post.xml b/app/src/main/res/layout/act_post.xml index e276c3c2..c5519db7 100644 --- a/app/src/main/res/layout/act_post.xml +++ b/app/src/main/res/layout/act_post.xml @@ -116,14 +116,13 @@ android:textAllCaps="false" /> - + app:flexWrap="wrap"> + + - + + + + + + + + + + + +