ActionMode selection in manga lists
parent
f2175b40c0
commit
76a9a0d1ab
@ -0,0 +1,111 @@
|
||||
package org.koitharu.kotatsu.base.ui.list.decor
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.view.View
|
||||
import androidx.core.view.children
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.NO_ID
|
||||
|
||||
abstract class AbstractSelectionItemDecoration : RecyclerView.ItemDecoration() {
|
||||
|
||||
private val bounds = Rect()
|
||||
private val boundsF = RectF()
|
||||
private val selection = HashSet<Long>()
|
||||
|
||||
protected var hasBackground: Boolean = true
|
||||
protected var hasForeground: Boolean = false
|
||||
protected var isIncludeDecorAndMargins: Boolean = true
|
||||
|
||||
val checkedItemsCount: Int
|
||||
get() = selection.size
|
||||
|
||||
val checkedItemsIds: Set<Long>
|
||||
get() = selection
|
||||
|
||||
fun toggleItemChecked(id: Long) {
|
||||
if (!selection.remove(id)) {
|
||||
selection.add(id)
|
||||
}
|
||||
}
|
||||
|
||||
fun setItemIsChecked(id: Long, isChecked: Boolean) {
|
||||
if (isChecked) {
|
||||
selection.add(id)
|
||||
} else {
|
||||
selection.remove(id)
|
||||
}
|
||||
}
|
||||
|
||||
fun checkAll(ids: Collection<Long>) {
|
||||
selection.addAll(ids)
|
||||
}
|
||||
|
||||
fun clearSelection() {
|
||||
selection.clear()
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||
if (hasBackground) {
|
||||
doDraw(canvas, parent, state, false)
|
||||
} else {
|
||||
super.onDraw(canvas, parent, state)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||
if (hasForeground) {
|
||||
doDraw(canvas, parent, state, true)
|
||||
} else {
|
||||
super.onDrawOver(canvas, parent, state)
|
||||
}
|
||||
}
|
||||
|
||||
private fun doDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State, isOver: Boolean) {
|
||||
val checkpoint = canvas.save()
|
||||
if (parent.clipToPadding) {
|
||||
canvas.clipRect(
|
||||
parent.paddingLeft, parent.paddingTop, parent.width - parent.paddingRight,
|
||||
parent.height - parent.paddingBottom
|
||||
)
|
||||
}
|
||||
|
||||
for (child in parent.children) {
|
||||
val itemId = getItemId(parent, child)
|
||||
if (itemId != NO_ID && itemId in selection) {
|
||||
if (isIncludeDecorAndMargins) {
|
||||
parent.getDecoratedBoundsWithMargins(child, bounds)
|
||||
} else {
|
||||
bounds.set(child.left, child.top, child.right, child.bottom)
|
||||
}
|
||||
boundsF.set(bounds)
|
||||
boundsF.offset(child.translationX, child.translationY)
|
||||
if (isOver) {
|
||||
onDrawForeground(canvas, parent, child, boundsF, state)
|
||||
} else {
|
||||
onDrawBackground(canvas, parent, child, boundsF, state)
|
||||
}
|
||||
}
|
||||
}
|
||||
canvas.restoreToCount(checkpoint)
|
||||
}
|
||||
|
||||
protected open fun getItemId(parent: RecyclerView, child: View) = parent.getChildItemId(child)
|
||||
|
||||
protected open fun onDrawBackground(
|
||||
canvas: Canvas,
|
||||
parent: RecyclerView,
|
||||
child: View,
|
||||
bounds: RectF,
|
||||
state: RecyclerView.State,
|
||||
) = Unit
|
||||
|
||||
protected open fun onDrawForeground(
|
||||
canvas: Canvas,
|
||||
parent: RecyclerView,
|
||||
child: View,
|
||||
bounds: RectF,
|
||||
state: RecyclerView.State,
|
||||
) = Unit
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
package org.koitharu.kotatsu.base.ui.util
|
||||
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
|
||||
class ActionModeDelegate {
|
||||
|
||||
private var activeActionMode: ActionMode? = null
|
||||
private var listeners: MutableList<ActionModeListener>? = null
|
||||
|
||||
val isActionModeStarted: Boolean
|
||||
get() = activeActionMode != null
|
||||
|
||||
fun onSupportActionModeStarted(mode: ActionMode) {
|
||||
activeActionMode = mode
|
||||
listeners?.forEach { it.onActionModeStarted(mode) }
|
||||
}
|
||||
|
||||
fun onSupportActionModeFinished(mode: ActionMode) {
|
||||
activeActionMode = null
|
||||
listeners?.forEach { it.onActionModeFinished(mode) }
|
||||
}
|
||||
|
||||
fun addListener(listener: ActionModeListener) {
|
||||
if (listeners == null) {
|
||||
listeners = ArrayList()
|
||||
}
|
||||
checkNotNull(listeners).add(listener)
|
||||
}
|
||||
|
||||
fun removeListener(listener: ActionModeListener) {
|
||||
listeners?.remove(listener)
|
||||
}
|
||||
|
||||
fun addListener(listener: ActionModeListener, owner: LifecycleOwner) {
|
||||
addListener(listener)
|
||||
owner.lifecycle.addObserver(ListenerLifecycleObserver(listener))
|
||||
}
|
||||
|
||||
private inner class ListenerLifecycleObserver(
|
||||
private val listener: ActionModeListener,
|
||||
) : DefaultLifecycleObserver {
|
||||
|
||||
override fun onDestroy(owner: LifecycleOwner) {
|
||||
super.onDestroy(owner)
|
||||
removeListener(listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
package org.koitharu.kotatsu.base.ui.util
|
||||
|
||||
import androidx.appcompat.view.ActionMode
|
||||
|
||||
interface ActionModeListener {
|
||||
|
||||
fun onActionModeStarted(mode: ActionMode)
|
||||
|
||||
fun onActionModeFinished(mode: ActionMode)
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
package org.koitharu.kotatsu.list.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.RectF
|
||||
import android.view.View
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.NO_ID
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration
|
||||
import org.koitharu.kotatsu.list.ui.model.MangaItemModel
|
||||
import org.koitharu.kotatsu.utils.ext.getItem
|
||||
import org.koitharu.kotatsu.utils.ext.getThemeColor
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
class MangaSelectionDecoration(context: Context) : AbstractSelectionItemDecoration() {
|
||||
|
||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val checkIcon = ContextCompat.getDrawable(context, materialR.drawable.ic_mtrl_checked_circle)
|
||||
private val iconOffset = context.resources.getDimensionPixelOffset(R.dimen.grid_spacing_outer)
|
||||
private val strokeColor = context.getThemeColor(materialR.attr.colorPrimary, Color.RED)
|
||||
private val fillColor = ColorUtils.setAlphaComponent(
|
||||
ColorUtils.blendARGB(strokeColor, context.getThemeColor(materialR.attr.colorSurface), 0.8f),
|
||||
0x74
|
||||
)
|
||||
|
||||
init {
|
||||
hasBackground = false
|
||||
hasForeground = true
|
||||
isIncludeDecorAndMargins = false
|
||||
|
||||
paint.strokeWidth = context.resources.getDimension(R.dimen.selection_stroke_width)
|
||||
checkIcon?.setTint(strokeColor)
|
||||
}
|
||||
|
||||
override fun getItemId(parent: RecyclerView, child: View): Long {
|
||||
val holder = parent.getChildViewHolder(child) ?: return NO_ID
|
||||
val item = holder.getItem<MangaItemModel>() ?: return NO_ID
|
||||
return item.id
|
||||
}
|
||||
|
||||
override fun onDrawForeground(
|
||||
canvas: Canvas,
|
||||
parent: RecyclerView,
|
||||
child: View,
|
||||
bounds: RectF,
|
||||
state: RecyclerView.State,
|
||||
) {
|
||||
val radius = (child as? CardView)?.radius ?: 0f
|
||||
paint.color = fillColor
|
||||
paint.style = Paint.Style.FILL
|
||||
canvas.drawRoundRect(bounds, radius, radius, paint)
|
||||
paint.color = strokeColor
|
||||
paint.style = Paint.Style.STROKE
|
||||
canvas.drawRoundRect(bounds, radius, radius, paint)
|
||||
checkIcon?.run {
|
||||
setBounds(
|
||||
(bounds.left + iconOffset).toInt(),
|
||||
(bounds.top + iconOffset).toInt(),
|
||||
(bounds.left + iconOffset + intrinsicWidth).toInt(),
|
||||
(bounds.top + iconOffset + intrinsicHeight).toInt(),
|
||||
)
|
||||
draw(canvas)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package org.koitharu.kotatsu.list.ui.model
|
||||
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
|
||||
sealed interface MangaItemModel : ListModel {
|
||||
|
||||
val id: Long
|
||||
val manga: Manga
|
||||
}
|
||||
@ -1,62 +1,82 @@
|
||||
package org.koitharu.kotatsu.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.core.app.ShareCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import java.io.File
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import java.io.File
|
||||
|
||||
private const val TYPE_TEXT = "text/plain"
|
||||
private const val TYPE_IMAGE = "image/*"
|
||||
private const val TYPE_CBZ = "application/x-cbz"
|
||||
|
||||
class ShareHelper(private val context: Context) {
|
||||
|
||||
fun shareMangaLink(manga: Manga) {
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
intent.type = "text/plain"
|
||||
intent.putExtra(Intent.EXTRA_TEXT, buildString {
|
||||
val text = buildString {
|
||||
append(manga.title)
|
||||
append("\n \n")
|
||||
append(manga.publicUrl)
|
||||
})
|
||||
val shareIntent =
|
||||
Intent.createChooser(intent, context.getString(R.string.share_s, manga.title))
|
||||
context.startActivity(shareIntent)
|
||||
}
|
||||
ShareCompat.IntentBuilder(context)
|
||||
.setText(text)
|
||||
.setType(TYPE_TEXT)
|
||||
.setChooserTitle(context.getString(R.string.share_s, manga.title))
|
||||
.startChooser()
|
||||
}
|
||||
|
||||
fun shareCbz(file: File) {
|
||||
val uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.files", file)
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
intent.setDataAndType(uri, context.contentResolver.getType(uri))
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
val shareIntent =
|
||||
Intent.createChooser(intent, context.getString(R.string.share_s, file.name))
|
||||
context.startActivity(shareIntent)
|
||||
fun shareMangaLinks(manga: Collection<Manga>) {
|
||||
if (manga.isEmpty()) {
|
||||
return
|
||||
}
|
||||
if (manga.size == 1) {
|
||||
shareMangaLink(manga.first())
|
||||
return
|
||||
}
|
||||
val text = manga.joinToString("\n \n") {
|
||||
"${it.title} - ${it.publicUrl}"
|
||||
}
|
||||
ShareCompat.IntentBuilder(context)
|
||||
.setText(text)
|
||||
.setType(TYPE_TEXT)
|
||||
.setChooserTitle(R.string.share)
|
||||
.startChooser()
|
||||
}
|
||||
|
||||
fun shareBackup(file: File) {
|
||||
val uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.files", file)
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
intent.setDataAndType(uri, context.contentResolver.getType(uri))
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
val shareIntent =
|
||||
Intent.createChooser(intent, context.getString(R.string.share_s, file.name))
|
||||
context.startActivity(shareIntent)
|
||||
fun shareCbz(files: Collection<File>) {
|
||||
if (files.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val intentBuilder = ShareCompat.IntentBuilder(context)
|
||||
.setType(TYPE_CBZ)
|
||||
for (file in files) {
|
||||
val uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.files", file)
|
||||
intentBuilder.addStream(uri)
|
||||
}
|
||||
files.singleOrNull()?.let {
|
||||
intentBuilder.setChooserTitle(context.getString(R.string.share_s, it.name))
|
||||
} ?: run {
|
||||
intentBuilder.setChooserTitle(R.string.share)
|
||||
}
|
||||
intentBuilder.startChooser()
|
||||
}
|
||||
|
||||
fun shareImage(uri: Uri) {
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
intent.setDataAndType(uri, context.contentResolver.getType(uri) ?: "image/*")
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
val shareIntent = Intent.createChooser(intent, context.getString(R.string.share_image))
|
||||
context.startActivity(shareIntent)
|
||||
ShareCompat.IntentBuilder(context)
|
||||
.setStream(uri)
|
||||
.setType(context.contentResolver.getType(uri) ?: TYPE_IMAGE)
|
||||
.setChooserTitle(R.string.share_image)
|
||||
.startChooser()
|
||||
}
|
||||
|
||||
fun shareText(text: String) {
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
intent.type = "text/plain"
|
||||
intent.putExtra(Intent.EXTRA_TEXT, text)
|
||||
val shareIntent = Intent.createChooser(intent, context.getString(R.string.share))
|
||||
context.startActivity(shareIntent)
|
||||
ShareCompat.IntentBuilder(context)
|
||||
.setText(text)
|
||||
.setType(TYPE_TEXT)
|
||||
.setChooserTitle(R.string.share)
|
||||
.startChooser()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M9,3V4H4V6H5V19A2,2 0 0,0 7,21H17A2,2 0 0,0 19,19V6H20V4H15V3H9M7,6H17V19H7V6M9,8V17H11V8H9M13,8V17H15V8H13Z" />
|
||||
</vector>
|
||||
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_share"
|
||||
android:icon="?actionModeShareDrawable"
|
||||
android:title="@string/share"
|
||||
app:showAsAction="ifRoom|withText" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_remove"
|
||||
android:icon="@drawable/ic_delete"
|
||||
android:title="@string/remove"
|
||||
app:showAsAction="ifRoom|withText" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_select_all"
|
||||
android:icon="?actionModeSelectAllDrawable"
|
||||
android:title="@android:string/selectAll"
|
||||
app:showAsAction="ifRoom|withText" />
|
||||
|
||||
</menu>
|
||||
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_share"
|
||||
android:icon="?actionModeShareDrawable"
|
||||
android:title="@string/share"
|
||||
app:showAsAction="ifRoom|withText" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_favourite"
|
||||
android:icon="@drawable/ic_heart_outline"
|
||||
android:title="@string/add_to_favourites"
|
||||
app:showAsAction="ifRoom|withText" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_remove"
|
||||
android:icon="@drawable/ic_delete"
|
||||
android:title="@string/remove"
|
||||
app:showAsAction="ifRoom|withText" />
|
||||
|
||||
</menu>
|
||||
@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_share"
|
||||
android:icon="?actionModeShareDrawable"
|
||||
android:title="@string/share"
|
||||
app:showAsAction="ifRoom|withText" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_remove"
|
||||
android:icon="@drawable/ic_delete"
|
||||
android:title="@string/delete"
|
||||
app:showAsAction="ifRoom|withText" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_select_all"
|
||||
android:icon="?actionModeSelectAllDrawable"
|
||||
android:title="@android:string/selectAll"
|
||||
app:showAsAction="ifRoom|withText" />
|
||||
</menu>
|
||||
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_share"
|
||||
android:icon="?actionModeShareDrawable"
|
||||
android:title="@string/share"
|
||||
app:showAsAction="ifRoom|withText" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_favourite"
|
||||
android:icon="@drawable/ic_heart_outline"
|
||||
android:title="@string/add_to_favourites"
|
||||
app:showAsAction="ifRoom|withText" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_save"
|
||||
android:icon="@drawable/ic_save"
|
||||
android:title="@string/save"
|
||||
app:showAsAction="ifRoom|withText" />
|
||||
|
||||
</menu>
|
||||
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_create"
|
||||
android:icon="@drawable/ic_list_add"
|
||||
android:title="@string/add_new_category"
|
||||
android:titleCondensed="@string/add"
|
||||
app:showAsAction="ifRoom|withText" />
|
||||
|
||||
</menu>
|
||||
@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_remove"
|
||||
android:title="@string/remove" />
|
||||
|
||||
</menu>
|
||||
@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_remove"
|
||||
android:title="@string/remove" />
|
||||
|
||||
</menu>
|
||||
@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_delete"
|
||||
android:title="@string/delete" />
|
||||
|
||||
</menu>
|
||||
Loading…
Reference in New Issue