Add infinite history and search history (#3827)

* Add infinite history and search history

* Cleanup code
This commit is contained in:
jobobby04 2020-09-27 18:17:14 -04:00 committed by GitHub
parent fb3756420b
commit 9d2adcd512
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 137 additions and 19 deletions

View File

@ -21,12 +21,15 @@ 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
* @param limit the limit of manga to grab
* @param offset offset the db by
* @param search what to search in the db history
*/ */
fun getRecentManga(date: Date) = db.get() fun getRecentManga(date: Date, limit: Int = 25, 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(limit, offset, search))
.args(date.time) .args(date.time)
.observesTables(HistoryTable.TABLE) .observesTables(HistoryTable.TABLE)
.build() .build()

View File

@ -49,9 +49,8 @@ fun getRecentsQuery() =
* The max_last_read table contains the most recent chapters grouped by manga * 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 * 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 * and are read after the given time period
* @return return limit is 25
*/ */
fun getRecentMangasQuery() = fun getRecentMangasQuery(limit: Int = 25, 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}
@ -65,9 +64,11 @@ fun getRecentMangasQuery() =
ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID} ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
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 $limit OFFSET $offset
""" """
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 uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.text.DecimalFormat import java.text.DecimalFormat
@ -15,7 +16,7 @@ import java.text.DecimalFormatSymbols
* @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,15 @@
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.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 +17,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 +36,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 +48,16 @@ class HistoryController :
var adapter: HistoryAdapter? = null var adapter: HistoryAdapter? = null
private set private set
/**
* Endless loading item.
*/
private var progressItem: ProgressItem? = null
/**
* Search query.
*/
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)
} }
@ -77,8 +97,23 @@ 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)
}
}
/**
* Safely error if next page load fails
*/
fun onAddPageError(error: Throwable) {
adapter?.onLoadMoreComplete(null)
adapter?.endlessTargetCount = 1
} }
override fun onUpdateEmptyView(size: Int) { override fun onUpdateEmptyView(size: Int) {
@ -89,9 +124,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) {
@ -103,12 +159,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())
} }
@ -121,4 +177,28 @@ class HistoryController :
presenter.removeFromHistory(history) presenter.removeFromHistory(history)
} }
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.history, 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.fixExpand(
onExpand = { invalidateMenuOnExpand() }
)
}
} }

View File

@ -13,7 +13,6 @@ import rx.Observable
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.util.Calendar import java.util.Calendar
import java.util.Comparator
import java.util.Date import java.util.Date
import java.util.TreeMap import java.util.TreeMap
@ -33,22 +32,31 @@ class HistoryPresenter : BasePresenter<HistoryController>() {
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 = "") {
getRecentMangaObservable(offset = offset, search = 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>> { private fun getRecentMangaObservable(limit: Int = 25, 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, limit, offset, 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 +79,20 @@ class HistoryPresenter : BasePresenter<HistoryController>() {
.subscribe() .subscribe()
} }
/**
* Pull a list of history from the db
* @param search a search query to use for filtering
*/
fun updateList(search: String = "") {
getRecentMangaObservable(search = search).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
@ -103,7 +125,7 @@ class HistoryPresenter : BasePresenter<HistoryController>() {
} }
val chapters = db.getChapters(manga).executeAsBlocking() val chapters = db.getChapters(manga).executeAsBlocking()
.sortedWith(Comparator { c1, c2 -> sortFunction(c1, c2) }) .sortedWith { c1, c2 -> sortFunction(c1, c2) }
val currChapterIndex = chapters.indexOfFirst { chapter.id == it.id } val currChapterIndex = chapters.indexOfFirst { chapter.id == it.id }
return when (manga.sorting) { return when (manga.sorting) {

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>