ReImplement history features

This commit is contained in:
Jobobby04 2020-05-07 14:50:24 -04:00
parent 35c4d0bf4e
commit 509d7b20b4
6 changed files with 200 additions and 15 deletions

View File

@ -22,11 +22,28 @@ interface HistoryQueries : DbProvider {
* Returns history of recent manga containing last read chapter * Returns history of recent manga containing last read chapter
* @param date recent date range * @param date recent date range
*/ */
fun getRecentManga(date: Date) = db.get() fun getRecentManga(date: Date, offset: Int = 0, search: String = "") = db.get()
.listOfObjects(MangaChapterHistory::class.java) .listOfObjects(MangaChapterHistory::class.java)
.withQuery( .withQuery(
RawQuery.builder() RawQuery.builder()
.query(getRecentMangasQuery()) .query(getRecentMangasQuery(offset, search))
.args(date.time)
.observesTables(HistoryTable.TABLE)
.build()
)
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
.prepare()
/**
* Returns history of recent manga containing last read chapter in 25s
* @param date recent date range
* @offset offset the db by
*/
fun getRecentMangaLimit(date: Date, limit: Int = 0, search: String = "") = db.get()
.listOfObjects(MangaChapterHistory::class.java)
.withQuery(
RawQuery.builder()
.query(getRecentMangasLimitQuery(limit, search))
.args(date.time) .args(date.time)
.observesTables(HistoryTable.TABLE) .observesTables(HistoryTable.TABLE)
.build() .build()

View File

@ -76,7 +76,7 @@ fun getRecentsQuery() =
* and are read after the given time period * and are read after the given time period
* @return return limit is 25 * @return return limit is 25
*/ */
fun getRecentMangasQuery() = fun getRecentMangasQuery(offset: Int = 0, search: String = "") =
""" """
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, ${Manga.TABLE}.*, ${Chapter.TABLE}.*, ${History.TABLE}.* SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, ${Manga.TABLE}.*, ${Chapter.TABLE}.*, ${History.TABLE}.*
FROM ${Manga.TABLE} FROM ${Manga.TABLE}
@ -91,8 +91,36 @@ fun getRecentMangasQuery() =
GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}) AS max_last_read GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}) AS max_last_read
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = max_last_read.${Chapter.COL_MANGA_ID} ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = max_last_read.${Chapter.COL_MANGA_ID}
WHERE ${History.TABLE}.${History.COL_LAST_READ} > ? AND max_last_read.${History.COL_CHAPTER_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID} WHERE ${History.TABLE}.${History.COL_LAST_READ} > ? AND max_last_read.${History.COL_CHAPTER_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
AND lower(${Manga.TABLE}.${Manga.COL_TITLE}) LIKE '%$search%'
ORDER BY max_last_read.${History.COL_LAST_READ} DESC ORDER BY max_last_read.${History.COL_LAST_READ} DESC
LIMIT 25 LIMIT 25 OFFSET $offset
"""
/**
* Query to get the recently read chapters of manga from the library up to a date.
* The max_last_read table contains the most recent chapters grouped by manga
* The select statement returns all information of chapters that have the same id as the chapter in max_last_read
* and are read after the given time period
*/
fun getRecentMangasLimitQuery(limit: Int = 25, search: String = "") =
"""
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, ${Manga.TABLE}.*, ${Chapter.TABLE}.*, ${History.TABLE}.*
FROM ${Manga.TABLE}
JOIN ${Chapter.TABLE}
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
JOIN ${History.TABLE}
ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
JOIN (
SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID},${Chapter.TABLE}.${Chapter.COL_ID} as ${History.COL_CHAPTER_ID}, MAX(${History.TABLE}.${History.COL_LAST_READ}) as ${History.COL_LAST_READ}
FROM ${Chapter.TABLE} JOIN ${History.TABLE}
ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}) AS max_last_read
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = max_last_read.${Chapter.COL_MANGA_ID}
WHERE ${History.TABLE}.${History.COL_LAST_READ} > ?
AND max_last_read.${History.COL_CHAPTER_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
AND lower(${Manga.TABLE}.${Manga.COL_TITLE}) LIKE '%$search%'
ORDER BY max_last_read.${History.COL_LAST_READ} DESC
LIMIT $limit
""" """
fun getHistoryByMangaId() = fun getHistoryByMangaId() =

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.recent.history package eu.kanade.tachiyomi.ui.recent.history
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import java.text.DecimalFormat import java.text.DecimalFormat
import java.text.DecimalFormatSymbols import java.text.DecimalFormatSymbols
@ -15,7 +16,7 @@ import uy.kohesive.injekt.injectLazy
* @constructor creates an instance of the adapter. * @constructor creates an instance of the adapter.
*/ */
class HistoryAdapter(controller: HistoryController) : class HistoryAdapter(controller: HistoryController) :
FlexibleAdapter<HistoryItem>(null, controller, true) { FlexibleAdapter<IFlexible<*>>(null, controller, true) {
val sourceManager by injectLazy<SourceManager>() val sourceManager by injectLazy<SourceManager>()

View File

@ -1,11 +1,16 @@
package eu.kanade.tachiyomi.ui.recent.history package eu.kanade.tachiyomi.ui.recent.history
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.widget.SearchView
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
import eu.kanade.tachiyomi.data.database.models.History import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.databinding.HistoryControllerBinding import eu.kanade.tachiyomi.databinding.HistoryControllerBinding
@ -13,9 +18,14 @@ import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.RootController import eu.kanade.tachiyomi.ui.base.controller.RootController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.source.browse.ProgressItem
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.appcompat.queryTextChanges
/** /**
* Fragment that shows recently read manga. * Fragment that shows recently read manga.
@ -27,6 +37,7 @@ class HistoryController :
RootController, RootController,
NoToolbarElevationController, NoToolbarElevationController,
FlexibleAdapter.OnUpdateListener, FlexibleAdapter.OnUpdateListener,
FlexibleAdapter.EndlessScrollListener,
HistoryAdapter.OnRemoveClickListener, HistoryAdapter.OnRemoveClickListener,
HistoryAdapter.OnResumeClickListener, HistoryAdapter.OnResumeClickListener,
HistoryAdapter.OnItemClickListener, HistoryAdapter.OnItemClickListener,
@ -38,6 +49,12 @@ class HistoryController :
var adapter: HistoryAdapter? = null var adapter: HistoryAdapter? = null
private set private set
/**
* Endless loading item.
*/
private var progressItem: ProgressItem? = null
private var query = ""
override fun getTitle(): String? { override fun getTitle(): String? {
return resources?.getString(R.string.label_recent_manga) return resources?.getString(R.string.label_recent_manga)
} }
@ -76,8 +93,20 @@ class HistoryController :
* *
* @param mangaHistory list of manga history * @param mangaHistory list of manga history
*/ */
fun onNextManga(mangaHistory: List<HistoryItem>) { fun onNextManga(mangaHistory: List<HistoryItem>, cleanBatch: Boolean = false) {
adapter?.updateDataSet(mangaHistory) if (adapter?.itemCount ?: 0 == 0 || cleanBatch) {
resetProgressItem()
}
if (cleanBatch) {
adapter?.updateDataSet(mangaHistory)
} else {
adapter?.onLoadMoreComplete(mangaHistory)
}
}
fun onAddPageError(error: Throwable) {
adapter?.onLoadMoreComplete(null)
adapter?.endlessTargetCount = 1
} }
override fun onUpdateEmptyView(size: Int) { override fun onUpdateEmptyView(size: Int) {
@ -88,9 +117,30 @@ class HistoryController :
} }
} }
/**
* Sets a new progress item and reenables the scroll listener.
*/
private fun resetProgressItem() {
progressItem = ProgressItem()
adapter?.endlessTargetCount = 0
adapter?.setEndlessScrollListener(this, progressItem!!)
}
override fun onLoadMore(lastPosition: Int, currentPage: Int) {
val view = view ?: return
if (BackupRestoreService.isRunning(view.context.applicationContext)) {
onAddPageError(Throwable())
return
}
val adapter = adapter ?: return
presenter.requestNext(adapter.itemCount, query)
}
override fun noMoreLoad(newItemsSize: Int) {}
override fun onResumeClick(position: Int) { override fun onResumeClick(position: Int) {
val activity = activity ?: return val activity = activity ?: return
val (manga, chapter, _) = adapter?.getItem(position)?.mch ?: return val (manga, chapter, _) = (adapter?.getItem(position) as? HistoryItem)?.mch ?: return
val nextChapter = presenter.getNextChapter(chapter, manga) val nextChapter = presenter.getNextChapter(chapter, manga)
if (nextChapter != null) { if (nextChapter != null) {
@ -102,12 +152,12 @@ class HistoryController :
} }
override fun onRemoveClick(position: Int) { override fun onRemoveClick(position: Int) {
val (manga, _, history) = adapter?.getItem(position)?.mch ?: return val (manga, _, history) = (adapter?.getItem(position) as? HistoryItem)?.mch ?: return
RemoveHistoryDialog(this, manga, history).showDialog(router) RemoveHistoryDialog(this, manga, history).showDialog(router)
} }
override fun onItemClick(position: Int) { override fun onItemClick(position: Int) {
val manga = adapter?.getItem(position)?.mch?.manga ?: return val manga = (adapter?.getItem(position) as? HistoryItem)?.mch?.manga ?: return
router.pushController(MangaController(manga).withFadeTransaction()) router.pushController(MangaController(manga).withFadeTransaction())
} }
@ -120,4 +170,35 @@ class HistoryController :
presenter.removeFromHistory(history) presenter.removeFromHistory(history)
} }
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.recently_read, menu)
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
searchView.maxWidth = Int.MAX_VALUE
if (query.isNotEmpty()) {
searchItem.expandActionView()
searchView.setQuery(query, true)
searchView.clearFocus()
}
searchView.queryTextChanges()
.filter { router.backstack.lastOrNull()?.controller() == this }
.onEach {
query = it.toString()
presenter.updateList(query)
}
.launchIn(scope)
// Fixes problem with the overflow icon showing up in lieu of search
searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
return true
}
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
activity?.invalidateOptionsMenu()
return true
}
})
}
} }

