Action mode chapters selection

This commit is contained in:
Koitharu
2020-05-11 17:30:59 +03:00
parent 0100974508
commit 50f8cb9193
14 changed files with 204 additions and 51 deletions

View File

@@ -2,5 +2,5 @@ package org.koitharu.kotatsu.domain.history
enum class ChapterExtra {
READ, CURRENT, UNREAD, NEW
READ, CURRENT, UNREAD, NEW, CHECKED
}

View File

@@ -1,6 +1,8 @@
package org.koitharu.kotatsu.ui.details
import android.graphics.Color
import android.view.ViewGroup
import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.item_chapter.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaChapter
@@ -14,6 +16,7 @@ class ChapterHolder(parent: ViewGroup) :
override fun onBind(data: MangaChapter, extra: ChapterExtra) {
textView_title.text = data.name
textView_number.text = data.number.toString()
imageView_check.isVisible = extra == ChapterExtra.CHECKED
when (extra) {
ChapterExtra.UNREAD -> {
textView_number.setBackgroundResource(R.drawable.bg_badge_default)
@@ -31,6 +34,10 @@ class ChapterHolder(parent: ViewGroup) :
textView_number.setBackgroundResource(R.drawable.bg_badge_accent)
textView_number.setTextColor(context.getThemeColor(android.R.attr.textColorPrimaryInverse))
}
ChapterExtra.CHECKED -> {
textView_number.setBackgroundResource(R.drawable.bg_badge_accent)
textView_number.setTextColor(Color.TRANSPARENT)
}
}
}
}

View File

