Added improvements for RecentChapters. Closes #320 (#324)

This commit is contained in:
Bram van de Kerkhof 2016-06-04 17:50:44 +02:00 committed by inorichi
parent 6196480d1d
commit 1fbec7bf3d
6 changed files with 281 additions and 61 deletions

View File

@ -2,9 +2,10 @@ package eu.kanade.tachiyomi.ui.recent
import android.os.Bundle import android.os.Bundle
import android.support.v4.app.DialogFragment import android.support.v4.app.DialogFragment
import android.view.LayoutInflater import android.support.v7.view.ActionMode
import android.view.View import android.view.*
import android.view.ViewGroup import com.afollestad.materialdialogs.MaterialDialog
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.MangaChapter import eu.kanade.tachiyomi.data.database.models.MangaChapter
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
@ -13,6 +14,7 @@ import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.getResourceDrawable import eu.kanade.tachiyomi.util.getResourceDrawable
import eu.kanade.tachiyomi.util.toast
import eu.kanade.tachiyomi.widget.DeletingChaptersDialog import eu.kanade.tachiyomi.widget.DeletingChaptersDialog
import eu.kanade.tachiyomi.widget.DividerItemDecoration import eu.kanade.tachiyomi.widget.DividerItemDecoration
import eu.kanade.tachiyomi.widget.NpaLinearLayoutManager import eu.kanade.tachiyomi.widget.NpaLinearLayoutManager
@ -22,15 +24,15 @@ import timber.log.Timber
/** /**
* Fragment that shows recent chapters. * Fragment that shows recent chapters.
* Uses R.layout.fragment_recent_chapters. * Uses [R.layout.fragment_recent_chapters].
* UI related actions should be called from here. * UI related actions should be called from here.
*/ */
@RequiresPresenter(RecentChaptersPresenter::class) @RequiresPresenter(RecentChaptersPresenter::class)
class RecentChaptersFragment : BaseRxFragment<RecentChaptersPresenter>(), FlexibleViewHolder.OnListItemClickListener { class RecentChaptersFragment : BaseRxFragment<RecentChaptersPresenter>(), ActionMode.Callback, FlexibleViewHolder.OnListItemClickListener {
companion object { companion object {
/** /**
* Create new RecentChaptersFragment. * Create new RecentChaptersFragment.
* * @return a new instance of [RecentChaptersFragment].
*/ */
@JvmStatic @JvmStatic
fun newInstance(): RecentChaptersFragment { fun newInstance(): RecentChaptersFragment {
@ -38,6 +40,59 @@ class RecentChaptersFragment : BaseRxFragment<RecentChaptersPresenter>(), Flexib
} }
} }
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
return false
}
/**
* Called when ActionMode item clicked
* @param mode the ActionMode object
* @param item item from ActionMode.
*/
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_mark_as_read -> markAsRead(getSelectedChapters())
R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters())
R.id.action_download -> downloadChapters(getSelectedChapters())
R.id.action_delete -> {
MaterialDialog.Builder(activity)
.content(R.string.confirm_delete_chapters)
.positiveText(android.R.string.yes)
.negativeText(android.R.string.no)
.onPositive { dialog, action -> deleteChapters(getSelectedChapters()) }
.show()
}
else -> return false
}
return true
}
/**
* Called when ActionMode created.
* @param mode the ActionMode object
* @param menu menu object of ActionMode
*/
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
mode.menuInflater.inflate(R.menu.chapter_recent_selection, menu)
adapter.mode = FlexibleAdapter.MODE_MULTI
return true
}
/**
* Called when ActionMode destroyed
* @param mode the ActionMode object
*/
override fun onDestroyActionMode(mode: ActionMode?) {
adapter.mode = FlexibleAdapter.MODE_SINGLE
adapter.clearSelection()
actionMode = null
}
/**
* Action mode for multiple selection.
*/
private var actionMode: ActionMode? = null
/** /**
* Adapter containing the recent chapters. * Adapter containing the recent chapters.
*/ */
@ -46,7 +101,6 @@ class RecentChaptersFragment : BaseRxFragment<RecentChaptersPresenter>(), Flexib
/** /**
* Called when view gets created * Called when view gets created
*
* @param inflater layout inflater * @param inflater layout inflater
* @param container view group * @param container view group
* @param savedState status of saved state * @param savedState status of saved state
@ -58,7 +112,6 @@ class RecentChaptersFragment : BaseRxFragment<RecentChaptersPresenter>(), Flexib
/** /**
* Called when view is created * Called when view is created
*
* @param view created view * @param view created view
* @param savedInstanceState status of saved sate * @param savedInstanceState status of saved sate
*/ */
@ -70,60 +123,108 @@ class RecentChaptersFragment : BaseRxFragment<RecentChaptersPresenter>(), Flexib
adapter = RecentChaptersAdapter(this) adapter = RecentChaptersAdapter(this)
recycler.adapter = adapter recycler.adapter = adapter
// Set swipe refresh listener
swipe_refresh.setOnRefreshListener { fetchChapters() }
// Update toolbar text // Update toolbar text
setToolbarTitle(R.string.label_recent_updates) setToolbarTitle(R.string.label_recent_updates)
} }
/**
* Returns selected chapters
* @return list of [MangaChapter]s
*/
fun getSelectedChapters(): List<MangaChapter> {
return adapter.selectedItems.map { adapter.getItem(it) as? MangaChapter }.filterNotNull()
}
/** /**
* Called when item in list is clicked * Called when item in list is clicked
*
* @param position position of clicked item * @param position position of clicked item
*/ */
override fun onListItemClick(position: Int): Boolean { override fun onListItemClick(position: Int): Boolean {
// Get item from position // Get item from position
val item = adapter.getItem(position) val item = adapter.getItem(position)
if (item is MangaChapter) { if (item is MangaChapter) {
// Open chapter in reader if (actionMode != null && adapter.mode == FlexibleAdapter.MODE_MULTI) {
toggleSelection(position)
return true
} else {
openChapter(item) openChapter(item)
return false
}
} }
return false return false
} }
/** /**
* Called when item in list is long clicked * Called when item in list is long clicked
*
* @param position position of clicked item * @param position position of clicked item
*/ */
override fun onListItemLongClick(position: Int) { override fun onListItemLongClick(position: Int) {
// Empty function if (actionMode == null)
actionMode = activity.startSupportActionMode(this)
toggleSelection(position)
}
/**
* Called to toggle selection
* @param position position of selected item
*/
private fun toggleSelection(position: Int) {
adapter.toggleSelection(position, false)
val count = adapter.selectedItemCount
if (count == 0) {
actionMode?.finish()
} else {
setContextTitle(count)
actionMode?.invalidate()
}
}
/**
* Set the context title
* @param count count of selected items
*/
private fun setContextTitle(count: Int) {
actionMode?.title = getString(R.string.label_selected, count)
} }
/** /**
* Open chapter in reader * Open chapter in reader
* * @param mangaChapter selected [MangaChapter]
* @param chapter selected chapter
*/ */
private fun openChapter(chapter: MangaChapter) { private fun openChapter(mangaChapter: MangaChapter) {
val intent = ReaderActivity.newIntent(activity, chapter.manga, chapter.chapter) val intent = ReaderActivity.newIntent(activity, mangaChapter.manga, mangaChapter.chapter)
startActivity(intent) startActivity(intent)
} }
/**
* Download selected items
* @param mangaChapters list of selected [MangaChapter]s
*/
fun downloadChapters(mangaChapters: List<MangaChapter>) {
destroyActionModeIfNeeded()
presenter.downloadChapters(mangaChapters)
}
/** /**
* Populate adapter with chapters * Populate adapter with chapters
* * @param chapters list of [Any]
* @param chapters list of chapters
*/ */
fun onNextMangaChapters(chapters: List<Any>) { fun onNextMangaChapters(chapters: List<Any>) {
(activity as MainActivity).updateEmptyView(chapters.isEmpty(), (activity as MainActivity).updateEmptyView(chapters.isEmpty(),
R.string.information_no_recent, R.drawable.ic_history_black_128dp) R.string.information_no_recent, R.drawable.ic_history_black_128dp)
destroyActionModeIfNeeded()
adapter.setItems(chapters) adapter.setItems(chapters)
} }
/** /**
* Update download status of chapter * Update download status of chapter
* @param download [Download] object containing download progress.
* @param download download object containing download progress.
*/ */
fun onChapterStatusChange(download: Download) { fun onChapterStatusChange(download: Download) {
getHolder(download)?.onStatusChange(download.status) getHolder(download)?.onStatusChange(download.status)
@ -132,8 +233,7 @@ class RecentChaptersFragment : BaseRxFragment<RecentChaptersPresenter>(), Flexib
/** /**
* Returns holder belonging to chapter * Returns holder belonging to chapter
* * @param download [Download] object containing download progress.
* @param download download object containing download progress.
*/ */
private fun getHolder(download: Download): RecentChaptersHolder? { private fun getHolder(download: Download): RecentChaptersHolder? {
return recycler.findViewHolderForItemId(download.chapter.id) as? RecentChaptersHolder return recycler.findViewHolderForItemId(download.chapter.id) as? RecentChaptersHolder
@ -141,28 +241,42 @@ class RecentChaptersFragment : BaseRxFragment<RecentChaptersPresenter>(), Flexib
/** /**
* Mark chapter as read * Mark chapter as read
* * @param mangaChapters list of [MangaChapter] objects
* @param item selected chapter with manga
*/ */
fun markAsRead(item: MangaChapter) { fun markAsRead(mangaChapters: List<MangaChapter>) {
presenter.markChapterRead(item.chapter, true) presenter.markChapterRead(mangaChapters, true)
if (presenter.preferences.removeAfterMarkedAsRead()) { if (presenter.preferences.removeAfterMarkedAsRead()) {
deleteChapter(item) deleteChapters(mangaChapters)
} }
} }
/**
* Delete selected chapters
* @param mangaChapters list of [MangaChapter] objects
*/
fun deleteChapters(mangaChapters: List<MangaChapter>) {
destroyActionModeIfNeeded()
DeletingChaptersDialog().show(childFragmentManager, DeletingChaptersDialog.TAG)
presenter.deleteChapters(mangaChapters)
}
/**
* Destory [ActionMode] if it's shown
*/
fun destroyActionModeIfNeeded() {
actionMode?.finish()
}
/** /**
* Mark chapter as unread * Mark chapter as unread
* * @param mangaChapters list of selected [MangaChapter]
* @param item selected chapter with manga
*/ */
fun markAsUnread(item: MangaChapter) { fun markAsUnread(mangaChapters: List<MangaChapter>) {
presenter.markChapterRead(item.chapter, false) presenter.markChapterRead(mangaChapters, false)
} }
/** /**
* Start downloading chapter * Start downloading chapter
*
* @param item selected chapter with manga * @param item selected chapter with manga
*/ */
fun downloadChapter(item: MangaChapter) { fun downloadChapter(item: MangaChapter) {
@ -171,7 +285,6 @@ class RecentChaptersFragment : BaseRxFragment<RecentChaptersPresenter>(), Flexib
/** /**
* Start deleting chapter * Start deleting chapter
*
* @param item selected chapter with manga * @param item selected chapter with manga
*/ */
fun deleteChapter(item: MangaChapter) { fun deleteChapter(item: MangaChapter) {
@ -179,18 +292,52 @@ class RecentChaptersFragment : BaseRxFragment<RecentChaptersPresenter>(), Flexib
presenter.deleteChapter(item) presenter.deleteChapter(item)
} }
/**
* Called when chapters are deleted
*/
fun onChaptersDeleted() { fun onChaptersDeleted() {
dismissDeletingDialog() dismissDeletingDialog()
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
} }
/**
* Called when error while deleting
* @param error error message
*/
fun onChaptersDeletedError(error: Throwable) { fun onChaptersDeletedError(error: Throwable) {
dismissDeletingDialog() dismissDeletingDialog()
Timber.e(error, error.message) Timber.e(error, error.message)
} }
/**
* Called to dismiss deleting dialog
*/
fun dismissDeletingDialog() { fun dismissDeletingDialog() {
(childFragmentManager.findFragmentByTag(DeletingChaptersDialog.TAG) as? DialogFragment)?.dismiss() (childFragmentManager.findFragmentByTag(DeletingChaptersDialog.TAG) as? DialogFragment)?.dismiss()
} }
/**
* Called when swipe refresh activated.
*/
fun fetchChapters() {
swipe_refresh.isRefreshing = true
presenter.fetchChaptersFromSource()
}
/**
* Called after refresh is completed
*/
fun onFetchChaptersDone() {
swipe_refresh.isRefreshing = false
}
/**
* Called when something went wrong while refreshing
* @param error information on what went wrong
*/
fun onFetchChaptersError(error: Throwable) {
swipe_refresh.isRefreshing = false
context.toast(error.message)
}
} }

View File

@ -122,8 +122,8 @@ class RecentChaptersHolder(view: View, private val adapter: RecentChaptersAdapte
when (menuItem.itemId) { when (menuItem.itemId) {
R.id.action_download -> adapter.fragment.downloadChapter(it) R.id.action_download -> adapter.fragment.downloadChapter(it)
R.id.action_delete -> adapter.fragment.deleteChapter(it) R.id.action_delete -> adapter.fragment.deleteChapter(it)
R.id.action_mark_as_read -> adapter.fragment.markAsRead(it) R.id.action_mark_as_read -> adapter.fragment.markAsRead(listOf(it))
R.id.action_mark_as_unread -> adapter.fragment.markAsUnread(it) R.id.action_mark_as_unread -> adapter.fragment.markAsUnread(listOf(it))
} }
true true
} }

View File

@ -60,12 +60,16 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
// Used to get recent chapters // Used to get recent chapters
restartableLatestCache(GET_RECENT_CHAPTERS, restartableLatestCache(GET_RECENT_CHAPTERS,
{ getRecentChaptersObservable() }, { getRecentChaptersObservable() },
{ recentChaptersFragment, chapters -> { fragment, chapters ->
// Update adapter to show recent manga's // Update adapter to show recent manga's
recentChaptersFragment.onNextMangaChapters(chapters) fragment.onNextMangaChapters(chapters)
// Update download status // Update download status
updateChapterStatus(convertToMangaChaptersList(chapters)) updateChapterStatus(convertToMangaChaptersList(chapters))
} // Stop refresh
fragment.onFetchChaptersDone()
},
{ fragment, error -> fragment.onFetchChaptersError(error) }
) )
// Used to update download status // Used to update download status
@ -88,7 +92,6 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
/** /**
* Returns observable containing chapter status. * Returns observable containing chapter status.
* @return download object containing download progress. * @return download object containing download progress.
*/ */
private fun getChapterStatusObs(): Observable<Download> { private fun getChapterStatusObs(): Observable<Download> {
@ -107,7 +110,6 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
/** /**
* Function to check if chapter is in recent list * Function to check if chapter is in recent list
* @param chaptersId id of chapter * @param chaptersId id of chapter
* *
* @return exist in recent list * @return exist in recent list
*/ */
private fun chapterIdEquals(chaptersId: Long): Boolean { private fun chapterIdEquals(chaptersId: Long): Boolean {
@ -121,9 +123,7 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
/** /**
* Returns a list only containing MangaChapter objects. * Returns a list only containing MangaChapter objects.
* @param input the list that will be converted. * @param input the list that will be converted.
* *
* @return list containing MangaChapters objects. * @return list containing MangaChapters objects.
*/ */
private fun convertToMangaChaptersList(input: List<Any>): List<MangaChapter> { private fun convertToMangaChaptersList(input: List<Any>): List<MangaChapter> {
@ -144,7 +144,6 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
/** /**
* Update status of chapters. * Update status of chapters.
* @param download download object containing progress. * @param download download object containing progress.
*/ */
private fun updateChapterStatus(download: Download) { private fun updateChapterStatus(download: Download) {
@ -162,7 +161,6 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
/** /**
* Update status of chapters * Update status of chapters
* @param mangaChapters list containing recent chapters * @param mangaChapters list containing recent chapters
*/ */
private fun updateChapterStatus(mangaChapters: List<MangaChapter>) { private fun updateChapterStatus(mangaChapters: List<MangaChapter>) {
@ -236,7 +234,6 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
/** /**
* Get date as time key * Get date as time key
* @param date desired date * @param date desired date
* *
* @return date as time key * @return date as time key
*/ */
private fun getMapKey(date: Long): Date { private fun getMapKey(date: Long): Date {
@ -251,26 +248,62 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
/** /**
* Mark selected chapter as read * Mark selected chapter as read
* * @param mangaChapters list of selected MangaChapters
* @param chapter selected chapter
* @param read read status * @param read read status
*/ */
fun markChapterRead(chapter: Chapter, read: Boolean) { fun markChapterRead(mangaChapters: List<MangaChapter>, read: Boolean) {
Observable.just(chapter) Observable.from(mangaChapters)
.doOnNext { chapter -> .doOnNext { mangaChapter ->
chapter.read = read mangaChapter.chapter.read = read
if (!read) { if (!read) {
chapter.last_page_read = 0 mangaChapter.chapter.last_page_read = 0
} }
} }
.flatMap { db.updateChapterProgress(it).asRxObservable() } .map { mangaChapter -> mangaChapter.chapter }
.toList()
.flatMap { db.updateChaptersProgress(it).asRxObservable() }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.subscribe() .subscribe()
} }
/**
* Delete selected chapters
* @param chapters list of MangaChapters
*/
fun deleteChapters(chapters: List<MangaChapter>) {
val wasRunning = downloadManager.isRunning
if (wasRunning) {
DownloadService.stop(context)
}
Observable.from(chapters)
.doOnNext { deleteChapter(it) }
.toList()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeFirst({ view, result ->
view.onChaptersDeleted()
if (wasRunning) {
DownloadService.start(context)
}
}, { view, error ->
view.onChaptersDeletedError(error)
})
}
/**
* Download selected chapters
* @param mangaChapters [MangaChapter] that is selected
*/
fun downloadChapters(mangaChapters: List<MangaChapter>) {
DownloadService.start(context)
Observable.from(mangaChapters)
.doOnNext { downloadChapter(it) }
.subscribeOn(AndroidSchedulers.mainThread())
.subscribe()
}
/** /**
* Download selected chapter * Download selected chapter
*
* @param item chapter that is selected * @param item chapter that is selected
*/ */
fun downloadChapter(item: MangaChapter) { fun downloadChapter(item: MangaChapter) {
@ -280,7 +313,6 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
/** /**
* Delete selected chapter * Delete selected chapter
*
* @param item chapter that are selected * @param item chapter that are selected
*/ */
fun deleteChapter(item: MangaChapter) { fun deleteChapter(item: MangaChapter) {
@ -304,7 +336,6 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
/** /**
* Delete selected chapter * Delete selected chapter
*
* @param chapter chapter that is selected * @param chapter chapter that is selected
* @param manga manga that belongs to chapter * @param manga manga that belongs to chapter
*/ */
@ -315,4 +346,8 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
chapter.status = Download.NOT_DOWNLOADED chapter.status = Download.NOT_DOWNLOADED
} }
fun fetchChaptersFromSource() {
start(GET_RECENT_CHAPTERS)
}
} }

View File

@ -5,6 +5,12 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.RecyclerView <android.support.v7.widget.RecyclerView
android:id="@+id/recycler" android:id="@+id/recycler"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -13,5 +19,6 @@
tools:listitem="@layout/item_recent_chapter"> tools:listitem="@layout/item_recent_chapter">
</android.support.v7.widget.RecyclerView> </android.support.v7.widget.RecyclerView>
</android.support.v4.widget.SwipeRefreshLayout>
</RelativeLayout> </RelativeLayout>

View File

@ -1,9 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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="fill_parent" android:layout_width="fill_parent"
android:layout_height="?android:attr/listPreferredItemHeight" android:layout_height="?android:attr/listPreferredItemHeight"
xmlns:app="http://schemas.android.com/apk/res-auto"> android:background="?attr/selectable_list_drawable">
<RelativeLayout <RelativeLayout
@ -39,8 +40,8 @@
android:layout_alignParentLeft="true" android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:layout_marginRight="30dp"
android:layout_marginEnd="30dp" android:layout_marginEnd="30dp"
android:layout_marginRight="30dp"
android:orientation="vertical"> android:orientation="vertical">
<TextView <TextView

View File

@ -0,0 +1,30 @@
<?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_download"
android:icon="@drawable/ic_file_download_white_24dp"
android:title="@string/action_download"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/action_delete"
android:icon="@drawable/ic_delete_white_24dp"
android:title="@string/action_delete"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/action_mark_as_read"
android:icon="@drawable/ic_done_all_white_24dp"
android:title="@string/action_mark_as_read"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/action_mark_as_unread"
android:icon="@drawable/ic_done_all_grey_24dp"
android:title="@string/action_mark_as_unread"
app:showAsAction="ifRoom"/>
</menu>