Update Recents updates items to new style

Also removed newly added manga from flooding recent updates (closing #52)
This commit is contained in:
Jay 2020-04-04 20:18:27 -04:00
parent 4e33b93c08
commit 83206ded5e
10 changed files with 318 additions and 482 deletions

View File

@ -57,7 +57,9 @@ fun getLibraryMangaQuery(id: Long) = """
fun getRecentsQuery() = """ fun getRecentsQuery() = """
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, * FROM ${Manga.TABLE} JOIN ${Chapter.TABLE} SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, * FROM ${Manga.TABLE} JOIN ${Chapter.TABLE}
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
WHERE ${Manga.COL_FAVORITE} = 1 AND ${Chapter.COL_DATE_UPLOAD} > ? WHERE ${Manga.COL_FAVORITE} = 1
AND ${Chapter.COL_DATE_UPLOAD} > ?
AND ${Chapter.COL_DATE_FETCH} > ${Manga.COL_DATE_ADDED}
ORDER BY ${Chapter.COL_DATE_UPLOAD} DESC ORDER BY ${Chapter.COL_DATE_UPLOAD} DESC
""" """

View File

@ -114,8 +114,7 @@ class ChapterHolder(
} }
private fun resetFrontView() { private fun resetFrontView() {
if (front_view.translationX == 0f) return if (front_view.translationX != 0f) itemView.post { adapter.notifyItemChanged(adapterPosition) }
itemView.post { adapter.notifyItemChanged(adapterPosition) }
} }
fun notifyStatus(status: Int, locked: Boolean, progress: Int) = with(download_button) { fun notifyStatus(status: Int, locked: Boolean, progress: Int) = with(download_button) {

View File

@ -1,14 +1,13 @@
package eu.kanade.tachiyomi.ui.recent_updates package eu.kanade.tachiyomi.ui.recent_updates
import android.view.View import android.view.View
import androidx.appcompat.widget.PopupMenu import androidx.core.content.ContextCompat
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import eu.kanade.tachiyomi.ui.manga.chapter.BaseChapterHolder
import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.view.setVectorCompat import kotlinx.android.synthetic.main.download_button.*
import kotlinx.android.synthetic.main.recent_chapters_item.* import kotlinx.android.synthetic.main.recent_chapters_item.*
/** /**
@ -22,7 +21,7 @@ import kotlinx.android.synthetic.main.recent_chapters_item.*
* @constructor creates a new recent chapter holder. * @constructor creates a new recent chapter holder.
*/ */
class RecentChapterHolder(private val view: View, private val adapter: RecentChaptersAdapter) : class RecentChapterHolder(private val view: View, private val adapter: RecentChaptersAdapter) :
BaseFlexibleViewHolder(view, adapter) { BaseChapterHolder(view, adapter) {
/** /**
* Color of read chapter * Color of read chapter
@ -40,10 +39,6 @@ class RecentChapterHolder(private val view: View, private val adapter: RecentCha
private var item: RecentChapterItem? = null private var item: RecentChapterItem? = null
init { init {
// We need to post a Runnable to show the popup to make sure that the PopupMenu is
// correctly positioned. The reason being that the view may change position before the
// PopupMenu is shown.
chapter_menu.setOnClickListener { it.post { showPopupMenu(it) } }
manga_cover.setOnClickListener { manga_cover.setOnClickListener {
adapter.coverClickListener.onCoverClick(adapterPosition) adapter.coverClickListener.onCoverClick(adapterPosition)
} }
@ -63,8 +58,14 @@ class RecentChapterHolder(private val view: View, private val adapter: RecentCha
// Set manga title // Set manga title
manga_full_title.text = item.manga.title manga_full_title.text = item.manga.title
// Set the correct drawable for dropdown and update the tint to match theme. if (front_view.translationX == 0f) {
chapter_menu_icon.setVectorCompat(R.drawable.ic_more_horiz_black_24dp, view.context.getResourceColor(R.attr.icon_color)) read.setImageDrawable(
ContextCompat.getDrawable(
read.context, if (item.read) R.drawable.eye_off
else R.drawable.eye
)
)
}
// Set cover // Set cover
GlideApp.with(itemView.context).clear(manga_cover) GlideApp.with(itemView.context).clear(manga_cover)
@ -86,7 +87,20 @@ class RecentChapterHolder(private val view: View, private val adapter: RecentCha
} }
// Set chapter status // Set chapter status
notifyStatus(item.status) notifyStatus(item.status, item.progress)
resetFrontView()
}
private fun resetFrontView() {
if (front_view.translationX != 0f) itemView.post { adapter.notifyItemChanged(adapterPosition) }
}
override fun getFrontView(): View {
return front_view
}
override fun getRearRightView(): View {
return right_view
} }
/** /**
@ -94,59 +108,6 @@ class RecentChapterHolder(private val view: View, private val adapter: RecentCha
* *
* @param status download status * @param status download status
*/ */
fun notifyStatus(status: Int) = with(download_text) { fun notifyStatus(status: Int, progress: Int) =
when (status) { download_button.setDownloadStatus(status, progress)
Download.QUEUE -> setText(R.string.chapter_queued)
Download.DOWNLOADING -> setText(R.string.chapter_downloading)
Download.DOWNLOADED -> setText(R.string.chapter_downloaded)
Download.ERROR -> setText(R.string.chapter_error)
else -> text = ""
}
}
/**
* Show pop up menu
*
* @param view view containing popup menu.
*/
private fun showPopupMenu(view: View) = item?.let { item ->
// Create a PopupMenu, giving it the clicked view for an anchor
val popup = PopupMenu(view.context, view)
// Inflate our menu resource into the PopupMenu's Menu
popup.menuInflater.inflate(R.menu.chapter_recent, popup.menu)
// Hide download and show delete if the chapter is downloaded and
if (item.isDownloaded) {
popup.menu.findItem(R.id.action_download).isVisible = false
popup.menu.findItem(R.id.action_delete).isVisible = true
}
// Hide mark as unread when the chapter is unread
if (!item.chapter.read /*&& mangaChapter.chapter.last_page_read == 0*/) {
popup.menu.findItem(R.id.action_mark_as_unread).isVisible = false
}
// Hide mark as read when the chapter is read
if (item.chapter.read) {
popup.menu.findItem(R.id.action_mark_as_read).isVisible = false
}
// Set a listener so we are notified if a menu item is clicked
popup.setOnMenuItemClickListener { menuItem ->
with(adapter.controller) {
when (menuItem.itemId) {
R.id.action_download -> downloadChapter(item)
R.id.action_delete -> deleteChapter(item)
R.id.action_mark_as_read -> markAsRead(listOf(item))
R.id.action_mark_as_unread -> markAsUnread(listOf(item))
}
}
true
}
// Finally show the PopupMenu
popup.show()
}
} }

View File

@ -3,26 +3,14 @@ package eu.kanade.tachiyomi.ui.recent_updates
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractSectionableItem
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.ui.manga.chapter.BaseChapterItem
class RecentChapterItem(val chapter: Chapter, val manga: Manga, header: DateItem) : class RecentChapterItem(chapter: Chapter, val manga: Manga, header: DateItem) :
AbstractSectionableItem<RecentChapterHolder, DateItem>(header) { BaseChapterItem<RecentChapterHolder, DateItem>(chapter, header) {
private var _status: Int = 0
var status: Int
get() = download?.status ?: _status
set(value) { _status = value }
@Transient var download: Download? = null
val isDownloaded: Boolean
get() = status == Download.DOWNLOADED
override fun getLayoutRes(): Int { override fun getLayoutRes(): Int {
return R.layout.recent_chapters_item return R.layout.recent_chapters_item
@ -38,7 +26,6 @@ class RecentChapterItem(val chapter: Chapter, val manga: Manga, header: DateItem
position: Int, position: Int,
payloads: MutableList<Any?>? payloads: MutableList<Any?>?
) { ) {
holder.bind(this) holder.bind(this)
} }
@ -46,16 +33,4 @@ class RecentChapterItem(val chapter: Chapter, val manga: Manga, header: DateItem
return chapter.name.contains(text, false) || return chapter.name.contains(text, false) ||
manga.title.contains(text, false) manga.title.contains(text, false)
} }
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other is RecentChapterItem) {
return chapter.id!! == other.chapter.id!!
}
return false
}
override fun hashCode(): Int {
return chapter.id!!.hashCode()
}
} }

View File

@ -1,13 +1,15 @@
package eu.kanade.tachiyomi.ui.recent_updates package eu.kanade.tachiyomi.ui.recent_updates
import eu.davidea.flexibleadapter.FlexibleAdapter import androidx.recyclerview.widget.ItemTouchHelper
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.ui.manga.chapter.BaseChapterAdapter
class RecentChaptersAdapter(val controller: RecentChaptersController) : class RecentChaptersAdapter(val controller: RecentChaptersController) :
FlexibleAdapter<IFlexible<*>>(null, controller, true) { BaseChapterAdapter<IFlexible<*>>(controller) {
val coverClickListener: OnCoverClickListener = controller val coverClickListener: OnCoverClickListener = controller
var recents = emptyList<RecentChapterItem>() var recents = emptyList<RecentChapterItem>()
private var isAnimating = false
init { init {
setDisplayHeadersAtStartUp(true) setDisplayHeadersAtStartUp(true)
@ -22,13 +24,22 @@ class RecentChaptersAdapter(val controller: RecentChaptersController) :
fun performFilter() { fun performFilter() {
val s = getFilter(String::class.java) val s = getFilter(String::class.java)
if (s.isNullOrBlank()) { if (s.isNullOrBlank()) {
updateDataSet(recents) updateDataSet(recents, isAnimating)
} else { } else {
updateDataSet(recents.filter { it.filter(s) }) updateDataSet(recents.filter { it.filter(s) }, isAnimating)
} }
isAnimating = false
} }
interface OnCoverClickListener { interface OnCoverClickListener {
fun onCoverClick(position: Int) fun onCoverClick(position: Int)
} }
override fun onItemSwiped(position: Int, direction: Int) {
super.onItemSwiped(position, direction)
isAnimating = true
when (direction) {
ItemTouchHelper.LEFT -> controller.toggleMarkAsRead(position)
}
}
} }

View File

@ -1,33 +1,35 @@
package eu.kanade.tachiyomi.ui.recent_updates package eu.kanade.tachiyomi.ui.recent_updates
import android.app.Activity
import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.jakewharton.rxbinding.support.v4.widget.refreshes import androidx.recyclerview.widget.RecyclerView
import com.jakewharton.rxbinding.support.v7.widget.scrollStateChanges import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.SelectableAdapter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.DownloadService
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.library.LibraryUpdateService import eu.kanade.tachiyomi.data.library.LibraryUpdateService
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.BaseController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaDetailsController import eu.kanade.tachiyomi.ui.manga.MangaDetailsController
import eu.kanade.tachiyomi.ui.manga.chapter.BaseChapterAdapter
import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.system.notificationManager import eu.kanade.tachiyomi.util.system.notificationManager
import eu.kanade.tachiyomi.util.view.scrollViewWith import eu.kanade.tachiyomi.util.view.scrollViewWith
import eu.kanade.tachiyomi.util.view.snack import eu.kanade.tachiyomi.util.view.snack
import kotlinx.android.synthetic.main.download_bottom_sheet.*
import kotlinx.android.synthetic.main.main_activity.* import kotlinx.android.synthetic.main.main_activity.*
import kotlinx.android.synthetic.main.recent_chapters_controller.* import kotlinx.android.synthetic.main.recent_chapters_controller.*
import kotlinx.android.synthetic.main.recent_chapters_controller.empty_view
import timber.log.Timber import timber.log.Timber
/** /**
@ -35,21 +37,10 @@ import timber.log.Timber
* Uses [R.layout.recent_chapters_controller]. * Uses [R.layout.recent_chapters_controller].
* UI related actions should be called from here. * UI related actions should be called from here.
*/ */
class RecentChaptersController : NucleusController<RecentChaptersPresenter>(), class RecentChaptersController(bundle: Bundle? = null) : BaseController(bundle),
ActionMode.Callback, FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnUpdateListener,
FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemMoveListener,
FlexibleAdapter.OnItemLongClickListener, RecentChaptersAdapter.OnCoverClickListener, BaseChapterAdapter.DownloadInterface {
FlexibleAdapter.OnUpdateListener,
ConfirmDeleteChaptersDialog.Listener,
RecentChaptersAdapter.OnCoverClickListener {
init {
setHasOptionsMenu(true)
}
/**
* Action mode for multiple selection.
*/
private var actionMode: ActionMode? = null
/** /**
* Adapter containing the recent chapters. * Adapter containing the recent chapters.
@ -57,16 +48,14 @@ class RecentChaptersController : NucleusController<RecentChaptersPresenter>(),
var adapter: RecentChaptersAdapter? = null var adapter: RecentChaptersAdapter? = null
private set private set
private var query = "" private var presenter = RecentChaptersPresenter(this)
private var snack: Snackbar? = null
private var lastChapterId: Long? = null
override fun getTitle(): String? { override fun getTitle(): String? {
return resources?.getString(R.string.label_recent_updates) return resources?.getString(R.string.label_recent_updates)
} }
override fun createPresenter(): RecentChaptersPresenter {
return RecentChaptersPresenter()
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
return inflater.inflate(R.layout.recent_chapters_controller, container, false) return inflater.inflate(R.layout.recent_chapters_controller, container, false)
} }
@ -88,14 +77,14 @@ class RecentChaptersController : NucleusController<RecentChaptersPresenter>(),
adapter = RecentChaptersAdapter(this@RecentChaptersController) adapter = RecentChaptersAdapter(this@RecentChaptersController)
recycler.adapter = adapter recycler.adapter = adapter
recycler.scrollStateChanges().subscribeUntilDestroy { adapter?.isSwipeEnabled = true
// Disable swipe refresh when view is not at the top adapter?.itemTouchHelperCallback?.setSwipeFlags(
val firstPos = layoutManager.findFirstCompletelyVisibleItemPosition() ItemTouchHelper.LEFT
swipe_refresh.isEnabled = firstPos <= 0 )
} if (presenter.chapters.isNotEmpty()) adapter?.updateDataSet(presenter.chapters.toList())
swipe_refresh.setDistanceToTriggerSync((2 * 64 * view.resources.displayMetrics.density).toInt()) swipe_refresh.setDistanceToTriggerSync((2 * 64 * view.resources.displayMetrics.density).toInt())
swipe_refresh.refreshes().subscribeUntilDestroy { swipe_refresh.setOnRefreshListener {
if (!LibraryUpdateService.isRunning()) { if (!LibraryUpdateService.isRunning()) {
LibraryUpdateService.start(view.context) LibraryUpdateService.start(view.context)
view.snack(R.string.updating_library) { view.snack(R.string.updating_library) {
@ -107,23 +96,31 @@ class RecentChaptersController : NucleusController<RecentChaptersPresenter>(),
swipe_refresh.isRefreshing = false swipe_refresh.isRefreshing = false
} }
scrollViewWith(recycler, swipeRefreshLayout = swipe_refresh) scrollViewWith(recycler, swipeRefreshLayout = swipe_refresh, padBottom = true)
presenter.onCreate()
}
override fun onDestroy() {
super.onDestroy()
presenter.onDestroy()
} }
override fun onDestroyView(view: View) { override fun onDestroyView(view: View) {
adapter = null adapter = null
actionMode = null snack = null
super.onDestroyView(view) super.onDestroyView(view)
} }
/** override fun onActivityResumed(activity: Activity) {
* Returns selected chapters super.onActivityResumed(activity)
* @return list of selected chapters if (view != null) {
*/ refresh()
fun getSelectedChapters(): List<RecentChapterItem> { dl_bottom_sheet?.update()
val adapter = adapter ?: return emptyList()
return adapter.selectedPositions.mapNotNull { adapter.getItem(it) as? RecentChapterItem }
} }
}
fun refresh() = presenter.getUpdates()
/** /**
* Called when item in list is clicked * Called when item in list is clicked
@ -134,35 +131,9 @@ class RecentChaptersController : NucleusController<RecentChaptersPresenter>(),
// Get item from position // Get item from position
val item = adapter.getItem(position) as? RecentChapterItem ?: return false val item = adapter.getItem(position) as? RecentChapterItem ?: return false
if (actionMode != null && adapter.mode == SelectableAdapter.Mode.MULTI) {
toggleSelection(position)
return true
} else {
openChapter(item) openChapter(item)
return false return false
} }
}
/**
* Called when item in list is long clicked
* @param position position of clicked item
*/
override fun onItemLongClick(position: Int) {
if (actionMode == null)
actionMode = (activity as AppCompatActivity).startSupportActionMode(this)
toggleSelection(position)
}
/**
* Called to toggle selection
* @param position position of selected item
*/
private fun toggleSelection(position: Int) {
val adapter = adapter ?: return
adapter.toggleSelection(position)
actionMode?.invalidate()
}
/** /**
* Open chapter in reader * Open chapter in reader
@ -174,24 +145,21 @@ class RecentChaptersController : NucleusController<RecentChaptersPresenter>(),
startActivity(intent) startActivity(intent)
} }
/**
* Download selected items
* @param chapters list of selected [RecentChapter]s
*/
fun downloadChapters(chapters: List<RecentChapterItem>) {
destroyActionModeIfNeeded()
presenter.downloadChapters(chapters)
}
/** /**
* Populate adapter with chapters * Populate adapter with chapters
* @param chapters list of [Any] * @param chapters list of [Any]
*/ */
fun onNextRecentChapters(chapters: List<RecentChapterItem>) { fun onNextRecentChapters(chapters: List<RecentChapterItem>) {
destroyActionModeIfNeeded()
adapter?.setItems(chapters) adapter?.setItems(chapters)
} }
fun updateChapterDownload(download: Download) {
if (view == null) return
val id = download.chapter.id ?: return
val holder = recycler.findViewHolderForItemId(id) as? RecentChapterHolder ?: return
holder.notifyStatus(download.status, download.progress)
}
override fun onUpdateEmptyView(size: Int) { override fun onUpdateEmptyView(size: Int) {
if (size > 0) { if (size > 0) {
empty_view?.hide() empty_view?.hide()
@ -200,12 +168,19 @@ class RecentChaptersController : NucleusController<RecentChaptersPresenter>(),
} }
} }
override fun onItemMove(fromPosition: Int, toPosition: Int) { }
override fun shouldMoveItem(fromPosition: Int, toPosition: Int) = true
override fun onActionStateChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
swipe_refresh.isEnabled = actionState != ItemTouchHelper.ACTION_STATE_SWIPE
}
/** /**
* 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)?.notifyStatus(download.status) getHolder(download)?.notifyStatus(download.status, download.progress)
} }
/** /**
@ -218,49 +193,54 @@ class RecentChaptersController : NucleusController<RecentChaptersPresenter>(),
/** /**
* Mark chapter as read * Mark chapter as read
* @param chapters list of chapters * @param position position of chapter item
*/ */
fun markAsRead(chapters: List<RecentChapterItem>) { fun toggleMarkAsRead(position: Int) {
presenter.markChapterRead(chapters, true) val item = adapter?.getItem(position) as? RecentChapterItem ?: return
if (presenter.preferences.removeAfterMarkedAsRead()) { val chapter = item.chapter
deleteChapters(chapters) val lastRead = chapter.last_page_read
val pagesLeft = chapter.pages_left
val read = item.chapter.read
lastChapterId = chapter.id
presenter.markChapterRead(item, !read)
if (!read) {
snack = view?.snack(R.string.marked_as_read, Snackbar.LENGTH_INDEFINITE) {
var undoing = false
setAction(R.string.action_undo) {
presenter.markChapterRead(item, !item.chapter.read, lastRead, pagesLeft)
undoing = true
}
addCallback(object : BaseTransientBottomBar.BaseCallback<Snackbar>() {
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
super.onDismissed(transientBottomBar, event)
if (!undoing && presenter.preferences.removeAfterMarkedAsRead()) {
lastChapterId = chapter.id
presenter.deleteChapter(chapter, item.manga)
}
}
})
}
(activity as? MainActivity)?.setUndoSnackBar(snack)
}
// presenter.markChapterRead(item, !item.chapter.read)
}
override fun downloadChapter(position: Int) {
val view = view ?: return
val item = adapter?.getItem(position) as? RecentChapterItem ?: return
val chapter = item.chapter
val manga = item.manga
if (item.status != Download.NOT_DOWNLOADED && item.status != Download.ERROR) {
presenter.deleteChapter(chapter, manga)
} else {
if (item.status == Download.ERROR) DownloadService.start(view.context)
else presenter.downloadChapters(listOf(item))
} }
} }
override fun deleteChapters(chaptersToDelete: List<RecentChapterItem>) { override fun startDownloadNow(position: Int) {
destroyActionModeIfNeeded() val chapter = (adapter?.getItem(position) as? RecentChapterItem)?.chapter ?: return
presenter.deleteChapters(chaptersToDelete) presenter.startDownloadChapterNow(chapter)
}
/**
* Destory [ActionMode] if it's shown
*/
private fun destroyActionModeIfNeeded() {
actionMode?.finish()
}
/**
* Mark chapter as unread
* @param chapters list of selected [RecentChapter]
*/
fun markAsUnread(chapters: List<RecentChapterItem>) {
presenter.markChapterRead(chapters, false)
}
/**
* Start downloading chapter
* @param chapter selected chapter with manga
*/
fun downloadChapter(chapter: RecentChapterItem) {
presenter.downloadChapters(listOf(chapter))
}
/**
* Start deleting chapter
* @param chapter selected chapter with manga
*/
fun deleteChapter(chapter: RecentChapterItem) {
presenter.deleteChapters(listOf(chapter))
} }
override fun onCoverClick(position: Int) { override fun onCoverClick(position: Int) {
@ -286,69 +266,4 @@ class RecentChaptersController : NucleusController<RecentChaptersPresenter>(),
fun onChaptersDeletedError(error: Throwable) { fun onChaptersDeletedError(error: Throwable) {
Timber.e(error) Timber.e(error)
} }
/**
* 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 = SelectableAdapter.Mode.MULTI
return true
}
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
val count = adapter?.selectedItemCount ?: 0
if (count == 0) {
// Destroy action mode if there are no items selected.
destroyActionModeIfNeeded()
} else {
mode.title = resources?.getString(R.string.label_selected, count)
}
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 -> ConfirmDeleteChaptersDialog(this, getSelectedChapters())
.showDialog(router)
else -> return false
}
return true
}
/**
* Called when ActionMode destroyed
* @param mode the ActionMode object
*/
override fun onDestroyActionMode(mode: ActionMode?) {
adapter?.mode = SelectableAdapter.Mode.IDLE
adapter?.clearSelection()
actionMode = null
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.recent_updates, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_sort -> {
/*router.setRoot(
RecentlyReadController().withFadeTransaction().tag(R.id.nav_recents.toString()))
Injekt.get<PreferencesHelper>().showRecentUpdates().set(false)
(activity as? MainActivity)?.updateRecentsIcon()*/
}
}
return super.onOptionsItemSelected(item)
}
} }

View File

@ -1,17 +1,24 @@
package eu.kanade.tachiyomi.ui.recent_updates package eu.kanade.tachiyomi.ui.recent_updates
import android.os.Bundle
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaChapter import eu.kanade.tachiyomi.data.database.models.MangaChapter
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
import eu.kanade.tachiyomi.data.library.LibraryServiceListener
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.util.system.executeOnIO
import rx.Observable import kotlinx.coroutines.CoroutineScope
import rx.android.schedulers.AndroidSchedulers import kotlinx.coroutines.Dispatchers
import rx.schedulers.Schedulers import kotlinx.coroutines.Job
import timber.log.Timber import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.Calendar import java.util.Calendar
@ -19,67 +26,64 @@ import java.util.Date
import java.util.TreeMap import java.util.TreeMap
class RecentChaptersPresenter( class RecentChaptersPresenter(
private val controller: RecentChaptersController,
val preferences: PreferencesHelper = Injekt.get(), val preferences: PreferencesHelper = Injekt.get(),
private val db: DatabaseHelper = Injekt.get(), private val db: DatabaseHelper = Injekt.get(),
private val downloadManager: DownloadManager = Injekt.get(), private val downloadManager: DownloadManager = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get() private val sourceManager: SourceManager = Injekt.get()
) : BasePresenter<RecentChaptersController>() { ) : DownloadQueue.DownloadListener, LibraryServiceListener {
/** /**
* List containing chapter and manga information * List containing chapter and manga information
*/ */
private var chapters: List<RecentChapterItem> = emptyList() var chapters: List<RecentChapterItem> = emptyList()
override fun onCreate(savedState: Bundle?) { private var scope = CoroutineScope(Job() + Dispatchers.Default)
super.onCreate(savedState)
getRecentChaptersObservable() fun onCreate() {
.observeOn(AndroidSchedulers.mainThread()) downloadManager.addListener(this)
.subscribeLatestCache(RecentChaptersController::onNextRecentChapters) LibraryUpdateService.setListener(this)
getUpdates()
getChapterStatusObservable()
.subscribeLatestCache(RecentChaptersController::onChapterStatusChange) {
_, error -> Timber.e(error)
}
} }
/** fun getUpdates() {
* Get observable containing recent chapters and date scope.launch {
*
* @return observable containing recent chapters and date
*/
fun getRecentChaptersObservable(): Observable<List<RecentChapterItem>> {
// Set date limit for recent chapters
val cal = Calendar.getInstance().apply { val cal = Calendar.getInstance().apply {
time = Date() time = Date()
add(Calendar.MONTH, -1) add(Calendar.MONTH, -1)
} }
val mangaChapters = db.getRecentChapters(cal.time).executeOnIO()
return db.getRecentChapters(cal.time).asRxObservable()
// Convert to a list of recent chapters.
.map { mangaChapters ->
val map = TreeMap<Date, MutableList<MangaChapter>> { d1, d2 -> d2.compareTo(d1) } val map = TreeMap<Date, MutableList<MangaChapter>> { d1, d2 -> d2.compareTo(d1) }
val byDay = mangaChapters val byDay = mangaChapters.groupByTo(map, { getMapKey(it.chapter.date_fetch) })
.groupByTo(map, { getMapKey(it.chapter.date_fetch) }) val items = byDay.flatMap {
byDay.flatMap {
val dateItem = DateItem(it.key) val dateItem = DateItem(it.key)
it.value.map { RecentChapterItem(it.chapter, it.manga, dateItem) } it.value.map { mc ->
RecentChapterItem(mc.chapter, mc.manga, dateItem) }
}
setDownloadedChapters(items)
chapters = items
withContext(Dispatchers.Main) { controller.onNextRecentChapters(chapters) }
} }
} }
.doOnNext {
it.forEach { item ->
// Find an active download for this chapter.
val download = downloadManager.queue.find { it.chapter.id == item.chapter.id }
// If there's an active download, assign it, otherwise ask the manager if fun onDestroy() {
// the chapter is downloaded and assign it to the status. downloadManager.removeListener(this)
if (download != null) { LibraryUpdateService.removeListener(this)
item.download = download }
fun cancelScope() {
scope.cancel()
}
override fun updateDownload(download: Download) {
chapters.find { it.chapter.id == download.chapter.id }?.download = download
scope.launch(Dispatchers.Main) {
controller.updateChapterDownload(download)
} }
} }
setDownloadedChapters(it)
chapters = it override fun onUpdateManga(manga: LibraryManga) {
} getUpdates()
} }
/** /**
@ -98,44 +102,18 @@ class RecentChaptersPresenter(
return cal.time return cal.time
} }
/**
* Returns observable containing chapter status.
*
* @return download object containing download progress.
*/
private fun getChapterStatusObservable(): Observable<Download> {
return downloadManager.queue.getStatusObservable()
.observeOn(AndroidSchedulers.mainThread())
.doOnNext { download -> onDownloadStatusChange(download) }
}
/** /**
* Finds and assigns the list of downloaded chapters. * Finds and assigns the list of downloaded chapters.
* *
* @param items the list of chapter from the database. * @param chapters the list of chapter from the database.
*/ */
private fun setDownloadedChapters(items: List<RecentChapterItem>) { private fun setDownloadedChapters(chapters: List<RecentChapterItem>) {
for (item in items) { for (item in chapters) {
val manga = item.manga if (downloadManager.isChapterDownloaded(item.chapter, item.manga)) {
val chapter = item.chapter
if (downloadManager.isChapterDownloaded(chapter, manga)) {
item.status = Download.DOWNLOADED item.status = Download.DOWNLOADED
} } else if (downloadManager.hasQueue()) {
} item.status = downloadManager.queue.find { it.chapter.id == item.chapter.id }
} ?.status ?: 0
/**
* Update status of chapters.
*
* @param download download object containing progress.
*/
private fun onDownloadStatusChange(download: Download) {
// Assign the download to the model object.
if (download.status == Download.QUEUE) {
val chapter = chapters.find { it.chapter.id == download.chapter.id }
if (chapter != null && chapter.download == null) {
chapter.download = download
} }
} }
} }
@ -146,34 +124,44 @@ class RecentChaptersPresenter(
* @param items list of selected chapters * @param items list of selected chapters
* @param read read status * @param read read status
*/ */
fun markChapterRead(items: List<RecentChapterItem>, read: Boolean) { fun markChapterRead(
val chapters = items.map { it.chapter } item: RecentChapterItem,
chapters.forEach { read: Boolean,
it.read = read lastRead: Int? = null,
pagesLeft: Int? = null
) {
item.chapter.apply {
this.read = read
if (!read) { if (!read) {
it.last_page_read = 0 last_page_read = lastRead ?: 0
it.pages_left = 0 pages_left = pagesLeft ?: 0
} }
} }
db.updateChapterProgress(item.chapter).executeAsBlocking()
controller.onNextRecentChapters(this.chapters)
}
Observable.fromCallable { db.updateChaptersProgress(chapters).executeAsBlocking() } fun startDownloadChapterNow(chapter: Chapter) {
.subscribeOn(Schedulers.io()) downloadManager.startDownloadNow(chapter)
.subscribe()
} }
/** /**
* Delete selected chapters * Deletes the given list of chapter.
* * @param chapter the chapter to delete.
* @param chapters list of chapters
*/ */
fun deleteChapters(chapters: List<RecentChapterItem>) { fun deleteChapter(chapter: Chapter, manga: Manga, update: Boolean = true) {
Observable.just(chapters) val source = Injekt.get<SourceManager>().getOrStub(manga.source)
.doOnNext { deleteChaptersInternal(it) } downloadManager.deleteChapters(listOf(chapter), manga, source)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) if (update) {
.subscribeFirst({ view, _ -> val item = chapters.find { it.chapter.id == chapter.id } ?: return
view.onChaptersDeleted() item.apply {
}, RecentChaptersController::onChaptersDeletedError) status = Download.NOT_DOWNLOADED
download = null
}
controller.onNextRecentChapters(chapters)
}
} }
/** /**
@ -183,24 +171,4 @@ class RecentChaptersPresenter(
fun downloadChapters(items: List<RecentChapterItem>) { fun downloadChapters(items: List<RecentChapterItem>) {
items.forEach { downloadManager.downloadChapters(it.manga, listOf(it.chapter)) } items.forEach { downloadManager.downloadChapters(it.manga, listOf(it.chapter)) }
} }
/**
* Delete selected chapters
*
* @param items chapters selected
*/
private fun deleteChaptersInternal(chapterItems: List<RecentChapterItem>) {
val itemsByManga = chapterItems.groupBy { it.manga.id }
for ((_, items) in itemsByManga) {
val manga = items.first().manga
val source = sourceManager.get(manga.source) ?: continue
val chapters = items.map { it.chapter }
downloadManager.deleteChapters(chapters, manga, source)
items.forEach {
it.status = Download.NOT_DOWNLOADED
it.download = null
}
}
}
} }

View File

@ -82,7 +82,7 @@ class RecentlyReadController(bundle: Bundle? = null) : BaseController(bundle),
recycler.setHasFixedSize(true) recycler.setHasFixedSize(true)
recycler.setOnApplyWindowInsetsListener(RecyclerWindowInsetsListener) recycler.setOnApplyWindowInsetsListener(RecyclerWindowInsetsListener)
resetProgressItem() resetProgressItem()
scrollViewWith(recycler) scrollViewWith(recycler, padBottom = true)
if (recentItems != null) if (recentItems != null)
adapter?.updateDataSet(recentItems!!.toList()) adapter?.updateDataSet(recentItems!!.toList())

View File

@ -1,9 +1,34 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" 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/material_component_lists_two_line_height">
<FrameLayout
android:id="@+id/right_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:background="@color/material_green_800"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/read"
android:layout_width="24dp"
android:layout_height="24dp"
android:tint="@color/md_white_1000"
android:layout_gravity="end|center"
android:layout_marginEnd="21dp"
android:src="@drawable/eye" />
</FrameLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/front_view"
android:layout_width="match_parent"
android:layout_height="@dimen/material_component_lists_two_line_height" android:layout_height="@dimen/material_component_lists_two_line_height"
android:background="?attr/selectable_list_drawable"> android:background="?attr/selectable_list_drawable">
@ -27,8 +52,8 @@
android:maxLines="1" android:maxLines="1"
android:textAppearance="@style/TextAppearance.Regular.Body1" android:textAppearance="@style/TextAppearance.Regular.Body1"
app:layout_constraintBottom_toTopOf="@+id/chapter_title" app:layout_constraintBottom_toTopOf="@+id/chapter_title"
app:layout_constraintEnd_toStartOf="@+id/download_button"
app:layout_constraintStart_toEndOf="@+id/manga_cover" app:layout_constraintStart_toEndOf="@+id/manga_cover"
app:layout_constraintEnd_toStartOf="@+id/chapter_menu"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" app:layout_constraintVertical_chainStyle="packed"
tools:text="Manga title" /> tools:text="Manga title" />
@ -42,40 +67,20 @@
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:textAppearance="@style/TextAppearance.Regular.Caption" android:textAppearance="@style/TextAppearance.Regular.Caption"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/download_button"
app:layout_constraintStart_toEndOf="@+id/manga_cover" app:layout_constraintStart_toEndOf="@+id/manga_cover"
app:layout_constraintTop_toBottomOf="@+id/manga_full_title" app:layout_constraintTop_toBottomOf="@+id/manga_full_title"
app:layout_constraintEnd_toStartOf="@+id/download_text"
app:layout_constraintBottom_toBottomOf="parent"
tools:text="Chapter title" /> tools:text="Chapter title" />
<TextView <include
android:id="@+id/download_text" layout="@layout/download_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginBottom="3dp"
android:textAllCaps="true"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
tools:text="Downloaded" />
<FrameLayout
android:id="@+id/chapter_menu"
android:layout_width="50dp" android:layout_width="50dp"
android:layout_height="0dp" android:layout_height="0dp"
android:paddingBottom="18dp" android:layout_marginEnd="8dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent" />
<ImageView </androidx.constraintlayout.widget.ConstraintLayout>
android:id="@+id/chapter_menu_icon" </FrameLayout>
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center|end"
android:layout_marginEnd="16dp" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -137,7 +137,7 @@
<string name="action_restore">Restore</string> <string name="action_restore">Restore</string>
<string name="action_webview_back">Back</string> <string name="action_webview_back">Back</string>
<string name="action_webview_forward">Forward</string> <string name="action_webview_forward">Forward</string>
<string name="action_auto_check_extensions">Auto-check for updates</string> <string name="action_auto_check_extensions">Notify for extension updates</string>
<string name="action_search_manually">Search manually</string> <string name="action_search_manually">Search manually</string>
<string name="action_migrate_now">Migrate now</string> <string name="action_migrate_now">Migrate now</string>
<string name="action_copy_now">Copy now</string> <string name="action_copy_now">Copy now</string>