Rename recently read / recent updates classes/layouts to match new names
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
package eu.kanade.tachiyomi.ui.recent.history
|
||||
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import java.text.DateFormat
|
||||
import java.text.DecimalFormat
|
||||
import java.text.DecimalFormatSymbols
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* Adapter of HistoryHolder.
|
||||
* Connection between Fragment and Holder
|
||||
* Holder updates should be called from here.
|
||||
*
|
||||
* @param controller a HistoryController object
|
||||
* @constructor creates an instance of the adapter.
|
||||
*/
|
||||
class HistoryAdapter(controller: HistoryController) :
|
||||
FlexibleAdapter<HistoryItem>(null, controller, true) {
|
||||
|
||||
val sourceManager by injectLazy<SourceManager>()
|
||||
|
||||
val resumeClickListener: OnResumeClickListener = controller
|
||||
|
||||
val removeClickListener: OnRemoveClickListener = controller
|
||||
|
||||
val coverClickListener: OnCoverClickListener = controller
|
||||
|
||||
/**
|
||||
* DecimalFormat used to display correct chapter number
|
||||
*/
|
||||
val decimalFormat = DecimalFormat("#.###", DecimalFormatSymbols()
|
||||
.apply { decimalSeparator = '.' })
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
val dateFormat: DateFormat = preferences.dateFormat().getOrDefault()
|
||||
|
||||
interface OnResumeClickListener {
|
||||
fun onResumeClick(position: Int)
|
||||
}
|
||||
|
||||
interface OnRemoveClickListener {
|
||||
fun onRemoveClick(position: Int)
|
||||
}
|
||||
|
||||
interface OnCoverClickListener {
|
||||
fun onCoverClick(position: Int)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package eu.kanade.tachiyomi.ui.recent.history
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.History
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.RootController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.android.synthetic.main.history_controller.empty_view
|
||||
import kotlinx.android.synthetic.main.history_controller.recycler
|
||||
|
||||
/**
|
||||
* Fragment that shows recently read manga.
|
||||
* Uses R.layout.fragment_recently_read.
|
||||
* UI related actions should be called from here.
|
||||
*/
|
||||
class HistoryController : NucleusController<HistoryPresenter>(),
|
||||
RootController,
|
||||
FlexibleAdapter.OnUpdateListener,
|
||||
HistoryAdapter.OnRemoveClickListener,
|
||||
HistoryAdapter.OnResumeClickListener,
|
||||
HistoryAdapter.OnCoverClickListener,
|
||||
RemoveHistoryDialog.Listener {
|
||||
|
||||
/**
|
||||
* Adapter containing the recent manga.
|
||||
*/
|
||||
var adapter: HistoryAdapter? = null
|
||||
private set
|
||||
|
||||
override fun getTitle(): String? {
|
||||
return resources?.getString(R.string.label_recent_manga)
|
||||
}
|
||||
|
||||
override fun createPresenter(): HistoryPresenter {
|
||||
return HistoryPresenter()
|
||||
}
|
||||
|
||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||
return inflater.inflate(R.layout.history_controller, container, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when view is created
|
||||
*
|
||||
* @param view created view
|
||||
*/
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
// Initialize adapter
|
||||
recycler.layoutManager = LinearLayoutManager(view.context)
|
||||
adapter = HistoryAdapter(this@HistoryController)
|
||||
recycler.setHasFixedSize(true)
|
||||
recycler.adapter = adapter
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
adapter = null
|
||||
super.onDestroyView(view)
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate adapter with chapters
|
||||
*
|
||||
* @param mangaHistory list of manga history
|
||||
*/
|
||||
fun onNextManga(mangaHistory: List<HistoryItem>) {
|
||||
adapter?.updateDataSet(mangaHistory)
|
||||
}
|
||||
|
||||
override fun onUpdateEmptyView(size: Int) {
|
||||
if (size > 0) {
|
||||
empty_view.hide()
|
||||
} else {
|
||||
empty_view.show(R.drawable.ic_history_black_128dp, R.string.information_no_recent_manga)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResumeClick(position: Int) {
|
||||
val activity = activity ?: return
|
||||
val (manga, chapter, _) = adapter?.getItem(position)?.mch ?: return
|
||||
|
||||
val nextChapter = presenter.getNextChapter(chapter, manga)
|
||||
if (nextChapter != null) {
|
||||
val intent = ReaderActivity.newIntent(activity, manga, nextChapter)
|
||||
startActivity(intent)
|
||||
} else {
|
||||
activity.toast(R.string.no_next_chapter)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRemoveClick(position: Int) {
|
||||
val (manga, _, history) = adapter?.getItem(position)?.mch ?: return
|
||||
RemoveHistoryDialog(this, manga, history).showDialog(router)
|
||||
}
|
||||
|
||||
override fun onCoverClick(position: Int) {
|
||||
val manga = adapter?.getItem(position)?.mch?.manga ?: return
|
||||
router.pushController(MangaController(manga).withFadeTransaction())
|
||||
}
|
||||
|
||||
override fun removeHistory(manga: Manga, history: History, all: Boolean) {
|
||||
if (all) {
|
||||
// Reset last read of chapter to 0L
|
||||
presenter.removeAllFromHistory(manga.id!!)
|
||||
} else {
|
||||
// Remove all chapters belonging to manga from library
|
||||
presenter.removeFromHistory(history)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package eu.kanade.tachiyomi.ui.recent.history
|
||||
|
||||
import android.view.View
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
|
||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.util.lang.toTimestampString
|
||||
import java.util.Date
|
||||
import kotlinx.android.synthetic.main.history_item.cover
|
||||
import kotlinx.android.synthetic.main.history_item.last_read
|
||||
import kotlinx.android.synthetic.main.history_item.manga_source
|
||||
import kotlinx.android.synthetic.main.history_item.manga_title
|
||||
import kotlinx.android.synthetic.main.history_item.remove
|
||||
import kotlinx.android.synthetic.main.history_item.resume
|
||||
|
||||
/**
|
||||
* Holder that contains recent manga item
|
||||
* Uses R.layout.item_recently_read.
|
||||
* UI related actions should be called from here.
|
||||
*
|
||||
* @param view the inflated view for this holder.
|
||||
* @param adapter the adapter handling this holder.
|
||||
* @constructor creates a new recent chapter holder.
|
||||
*/
|
||||
class HistoryHolder(
|
||||
view: View,
|
||||
val adapter: HistoryAdapter
|
||||
) : BaseFlexibleViewHolder(view, adapter) {
|
||||
|
||||
init {
|
||||
remove.setOnClickListener {
|
||||
adapter.removeClickListener.onRemoveClick(adapterPosition)
|
||||
}
|
||||
|
||||
resume.setOnClickListener {
|
||||
adapter.resumeClickListener.onResumeClick(adapterPosition)
|
||||
}
|
||||
|
||||
cover.setOnClickListener {
|
||||
adapter.coverClickListener.onCoverClick(adapterPosition)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set values of view
|
||||
*
|
||||
* @param item item containing history information
|
||||
*/
|
||||
fun bind(item: MangaChapterHistory) {
|
||||
// Retrieve objects
|
||||
val (manga, chapter, history) = item
|
||||
|
||||
// Set manga title
|
||||
manga_title.text = manga.title
|
||||
|
||||
// Set source + chapter title
|
||||
val formattedNumber = adapter.decimalFormat.format(chapter.chapter_number.toDouble())
|
||||
manga_source.text = itemView.context.getString(R.string.recent_manga_source)
|
||||
.format(adapter.sourceManager.getOrStub(manga.source).toString(), formattedNumber)
|
||||
|
||||
// Set last read timestamp title
|
||||
last_read.text = Date(history.last_read).toTimestampString(adapter.dateFormat)
|
||||
|
||||
// Set cover
|
||||
GlideApp.with(itemView.context).clear(cover)
|
||||
if (!manga.thumbnail_url.isNullOrEmpty()) {
|
||||
GlideApp.with(itemView.context)
|
||||
.load(manga)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.centerCrop()
|
||||
.into(cover)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package eu.kanade.tachiyomi.ui.recent.history
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
|
||||
|
||||
class HistoryItem(val mch: MangaChapterHistory) : AbstractFlexibleItem<HistoryHolder>() {
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.history_item
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): HistoryHolder {
|
||||
return HistoryHolder(view, adapter as HistoryAdapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: HistoryHolder,
|
||||
position: Int,
|
||||
payloads: List<Any?>?
|
||||
) {
|
||||
|
||||
holder.bind(mch)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other is HistoryItem) {
|
||||
return mch.manga.id == other.mch.manga.id
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return mch.manga.id!!.hashCode()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package eu.kanade.tachiyomi.ui.recent.history
|
||||
|
||||
import android.os.Bundle
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.History
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import java.util.Calendar
|
||||
import java.util.Comparator
|
||||
import java.util.Date
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* Presenter of HistoryFragment.
|
||||
* Contains information and data for fragment.
|
||||
* Observable updates should be called from here.
|
||||
*/
|
||||
class HistoryPresenter : BasePresenter<HistoryController>() {
|
||||
|
||||
/**
|
||||
* Used to connect to database
|
||||
*/
|
||||
val db: DatabaseHelper by injectLazy()
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
// Used to get a list of recently read manga
|
||||
getRecentMangaObservable()
|
||||
.subscribeLatestCache(HistoryController::onNextManga)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent manga observable
|
||||
* @return list of history
|
||||
*/
|
||||
fun getRecentMangaObservable(): Observable<List<HistoryItem>> {
|
||||
// Set date for recent manga
|
||||
val cal = Calendar.getInstance()
|
||||
cal.time = Date()
|
||||
cal.add(Calendar.MONTH, -1)
|
||||
|
||||
return db.getRecentManga(cal.time).asRxObservable()
|
||||
.map { recents -> recents.map(::HistoryItem) }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset last read of chapter to 0L
|
||||
* @param history history belonging to chapter
|
||||
*/
|
||||
fun removeFromHistory(history: History) {
|
||||
history.last_read = 0L
|
||||
db.updateHistoryLastRead(history).asRxObservable()
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all chapters belonging to manga from history.
|
||||
* @param mangaId id of manga
|
||||
*/
|
||||
fun removeAllFromHistory(mangaId: Long) {
|
||||
db.getHistoryByMangaId(mangaId).asRxSingle()
|
||||
.map { list ->
|
||||
list.forEach { it.last_read = 0L }
|
||||
db.updateHistoryLastRead(list).executeAsBlocking()
|
||||
}
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the next chapter of the given one.
|
||||
*
|
||||
* @param chapter the chapter of the history object.
|
||||
* @param manga the manga of the chapter.
|
||||
*/
|
||||
fun getNextChapter(chapter: Chapter, manga: Manga): Chapter? {
|
||||
if (!chapter.read) {
|
||||
return chapter
|
||||
}
|
||||
|
||||
val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) {
|
||||
Manga.SORTING_SOURCE -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) }
|
||||
Manga.SORTING_NUMBER -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) }
|
||||
else -> throw NotImplementedError("Unknown sorting method")
|
||||
}
|
||||
|
||||
val chapters = db.getChapters(manga).executeAsBlocking()
|
||||
.sortedWith(Comparator<Chapter> { c1, c2 -> sortFunction(c1, c2) })
|
||||
|
||||
val currChapterIndex = chapters.indexOfFirst { chapter.id == it.id }
|
||||
return when (manga.sorting) {
|
||||
Manga.SORTING_SOURCE -> chapters.getOrNull(currChapterIndex + 1)
|
||||
Manga.SORTING_NUMBER -> {
|
||||
val chapterNumber = chapter.chapter_number
|
||||
|
||||
((currChapterIndex + 1) until chapters.size)
|
||||
.map { chapters[it] }
|
||||
.firstOrNull {
|
||||
it.chapter_number > chapterNumber &&
|
||||
it.chapter_number <= chapterNumber + 1
|
||||
}
|
||||
}
|
||||
else -> throw NotImplementedError("Unknown sorting method")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package eu.kanade.tachiyomi.ui.recent.history
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.History
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import eu.kanade.tachiyomi.widget.DialogCheckboxView
|
||||
|
||||
class RemoveHistoryDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
|
||||
where T : Controller, T : RemoveHistoryDialog.Listener {
|
||||
|
||||
private var manga: Manga? = null
|
||||
|
||||
private var history: History? = null
|
||||
|
||||
constructor(target: T, manga: Manga, history: History) : this() {
|
||||
this.manga = manga
|
||||
this.history = history
|
||||
targetController = target
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
val activity = activity!!
|
||||
|
||||
// Create custom view
|
||||
val dialogCheckboxView = DialogCheckboxView(activity).apply {
|
||||
setDescription(R.string.dialog_with_checkbox_remove_description)
|
||||
setOptionDescription(R.string.dialog_with_checkbox_reset)
|
||||
}
|
||||
|
||||
return MaterialDialog.Builder(activity)
|
||||
.title(R.string.action_remove)
|
||||
.customView(dialogCheckboxView, true)
|
||||
.positiveText(R.string.action_remove)
|
||||
.negativeText(android.R.string.cancel)
|
||||
.onPositive { _, _ -> onPositive(dialogCheckboxView.isChecked()) }
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun onPositive(checked: Boolean) {
|
||||
val target = targetController as? Listener ?: return
|
||||
val manga = manga ?: return
|
||||
val history = history ?: return
|
||||
|
||||
target.removeHistory(manga, history, checked)
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun removeHistory(manga: Manga, history: History, all: Boolean)
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package eu.kanade.tachiyomi.ui.recent.updates
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
|
||||
class ConfirmDeleteChaptersDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
|
||||
where T : Controller, T : ConfirmDeleteChaptersDialog.Listener {
|
||||
|
||||
private var chaptersToDelete = emptyList<UpdatesItem>()
|
||||
|
||||
constructor(target: T, chaptersToDelete: List<UpdatesItem>) : this() {
|
||||
this.chaptersToDelete = chaptersToDelete
|
||||
targetController = target
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
return MaterialDialog.Builder(activity!!)
|
||||
.content(R.string.confirm_delete_chapters)
|
||||
.positiveText(android.R.string.ok)
|
||||
.negativeText(android.R.string.cancel)
|
||||
.onPositive { _, _ ->
|
||||
(targetController as? Listener)?.deleteChapters(chaptersToDelete)
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun deleteChapters(chaptersToDelete: List<UpdatesItem>)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package eu.kanade.tachiyomi.ui.recent.updates
|
||||
|
||||
import android.text.format.DateUtils
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import java.util.Date
|
||||
|
||||
class DateItem(val date: Date) : AbstractHeaderItem<DateItem.Holder>() {
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.updates_section_item
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
|
||||
return Holder(view, adapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
|
||||
holder.bind(this)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other is DateItem) {
|
||||
return date == other.date
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return date.hashCode()
|
||||
}
|
||||
|
||||
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter, true) {
|
||||
|
||||
private val now = Date().time
|
||||
|
||||
val section_text: TextView = view.findViewById(R.id.section_text)
|
||||
|
||||
fun bind(item: DateItem) {
|
||||
section_text.text = DateUtils.getRelativeTimeSpanString(item.date.time, now, DateUtils.DAY_IN_MILLIS)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package eu.kanade.tachiyomi.ui.recent.updates
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.bluelinelabs.conductor.Router
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
|
||||
class DeletingChaptersDialog(bundle: Bundle? = null) : DialogController(bundle) {
|
||||
|
||||
companion object {
|
||||
const val TAG = "deleting_dialog"
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
return MaterialDialog.Builder(activity!!)
|
||||
.progress(true, 0)
|
||||
.content(R.string.deleting)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun showDialog(router: Router) {
|
||||
showDialog(router, TAG)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package eu.kanade.tachiyomi.ui.recent.updates
|
||||
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
|
||||
class UpdatesAdapter(val controller: UpdatesController) :
|
||||
FlexibleAdapter<IFlexible<*>>(null, controller, true) {
|
||||
|
||||
val coverClickListener: OnCoverClickListener = controller
|
||||
|
||||
init {
|
||||
setDisplayHeadersAtStartUp(true)
|
||||
setStickyHeaders(true)
|
||||
}
|
||||
|
||||
interface OnCoverClickListener {
|
||||
fun onCoverClick(position: Int)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
package eu.kanade.tachiyomi.ui.recent.updates
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.jakewharton.rxbinding.support.v4.widget.refreshes
|
||||
import com.jakewharton.rxbinding.support.v7.widget.scrollStateChanges
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.SelectableAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.RootController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.popControllerWithTag
|
||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.android.synthetic.main.updates_controller.empty_view
|
||||
import kotlinx.android.synthetic.main.updates_controller.recycler
|
||||
import kotlinx.android.synthetic.main.updates_controller.swipe_refresh
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Fragment that shows recent chapters.
|
||||
* Uses [R.layout.updates_controller].
|
||||
* UI related actions should be called from here.
|
||||
*/
|
||||
class UpdatesController : NucleusController<UpdatesPresenter>(),
|
||||
RootController,
|
||||
NoToolbarElevationController,
|
||||
ActionMode.Callback,
|
||||
FlexibleAdapter.OnItemClickListener,
|
||||
FlexibleAdapter.OnItemLongClickListener,
|
||||
FlexibleAdapter.OnUpdateListener,
|
||||
ConfirmDeleteChaptersDialog.Listener,
|
||||
UpdatesAdapter.OnCoverClickListener {
|
||||
|
||||
/**
|
||||
* Action mode for multiple selection.
|
||||
*/
|
||||
private var actionMode: ActionMode? = null
|
||||
|
||||
/**
|
||||
* Adapter containing the recent chapters.
|
||||
*/
|
||||
var adapter: UpdatesAdapter? = null
|
||||
private set
|
||||
|
||||
override fun getTitle(): String? {
|
||||
return resources?.getString(R.string.label_recent_updates)
|
||||
}
|
||||
|
||||
override fun createPresenter(): UpdatesPresenter {
|
||||
return UpdatesPresenter()
|
||||
}
|
||||
|
||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||
return inflater.inflate(R.layout.updates_controller, container, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when view is created
|
||||
* @param view created view
|
||||
*/
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
view.context.notificationManager.cancel(Notifications.ID_NEW_CHAPTERS)
|
||||
// Init RecyclerView and adapter
|
||||
val layoutManager = LinearLayoutManager(view.context)
|
||||
recycler.layoutManager = layoutManager
|
||||
recycler.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
|
||||
recycler.setHasFixedSize(true)
|
||||
adapter = UpdatesAdapter(this@UpdatesController)
|
||||
recycler.adapter = adapter
|
||||
|
||||
recycler.scrollStateChanges().subscribeUntilDestroy {
|
||||
// Disable swipe refresh when view is not at the top
|
||||
val firstPos = layoutManager.findFirstCompletelyVisibleItemPosition()
|
||||
swipe_refresh.isEnabled = firstPos <= 0
|
||||
}
|
||||
|
||||
swipe_refresh.setDistanceToTriggerSync((2 * 64 * view.resources.displayMetrics.density).toInt())
|
||||
swipe_refresh.refreshes().subscribeUntilDestroy {
|
||||
if (!LibraryUpdateService.isRunning(view.context)) {
|
||||
LibraryUpdateService.start(view.context)
|
||||
view.context.toast(R.string.action_update_library)
|
||||
}
|
||||
// It can be a very long operation, so we disable swipe refresh and show a toast.
|
||||
swipe_refresh.isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
adapter = null
|
||||
actionMode = null
|
||||
super.onDestroyView(view)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns selected chapters
|
||||
* @return list of selected chapters
|
||||
*/
|
||||
fun getSelectedChapters(): List<UpdatesItem> {
|
||||
val adapter = adapter ?: return emptyList()
|
||||
return adapter.selectedPositions.mapNotNull { adapter.getItem(it) as? UpdatesItem }
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when item in list is clicked
|
||||
* @param position position of clicked item
|
||||
*/
|
||||
override fun onItemClick(view: View, position: Int): Boolean {
|
||||
val adapter = adapter ?: return false
|
||||
|
||||
// Get item from position
|
||||
val item = adapter.getItem(position) as? UpdatesItem ?: return false
|
||||
if (actionMode != null && adapter.mode == SelectableAdapter.Mode.MULTI) {
|
||||
toggleSelection(position)
|
||||
return true
|
||||
} else {
|
||||
openChapter(item)
|
||||
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
|
||||
* @param chapter selected chapter
|
||||
*/
|
||||
private fun openChapter(item: UpdatesItem) {
|
||||
val activity = activity ?: return
|
||||
val intent = ReaderActivity.newIntent(activity, item.manga, item.chapter)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Download selected items
|
||||
* @param chapters list of selected [UpdatesItem]s
|
||||
*/
|
||||
fun downloadChapters(chapters: List<UpdatesItem>) {
|
||||
destroyActionModeIfNeeded()
|
||||
presenter.downloadChapters(chapters)
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate adapter with chapters
|
||||
* @param chapters list of [Any]
|
||||
*/
|
||||
fun onNextRecentChapters(chapters: List<IFlexible<*>>) {
|
||||
destroyActionModeIfNeeded()
|
||||
adapter?.updateDataSet(chapters)
|
||||
}
|
||||
|
||||
override fun onUpdateEmptyView(size: Int) {
|
||||
if (size > 0) {
|
||||
empty_view?.hide()
|
||||
} else {
|
||||
empty_view?.show(R.drawable.ic_new_releases_black_128dp, R.string.information_no_recent)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update download status of chapter
|
||||
* @param download [Download] object containing download progress.
|
||||
*/
|
||||
fun onChapterStatusChange(download: Download) {
|
||||
getHolder(download)?.notifyStatus(download.status)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns holder belonging to chapter
|
||||
* @param download [Download] object containing download progress.
|
||||
*/
|
||||
private fun getHolder(download: Download): UpdatesHolder? {
|
||||
return recycler?.findViewHolderForItemId(download.chapter.id!!) as? UpdatesHolder
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark chapter as read
|
||||
* @param chapters list of chapters
|
||||
*/
|
||||
fun markAsRead(chapters: List<UpdatesItem>) {
|
||||
presenter.markChapterRead(chapters, true)
|
||||
if (presenter.preferences.removeAfterMarkedAsRead()) {
|
||||
deleteChapters(chapters)
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteChapters(chaptersToDelete: List<UpdatesItem>) {
|
||||
destroyActionModeIfNeeded()
|
||||
DeletingChaptersDialog().showDialog(router)
|
||||
presenter.deleteChapters(chaptersToDelete)
|
||||
}
|
||||
|
||||
/**
|
||||
* Destory [ActionMode] if it's shown
|
||||
*/
|
||||
fun destroyActionModeIfNeeded() {
|
||||
actionMode?.finish()
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark chapter as unread
|
||||
* @param chapters list of selected [UpdatesItem]
|
||||
*/
|
||||
fun markAsUnread(chapters: List<UpdatesItem>) {
|
||||
presenter.markChapterRead(chapters, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Start downloading chapter
|
||||
* @param chapter selected chapter with manga
|
||||
*/
|
||||
fun downloadChapter(chapter: UpdatesItem) {
|
||||
presenter.downloadChapters(listOf(chapter))
|
||||
}
|
||||
|
||||
/**
|
||||
* Start deleting chapter
|
||||
* @param chapter selected chapter with manga
|
||||
*/
|
||||
fun deleteChapter(chapter: UpdatesItem) {
|
||||
DeletingChaptersDialog().showDialog(router)
|
||||
presenter.deleteChapters(listOf(chapter))
|
||||
}
|
||||
|
||||
override fun onCoverClick(position: Int) {
|
||||
val chapterClicked = adapter?.getItem(position) as? UpdatesItem ?: return
|
||||
openManga(chapterClicked)
|
||||
}
|
||||
|
||||
fun openManga(chapter: UpdatesItem) {
|
||||
router.pushController(MangaController(chapter.manga).withFadeTransaction())
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when chapters are deleted
|
||||
*/
|
||||
fun onChaptersDeleted() {
|
||||
dismissDeletingDialog()
|
||||
adapter?.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when error while deleting
|
||||
* @param error error message
|
||||
*/
|
||||
fun onChaptersDeletedError(error: Throwable) {
|
||||
dismissDeletingDialog()
|
||||
Timber.e(error)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to dismiss deleting dialog
|
||||
*/
|
||||
fun dismissDeletingDialog() {
|
||||
router.popControllerWithTag(DeletingChaptersDialog.TAG)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = count.toString()
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package eu.kanade.tachiyomi.ui.recent.updates
|
||||
|
||||
import android.view.View
|
||||
import android.widget.PopupMenu
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
import kotlinx.android.synthetic.main.updates_item.chapter_menu
|
||||
import kotlinx.android.synthetic.main.updates_item.chapter_title
|
||||
import kotlinx.android.synthetic.main.updates_item.download_text
|
||||
import kotlinx.android.synthetic.main.updates_item.manga_cover
|
||||
import kotlinx.android.synthetic.main.updates_item.manga_title
|
||||
|
||||
/**
|
||||
* Holder that contains chapter item
|
||||
* Uses R.layout.item_recent_chapters.
|
||||
* UI related actions should be called from here.
|
||||
*
|
||||
* @param view the inflated view for this holder.
|
||||
* @param adapter the adapter handling this holder.
|
||||
* @param listener a listener to react to single tap and long tap events.
|
||||
* @constructor creates a new recent chapter holder.
|
||||
*/
|
||||
class UpdatesHolder(private val view: View, private val adapter: UpdatesAdapter) :
|
||||
BaseFlexibleViewHolder(view, adapter) {
|
||||
|
||||
/**
|
||||
* Color of read chapter
|
||||
*/
|
||||
private var readColor = view.context.getResourceColor(android.R.attr.textColorHint)
|
||||
|
||||
/**
|
||||
* Color of unread chapter
|
||||
*/
|
||||
private var unreadColor = view.context.getResourceColor(android.R.attr.textColorPrimary)
|
||||
|
||||
/**
|
||||
* Currently bound item.
|
||||
*/
|
||||
private var item: UpdatesItem? = null
|
||||
|
||||
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 {
|
||||
adapter.coverClickListener.onCoverClick(adapterPosition)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set values of view
|
||||
*
|
||||
* @param item item containing chapter information
|
||||
*/
|
||||
fun bind(item: UpdatesItem) {
|
||||
this.item = item
|
||||
|
||||
// Set chapter title
|
||||
chapter_title.text = item.chapter.name
|
||||
|
||||
// Set manga title
|
||||
manga_title.text = item.manga.title
|
||||
|
||||
// Set cover
|
||||
GlideApp.with(itemView.context).clear(manga_cover)
|
||||
if (!item.manga.thumbnail_url.isNullOrEmpty()) {
|
||||
GlideApp.with(itemView.context)
|
||||
.load(item.manga)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.circleCrop()
|
||||
.into(manga_cover)
|
||||
}
|
||||
|
||||
// Check if chapter is read and set correct color
|
||||
if (item.chapter.read) {
|
||||
chapter_title.setTextColor(readColor)
|
||||
manga_title.setTextColor(readColor)
|
||||
} else {
|
||||
chapter_title.setTextColor(unreadColor)
|
||||
manga_title.setTextColor(unreadColor)
|
||||
}
|
||||
|
||||
// Set chapter status
|
||||
notifyStatus(item.status)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates chapter status in view.
|
||||
*
|
||||
* @param status download status
|
||||
*/
|
||||
fun notifyStatus(status: Int) = with(download_text) {
|
||||
when (status) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package eu.kanade.tachiyomi.ui.recent.updates
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractSectionableItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
|
||||
class UpdatesItem(val chapter: Chapter, val manga: Manga, header: DateItem) :
|
||||
AbstractSectionableItem<UpdatesHolder, DateItem>(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 {
|
||||
return R.layout.updates_item
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): UpdatesHolder {
|
||||
return UpdatesHolder(view, adapter as UpdatesAdapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: UpdatesHolder,
|
||||
position: Int,
|
||||
payloads: List<Any?>?
|
||||
) {
|
||||
|
||||
holder.bind(this)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other is UpdatesItem) {
|
||||
return chapter.id!! == other.chapter.id!!
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return chapter.id!!.hashCode()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package eu.kanade.tachiyomi.ui.recent.updates
|
||||
|
||||
import android.os.Bundle
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaChapter
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.TreeMap
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class UpdatesPresenter(
|
||||
val preferences: PreferencesHelper = Injekt.get(),
|
||||
private val db: DatabaseHelper = Injekt.get(),
|
||||
private val downloadManager: DownloadManager = Injekt.get(),
|
||||
private val sourceManager: SourceManager = Injekt.get()
|
||||
) : BasePresenter<UpdatesController>() {
|
||||
|
||||
/**
|
||||
* List containing chapter and manga information
|
||||
*/
|
||||
private var chapters: List<UpdatesItem> = emptyList()
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
getUpdatesObservable()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeLatestCache(UpdatesController::onNextRecentChapters)
|
||||
|
||||
getChapterStatusObservable()
|
||||
.subscribeLatestCache(UpdatesController::onChapterStatusChange) { _, error ->
|
||||
Timber.e(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get observable containing recent chapters and date
|
||||
*
|
||||
* @return observable containing recent chapters and date
|
||||
*/
|
||||
private fun getUpdatesObservable(): Observable<List<UpdatesItem>> {
|
||||
// Set date limit for recent chapters
|
||||
val cal = Calendar.getInstance().apply {
|
||||
time = Date()
|
||||
add(Calendar.MONTH, -1)
|
||||
}
|
||||
|
||||
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 byDay = mangaChapters
|
||||
.groupByTo(map, { getMapKey(it.chapter.date_fetch) })
|
||||
byDay.flatMap {
|
||||
val dateItem = DateItem(it.key)
|
||||
it.value
|
||||
.sortedWith(compareBy({ it.chapter.date_fetch }, { it.chapter.chapter_number })).asReversed()
|
||||
.map { UpdatesItem(it.chapter, it.manga, dateItem) }
|
||||
}
|
||||
}
|
||||
.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
|
||||
// the chapter is downloaded and assign it to the status.
|
||||
if (download != null) {
|
||||
item.download = download
|
||||
}
|
||||
}
|
||||
setDownloadedChapters(it)
|
||||
chapters = it
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get date as time key
|
||||
*
|
||||
* @param date desired date
|
||||
* @return date as time key
|
||||
*/
|
||||
private fun getMapKey(date: Long): Date {
|
||||
val cal = Calendar.getInstance()
|
||||
cal.time = Date(date)
|
||||
cal[Calendar.HOUR_OF_DAY] = 0
|
||||
cal[Calendar.MINUTE] = 0
|
||||
cal[Calendar.SECOND] = 0
|
||||
cal[Calendar.MILLISECOND] = 0
|
||||
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.
|
||||
*
|
||||
* @param items the list of chapter from the database.
|
||||
*/
|
||||
private fun setDownloadedChapters(items: List<UpdatesItem>) {
|
||||
for (item in items) {
|
||||
val manga = item.manga
|
||||
val chapter = item.chapter
|
||||
|
||||
if (downloadManager.isChapterDownloaded(chapter, manga)) {
|
||||
item.status = Download.DOWNLOADED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark selected chapter as read
|
||||
*
|
||||
* @param items list of selected chapters
|
||||
* @param read read status
|
||||
*/
|
||||
fun markChapterRead(items: List<UpdatesItem>, read: Boolean) {
|
||||
val chapters = items.map { it.chapter }
|
||||
chapters.forEach {
|
||||
it.read = read
|
||||
if (!read) {
|
||||
it.last_page_read = 0
|
||||
}
|
||||
}
|
||||
|
||||
Observable.fromCallable { db.updateChaptersProgress(chapters).executeAsBlocking() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete selected chapters
|
||||
*
|
||||
* @param chapters list of chapters
|
||||
*/
|
||||
fun deleteChapters(chapters: List<UpdatesItem>) {
|
||||
Observable.just(chapters)
|
||||
.doOnNext { deleteChaptersInternal(it) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst({ view, _ ->
|
||||
view.onChaptersDeleted()
|
||||
}, UpdatesController::onChaptersDeletedError)
|
||||
}
|
||||
|
||||
/**
|
||||
* Download selected chapters
|
||||
* @param items list of recent chapters seleted.
|
||||
*/
|
||||
fun downloadChapters(items: List<UpdatesItem>) {
|
||||
items.forEach { downloadManager.downloadChapters(it.manga, listOf(it.chapter)) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete selected chapters
|
||||
*
|
||||
* @param items chapters selected
|
||||
*/
|
||||
private fun deleteChaptersInternal(chapterItems: List<UpdatesItem>) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user