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 { enum class ChapterExtra {
READ, CURRENT, UNREAD, NEW READ, CURRENT, UNREAD, NEW, CHECKED
} }

View File

@@ -1,6 +1,8 @@
package org.koitharu.kotatsu.ui.details package org.koitharu.kotatsu.ui.details
import android.graphics.Color
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.item_chapter.* import kotlinx.android.synthetic.main.item_chapter.*
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaChapter import org.koitharu.kotatsu.core.model.MangaChapter
@@ -14,6 +16,7 @@ class ChapterHolder(parent: ViewGroup) :
override fun onBind(data: MangaChapter, extra: ChapterExtra) { override fun onBind(data: MangaChapter, extra: ChapterExtra) {
textView_title.text = data.name textView_title.text = data.name
textView_number.text = data.number.toString() textView_number.text = data.number.toString()
imageView_check.isVisible = extra == ChapterExtra.CHECKED
when (extra) { when (extra) {
ChapterExtra.UNREAD -> { ChapterExtra.UNREAD -> {
textView_number.setBackgroundResource(R.drawable.bg_badge_default) 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.setBackgroundResource(R.drawable.bg_badge_accent)
textView_number.setTextColor(context.getThemeColor(android.R.attr.textColorPrimaryInverse)) 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>) : class ChaptersAdapter(onItemClickListener: OnRecyclerItemClickListener<MangaChapter>) :
BaseRecyclerAdapter<MangaChapter, ChapterExtra>(onItemClickListener) { 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 var currentChapterId: Long? = null
set(value) { set(value) {
field = value field = value
@@ -26,11 +34,37 @@ class ChaptersAdapter(onItemClickListener: OnRecyclerItemClickListener<MangaChap
var currentChapterPosition = RecyclerView.NO_POSITION var currentChapterPosition = RecyclerView.NO_POSITION
private set 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 onCreateViewHolder(parent: ViewGroup) = ChapterHolder(parent)
override fun onGetItemId(item: MangaChapter) = item.id override fun onGetItemId(item: MangaChapter) = item.id
override fun getExtra(item: MangaChapter, position: Int): ChapterExtra = when { override fun getExtra(item: MangaChapter, position: Int): ChapterExtra = when {
item.id in checkedIds -> ChapterExtra.CHECKED
currentChapterPosition == RecyclerView.NO_POSITION currentChapterPosition == RecyclerView.NO_POSITION
|| currentChapterPosition < position -> if (position >= itemCount - newChaptersCount) { || currentChapterPosition < position -> if (position >= itemCount - newChaptersCount) {
ChapterExtra.NEW ChapterExtra.NEW

View File

@@ -1,11 +1,12 @@
package org.koitharu.kotatsu.ui.details package org.koitharu.kotatsu.ui.details
import android.app.ActivityOptions import android.app.ActivityOptions
import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View 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.core.view.isVisible
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@@ -21,7 +22,7 @@ import org.koitharu.kotatsu.ui.reader.ReaderActivity
import org.koitharu.kotatsu.utils.ext.resolveDp import org.koitharu.kotatsu.utils.ext.resolveDp
class ChaptersFragment : BaseFragment(R.layout.fragment_chapters), MangaDetailsView, class ChaptersFragment : BaseFragment(R.layout.fragment_chapters), MangaDetailsView,
OnRecyclerItemClickListener<MangaChapter> { OnRecyclerItemClickListener<MangaChapter>, ActionMode.Callback {
@Suppress("unused") @Suppress("unused")
private val presenter by moxyPresenter(factory = MangaDetailsPresenter.Companion::getInstance) 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 var manga: Manga? = null
private lateinit var adapter: ChaptersAdapter private lateinit var adapter: ChaptersAdapter
private var actionMode: ActionMode? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) 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 onFavouriteChanged(categories: List<FavouriteCategory>) = Unit
override fun onItemClick(item: MangaChapter, position: Int, view: View) { 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( val options = ActivityOptions.makeScaleUpAnimation(
view, view,
0, 0,
@@ -86,16 +97,13 @@ class ChaptersFragment : BaseFragment(R.layout.fragment_chapters), MangaDetailsV
} }
override fun onItemLongClick(item: MangaChapter, position: Int, view: View): Boolean { override fun onItemLongClick(item: MangaChapter, position: Int, view: View): Boolean {
if (item.source == MangaSource.LOCAL) { if (actionMode == null) {
return false actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this)
} }
return context?.run { return actionMode?.also {
val menu = PopupMenu(this, view) adapter.setItemIsChecked(item.id, true)
menu.inflate(R.menu.popup_chapter) it.invalidate()
menu.setOnMenuItemClickListener(PopupMenuListener(this, manga ?: return false, item)) } != null
menu.show()
true
} ?: false
} }
private fun scrollToCurrent() { private fun scrollToCurrent() {
@@ -107,29 +115,45 @@ class ChaptersFragment : BaseFragment(R.layout.fragment_chapters), MangaDetailsV
} }
} }
private class PopupMenuListener( override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
private val context: Context, return when (item.itemId) {
private val manga: Manga, R.id.action_save -> {
private val chapter: MangaChapter DownloadService.start(
) : PopupMenu.OnMenuItemClickListener { context ?: return false,
manga ?: return false,
override fun onMenuItemClick(item: MenuItem?): Boolean = when (item?.itemId) { adapter.checkedItemsIds
R.id.action_save_this -> { )
DownloadService.start(context, manga, setOf(chapter.id))
true true
} }
R.id.action_save_this_next -> { R.id.action_select_all -> {
DownloadService.start(context, manga, manga.chapters.orEmpty() adapter.checkAll()
.filter { x -> x.number >= chapter.number }.map { x -> x.id }) mode.invalidate()
true
}
R.id.action_save_this_prev -> {
DownloadService.start(context, manga, manga.chapters.orEmpty()
.filter { x -> x.number <= chapter.number }.map { x -> x.id })
true true
} }
else -> false 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.view.MenuItem
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.view.ActionMode
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.net.toFile import androidx.core.net.toFile
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.snackbar.Snackbar 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.android.synthetic.main.activity_details.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import moxy.MvpDelegate import moxy.MvpDelegate
@@ -29,7 +32,8 @@ import org.koitharu.kotatsu.utils.MangaShortcut
import org.koitharu.kotatsu.utils.ShareHelper import org.koitharu.kotatsu.utils.ShareHelper
import org.koitharu.kotatsu.utils.ext.getDisplayMessage 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) private val presenter by moxyPresenter(factory = MangaDetailsPresenter.Companion::getInstance)
@@ -39,8 +43,8 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_details) setContentView(R.layout.activity_details)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
pager.adapter = MangaDetailsAdapter(resources, supportFragmentManager) pager.adapter = MangaDetailsAdapter(this)
tabs.setupWithViewPager(pager) TabLayoutMediator(tabs, pager, this).attach()
if (savedInstanceState?.containsKey(MvpDelegate.MOXY_DELEGATE_TAGS_KEY) != true) { if (savedInstanceState?.containsKey(MvpDelegate.MOXY_DELEGATE_TAGS_KEY) != true) {
intent?.getParcelableExtra<Manga>(EXTRA_MANGA)?.let { intent?.getParcelableExtra<Manga>(EXTRA_MANGA)?.let {
presenter.loadDetails(it, true) presenter.loadDetails(it, true)
@@ -169,6 +173,24 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView {
else -> super.onOptionsItemSelected(item) 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 { companion object {
private const val EXTRA_MANGA = "manga" private const val EXTRA_MANGA = "manga"

View File

@@ -1,24 +1,16 @@
package org.koitharu.kotatsu.ui.details package org.koitharu.kotatsu.ui.details
import android.content.res.Resources
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentPagerAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import org.koitharu.kotatsu.R
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() 0 -> MangaDetailsFragment()
1 -> ChaptersFragment() 1 -> ChaptersFragment()
else -> throw IndexOutOfBoundsException("No fragment for position $position") 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> </com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager.widget.ViewPager <androidx.viewpager2.widget.ViewPager2
android:id="@+id/pager" android:id="@+id/pager"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"

View File

@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?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" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/chapter_list_item_height" android:layout_height="@dimen/chapter_list_item_height"
android:background="?selectableItemBackground" android:background="?selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="12dp" android:paddingStart="12dp"
android:paddingEnd="12dp"> android:paddingEnd="12dp">
@@ -13,21 +13,41 @@
android:id="@+id/textView_number" android:id="@+id/textView_number"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:background="@drawable/bg_badge_default" android:background="@drawable/bg_badge_default"
android:gravity="center" android:gravity="center"
android:minWidth="26dp" android:minWidth="26dp"
android:textAlignment="center"
android:textColor="?android:textColorSecondaryInverse" android:textColor="?android:textColorSecondaryInverse"
tools:text="13" /> 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 <TextView
android:id="@+id/textView_title" android:id="@+id/textView_title"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="10dp" android:layout_marginStart="10dp"
android:layout_toEndOf="@id/textView_number"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="2" android:maxLines="2"
android:text="?android:textColorPrimary" android:text="?android:textColorPrimary"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" android:textAppearance="@style/TextAppearance.AppCompat.Body1"
tools:text="@tools:sample/lorem[15]" /> 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="few">%1$d новых главы</item>
<item quantity="many">%1$d новых глав</item> <item quantity="many">%1$d новых глав</item>
</plurals> </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> </resources>

View File

@@ -12,4 +12,8 @@
<item quantity="one">%1$d new chapter</item> <item quantity="one">%1$d new chapter</item>
<item quantity="other">%1$d new chapters</item> <item quantity="other">%1$d new chapters</item>
</plurals> </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> </resources>