diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/HistoryQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/HistoryQueries.kt index fa4072dd9..30ee1f798 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/HistoryQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/HistoryQueries.kt @@ -22,11 +22,28 @@ interface HistoryQueries : DbProvider { * Returns history of recent manga containing last read chapter * @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) .withQuery( 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) .observesTables(HistoryTable.TABLE) .build() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt index 7a398d753..a169b3bfb 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt @@ -76,7 +76,7 @@ fun getRecentsQuery() = * and are read after the given time period * @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}.* FROM ${Manga.TABLE} @@ -91,8 +91,36 @@ fun getRecentMangasQuery() = 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 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() = diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryAdapter.kt index 98f2f4245..e1d761025 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryAdapter.kt @@ -1,6 +1,7 @@ package eu.kanade.tachiyomi.ui.recent.history import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.IFlexible import eu.kanade.tachiyomi.source.SourceManager import java.text.DecimalFormat import java.text.DecimalFormatSymbols @@ -15,7 +16,7 @@ import uy.kohesive.injekt.injectLazy * @constructor creates an instance of the adapter. */ class HistoryAdapter(controller: HistoryController) : - FlexibleAdapter(null, controller, true) { + FlexibleAdapter>(null, controller, true) { val sourceManager by injectLazy() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryController.kt index fa7d248b2..63191fe1f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryController.kt @@ -1,11 +1,16 @@ package eu.kanade.tachiyomi.ui.recent.history import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem import android.view.View import android.view.ViewGroup +import androidx.appcompat.widget.SearchView import androidx.recyclerview.widget.LinearLayoutManager import eu.davidea.flexibleadapter.FlexibleAdapter 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.Manga 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.RootController 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.reader.ReaderActivity 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. @@ -27,6 +37,7 @@ class HistoryController : RootController, NoToolbarElevationController, FlexibleAdapter.OnUpdateListener, + FlexibleAdapter.EndlessScrollListener, HistoryAdapter.OnRemoveClickListener, HistoryAdapter.OnResumeClickListener, HistoryAdapter.OnItemClickListener, @@ -38,6 +49,12 @@ class HistoryController : var adapter: HistoryAdapter? = null private set + /** + * Endless loading item. + */ + private var progressItem: ProgressItem? = null + private var query = "" + override fun getTitle(): String? { return resources?.getString(R.string.label_recent_manga) } @@ -76,8 +93,20 @@ class HistoryController : * * @param mangaHistory list of manga history */ - fun onNextManga(mangaHistory: List) { - adapter?.updateDataSet(mangaHistory) + fun onNextManga(mangaHistory: List, cleanBatch: Boolean = false) { + 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) { @@ -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) { 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) if (nextChapter != null) { @@ -102,12 +152,12 @@ class HistoryController : } 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) } 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()) } @@ -120,4 +170,35 @@ class HistoryController : 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 + } + }) + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryPresenter.kt index 911b6fabd..3aa4a4a70 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryPresenter.kt @@ -28,27 +28,63 @@ class HistoryPresenter : BasePresenter() { * Used to connect to database */ val db: DatabaseHelper by injectLazy() + var lastCount = 25 + var lastSearch = "" override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) // Used to get a list of recently read manga - getRecentMangaObservable() - .subscribeLatestCache(HistoryController::onNextManga) + updateList() + } + + 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 * @return list of history */ - fun getRecentMangaObservable(): Observable> { + fun getRecentMangaObservable(offset: Int = 0, search: String = ""): Observable> { // Set date limit for recent manga val cal = Calendar.getInstance().apply { 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> { 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> { + // 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 -> val map = TreeMap> { d1, d2 -> d2.compareTo(d1) } val byDay = recents @@ -71,6 +107,17 @@ class HistoryPresenter : BasePresenter() { .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. * @param mangaId id of manga diff --git a/app/src/main/res/menu/recently_read.xml b/app/src/main/res/menu/recently_read.xml new file mode 100644 index 000000000..b84ac7836 --- /dev/null +++ b/app/src/main/res/menu/recently_read.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file