View File

@ -28,27 +28,63 @@ class HistoryPresenter : BasePresenter<HistoryController>() {
* Used to connect to database * Used to connect to database
*/ */
val db: DatabaseHelper by injectLazy() val db: DatabaseHelper by injectLazy()
var lastCount = 25
var lastSearch = ""
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
// Used to get a list of recently read manga // Used to get a list of recently read manga
getRecentMangaObservable() updateList()
.subscribeLatestCache(HistoryController::onNextManga) }
fun requestNext(offset: Int, search: String = "") {
lastCount = offset
lastSearch = search
getRecentMangaObservable((offset), search)
.subscribeLatestCache(
{ view, mangas ->
view.onNextManga(mangas)
},
HistoryController::onAddPageError
)
} }
/** /**
* Get recent manga observable * Get recent manga observable
* @return list of history * @return list of history
*/ */
fun getRecentMangaObservable(): Observable<List<HistoryItem>> { fun getRecentMangaObservable(offset: Int = 0, search: String = ""): Observable<List<HistoryItem>> {
// Set date limit for recent manga // Set date limit for recent manga
val cal = Calendar.getInstance().apply { val cal = Calendar.getInstance().apply {
time = Date() time = Date()
add(Calendar.MONTH, -3) add(Calendar.YEAR, -50)
} }
return db.getRecentManga(cal.time).asRxObservable() return db.getRecentManga(cal.time, offset, search).asRxObservable()
.map { recents ->
val map = TreeMap<Date, MutableList<MangaChapterHistory>> { d1, d2 -> d2.compareTo(d1) }
val byDay = recents
.groupByTo(map, { it.history.last_read.toDateKey() })
byDay.flatMap { entry ->
val dateItem = DateSectionItem(entry.key)
entry.value.map { HistoryItem(it, dateItem) }
}
}
.observeOn(AndroidSchedulers.mainThread())
}
/**
* Get recent manga observable
* @return list of history
*/
private fun getRecentMangaLimitObservable(offset: Int = 0, search: String = ""): Observable<List<HistoryItem>> {
// Set limit for recent manga
val cal = Calendar.getInstance()
cal.time = Date()
cal.add(Calendar.YEAR, -50)
return db.getRecentMangaLimit(cal.time, lastCount, search).asRxObservable()
.map { recents -> .map { recents ->
val map = TreeMap<Date, MutableList<MangaChapterHistory>> { d1, d2 -> d2.compareTo(d1) } val map = TreeMap<Date, MutableList<MangaChapterHistory>> { d1, d2 -> d2.compareTo(d1) }
val byDay = recents val byDay = recents
@ -71,6 +107,17 @@ class HistoryPresenter : BasePresenter<HistoryController>() {
.subscribe() .subscribe()
} }
fun updateList(search: String? = null) {
lastSearch = search ?: lastSearch
getRecentMangaLimitObservable(lastCount, lastSearch).take(1)
.subscribeLatestCache(
{ view, mangas ->
view.onNextManga(mangas, true)
},
HistoryController::onAddPageError
)
}
/** /**
* Removes all chapters belonging to manga from history. * Removes all chapters belonging to manga from history.
* @param mangaId id of manga * @param mangaId id of manga

View File

@ -0,0 +1,11 @@
<?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_search"
android:icon="@drawable/ic_search_24dp"
android:title="@string/action_search"
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:iconTint="?attr/colorOnPrimary"
app:showAsAction="ifRoom|collapseActionView" />
</menu>