@@ -10,6 +10,14 @@ import org.koitharu.kotatsu.ui.common.list.OnRecyclerItemClickListener
class ChaptersAdapter(onItemClickListener: OnRecyclerItemClickListener<MangaChapter>) :
BaseRecyclerAdapter<MangaChapter, ChapterExtra>(onItemClickListener) {
private val checkedIds = HashSet<Long>()
val checkedItemsCount: Int
get() = checkedIds.size
val checkedItemsIds: Set<Long>
get() = checkedIds
var currentChapterId: Long? = null
set(value) {
field = value
@@ -26,11 +34,37 @@ class ChaptersAdapter(onItemClickListener: OnRecyclerItemClickListener<MangaChap
var currentChapterPosition = RecyclerView.NO_POSITION
private set
fun clearChecked() {
checkedIds.clear()
notifyDataSetChanged()
}
fun checkAll() {
for (item in dataSet) {
checkedIds.add(item.id)
}
notifyDataSetChanged()
}
fun setItemIsChecked(itemId: Long, isChecked: Boolean) {
if ((isChecked && checkedIds.add(itemId)) || (!isChecked && checkedIds.remove(itemId))) {
val pos = findItemPositionById(itemId)
if (pos != RecyclerView.NO_POSITION) {
notifyItemChanged(pos)
}
}
}
fun toggleItemChecked(itemId: Long) {
setItemIsChecked(itemId, itemId !in checkedIds)
}
override fun onCreateViewHolder(parent: ViewGroup) = ChapterHolder(parent)
override fun onGetItemId(item: MangaChapter) = item.id
override fun getExtra(item: MangaChapter, position: Int): ChapterExtra = when {
item.id in checkedIds -> ChapterExtra.CHECKED
currentChapterPosition == RecyclerView.NO_POSITION
|| currentChapterPosition < position -> if (position >= itemCount - newChaptersCount) {
ChapterExtra.NEW

View File

@@ -1,11 +1,12 @@
package org.koitharu.kotatsu.ui.details
import android.app.ActivityOptions
import android.content.Context
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.appcompat.widget.PopupMenu
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
@@ -21,7 +22,7 @@ import org.koitharu.kotatsu.ui.reader.ReaderActivity
import org.koitharu.kotatsu.utils.ext.resolveDp
class ChaptersFragment : BaseFragment(R.layout.fragment_chapters), MangaDetailsView,
OnRecyclerItemClickListener<MangaChapter> {
OnRecyclerItemClickListener<MangaChapter>, ActionMode.Callback {
@Suppress("unused")
private val presenter by moxyPresenter(factory = MangaDetailsPresenter.Companion::getInstance)
@@ -29,6 +30,7 @@ class ChaptersFragment : BaseFragment(R.layout.fragment_chapters), MangaDetailsV
private var manga: Manga? = null
private lateinit var adapter: ChaptersAdapter
private var actionMode: ActionMode? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -69,6 +71,15 @@ class ChaptersFragment : BaseFragment(R.layout.fragment_chapters), MangaDetailsV
override fun onFavouriteChanged(categories: List<FavouriteCategory>) = Unit
override fun onItemClick(item: MangaChapter, position: Int, view: View) {
if (adapter.checkedItemsCount != 0) {
adapter.toggleItemChecked(item.id)
if (adapter.checkedItemsCount == 0) {
actionMode?.finish()
} else {
actionMode?.invalidate()
}
return
}
val options = ActivityOptions.makeScaleUpAnimation(
view,
0,
@@ -86,16 +97,13 @@ class ChaptersFragment : BaseFragment(R.layout.fragment_chapters), MangaDetailsV
}
override fun onItemLongClick(item: MangaChapter, position: Int, view: View): Boolean {
if (item.source == MangaSource.LOCAL) {
return false
if (actionMode == null) {
actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this)
}
return context?.run {
val menu = PopupMenu(this, view)
menu.inflate(R.menu.popup_chapter)
menu.setOnMenuItemClickListener(PopupMenuListener(this, manga ?: return false, item))
menu.show()
true
} ?: false
return actionMode?.also {
adapter.setItemIsChecked(item.id, true)
it.invalidate()
} != null
}
private fun scrollToCurrent() {
@@ -107,29 +115,45 @@ class ChaptersFragment : BaseFragment(R.layout.fragment_chapters), MangaDetailsV
}
}
private class PopupMenuListener(
private val context: Context,
private val manga: Manga,
private val chapter: MangaChapter
) : PopupMenu.OnMenuItemClickListener {
override fun onMenuItemClick(item: MenuItem?): Boolean = when (item?.itemId) {
R.id.action_save_this -> {
DownloadService.start(context, manga, setOf(chapter.id))
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_save -> {
DownloadService.start(
context ?: return false,
manga ?: return false,
adapter.checkedItemsIds
)
true
}
R.id.action_save_this_next -> {
DownloadService.start(context, manga, manga.chapters.orEmpty()
.filter { x -> x.number >= chapter.number }.map { x -> x.id })
true
}
R.id.action_save_this_prev -> {
DownloadService.start(context, manga, manga.chapters.orEmpty()
.filter { x -> x.number <= chapter.number }.map { x -> x.id })
R.id.action_select_all -> {
adapter.checkAll()
mode.invalidate()
true
}
else -> false
}
}
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
mode.menuInflater.inflate(R.menu.mode_chapters, menu)
menu.findItem(R.id.action_save).isVisible = manga?.source != MangaSource.LOCAL
mode.title = manga?.title
return true
}
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
val count = adapter.checkedItemsCount
mode.subtitle = resources.getQuantityString(
R.plurals.chapters_from_x,
count,
count,
adapter.itemCount
)
return true
}
override fun onDestroyActionMode(mode: ActionMode?) {
adapter.clearChecked()
actionMode = null
}
}

View File

@@ -8,10 +8,13 @@ import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.view.ActionMode
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.net.toFile
import androidx.lifecycle.lifecycleScope
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.android.synthetic.main.activity_details.*
import kotlinx.coroutines.launch
import moxy.MvpDelegate
@@ -29,7 +32,8 @@ import org.koitharu.kotatsu.utils.MangaShortcut
import org.koitharu.kotatsu.utils.ShareHelper
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
class MangaDetailsActivity : BaseActivity(), MangaDetailsView {
class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
TabLayoutMediator.TabConfigurationStrategy {
private val presenter by moxyPresenter(factory = MangaDetailsPresenter.Companion::getInstance)
@@ -39,8 +43,8 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_details)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
pager.adapter = MangaDetailsAdapter(resources, supportFragmentManager)
tabs.setupWithViewPager(pager)
pager.adapter = MangaDetailsAdapter(this)
TabLayoutMediator(tabs, pager, this).attach()
if (savedInstanceState?.containsKey(MvpDelegate.MOXY_DELEGATE_TAGS_KEY) != true) {
intent?.getParcelableExtra<Manga>(EXTRA_MANGA)?.let {
presenter.loadDetails(it, true)
@@ -169,6 +173,24 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView {
else -> super.onOptionsItemSelected(item)
}
override fun onConfigureTab(tab: TabLayout.Tab, position: Int) {
tab.text = when(position) {
0 -> getString(R.string.details)
1 -> getString(R.string.chapters)
else -> null
}
}
override fun onSupportActionModeStarted(mode: ActionMode) {
super.onSupportActionModeStarted(mode)
pager.isUserInputEnabled = false
}
override fun onSupportActionModeFinished(mode: ActionMode) {
super.onSupportActionModeFinished(mode)
pager.isUserInputEnabled = true
}
companion object {
private const val EXTRA_MANGA = "manga"

View File

@@ -1,24 +1,16 @@
package org.koitharu.kotatsu.ui.details
import android.content.res.Resources
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
import org.koitharu.kotatsu.R
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
class MangaDetailsAdapter(private val resources: Resources, fm: FragmentManager) : FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
class MangaDetailsAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
override fun getCount() = 2
override fun getItemCount() = 2
override fun getItem(position: Int): Fragment = when(position) {
override fun createFragment(position: Int): Fragment = when(position) {
0 -> MangaDetailsFragment()
1 -> ChaptersFragment()
else -> throw IndexOutOfBoundsException("No fragment for position $position")
}
override fun getPageTitle(position: Int): CharSequence? = when(position) {
0 -> resources.getString(R.string.details)
1 -> resources.getString(R.string.chapters)
else -> null
}
}

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
</vector>

View File

@@ -0,0 +1,11 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM19,19L5,19L5,5h11.17L19,7.83L19,19zM12,12c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3zM6,6h9v4L6,10z" />
</vector>

View File

@@ -0,0 +1,11 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M3,5h2L5,3c-1.1,0 -2,0.9 -2,2zM3,13h2v-2L3,11v2zM7,21h2v-2L7,19v2zM3,9h2L5,7L3,7v2zM13,3h-2v2h2L13,3zM19,3v2h2c0,-1.1 -0.9,-2 -2,-2zM5,21v-2L3,19c0,1.1 0.9,2 2,2zM3,17h2v-2L3,15v2zM9,3L7,3v2h2L9,3zM11,21h2v-2h-2v2zM19,13h2v-2h-2v2zM19,21c1.1,0 2,-0.9 2,-2h-2v2zM19,9h2L21,7h-2v2zM19,17h2v-2h-2v2zM15,21h2v-2h-2v2zM15,5h2L17,3h-2v2zM7,17h10L17,7L7,7v10zM9,9h6v6L9,15L9,9z" />
</vector>

View File

@@ -27,7 +27,7 @@
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager.widget.ViewPager
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent"

View File

@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="@dimen/chapter_list_item_height"
android:background="?selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="12dp"
android:paddingEnd="12dp">
@@ -13,21 +13,41 @@
android:id="@+id/textView_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:background="@drawable/bg_badge_default"
android:gravity="center"
android:minWidth="26dp"
android:textAlignment="center"
android:textColor="?android:textColorSecondaryInverse"
tools:text="13" />
<ImageView
android:contentDescription="@null"
android:visibility="gone"
tools:visibility="visible"
android:id="@+id/imageView_check"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_alignStart="@id/textView_number"
android:layout_alignTop="@id/textView_number"
android:layout_alignEnd="@id/textView_number"
android:layout_alignBottom="@id/textView_number"
android:scaleType="fitCenter"
android:src="@drawable/ic_check"
android:padding="2dp"
app:tint="@android:color/white" />
<TextView
android:id="@+id/textView_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="10dp"
android:layout_toEndOf="@id/textView_number"
android:ellipsize="end"
android:maxLines="2"
android:text="?android:textColorPrimary"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
tools:text="@tools:sample/lorem[15]" />
</LinearLayout>
</RelativeLayout>

View File

@@ -0,0 +1,18 @@
<?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_select_all"
android:icon="@drawable/ic_select_all"
android:title="@android:string/selectAll"
app:showAsAction="ifRoom|withText" />
<item
android:id="@+id/action_save"
android:icon="@drawable/ic_save"
android:title="@string/save"
app:showAsAction="ifRoom|withText" />
</menu>

View File

@@ -15,4 +15,9 @@
<item quantity="few">%1$d новых главы</item>
<item quantity="many">%1$d новых глав</item>
</plurals>
<plurals name="chapters_from_x">
<item quantity="one">%1$d глава из %2$d</item>
<item quantity="few">%1$d главы из %2$d</item>
<item quantity="many">%1$d глав из %2$d</item>
</plurals>
</resources>

View File

@@ -12,4 +12,8 @@
<item quantity="one">%1$d new chapter</item>
<item quantity="other">%1$d new chapters</item>
</plurals>
<plurals name="chapters_from_x">
<item quantity="one">%1$d chapter from %2$d</item>
<item quantity="other">%1$d chapters from %2$d</item>
</plurals>
</resources>