diff --git a/app/src/main/java/eu/kanade/data/manga/MangaMapper.kt b/app/src/main/java/eu/kanade/data/manga/MangaMapper.kt index 96ede4e5ba..153c47a8f0 100644 --- a/app/src/main/java/eu/kanade/data/manga/MangaMapper.kt +++ b/app/src/main/java/eu/kanade/data/manga/MangaMapper.kt @@ -1,5 +1,6 @@ package eu.kanade.data.manga +import eu.kanade.domain.chapter.model.Chapter import eu.kanade.domain.manga.model.Manga val mangaMapper: (Long, Long, String, String?, String?, String?, List?, String, Long, String?, Boolean, Long?, Long?, Boolean, Long, Long, Long, Long) -> Manga = @@ -24,3 +25,39 @@ val mangaMapper: (Long, Long, String, String?, String?, String?, List?, initialized = initialized, ) } + +val mangaChapterMapper: (Long, Long, String, String?, String?, String?, List?, String, Long, String?, Boolean, Long?, Long?, Boolean, Long, Long, Long, Long, Long, Long, String, String, String?, Boolean, Boolean, Long, Float, Long, Long, Long) -> Pair = + { _id, source, url, artist, author, description, genre, title, status, thumbnailUrl, favorite, lastUpdate, next_update, initialized, viewerFlags, chapterFlags, coverLastModified, dateAdded, chapterId, mangaId, chapterUrl, name, scanlator, read, bookmark, lastPageRead, chapterNumber, sourceOrder, dateFetch, dateUpload -> + Manga( + id = _id, + source = source, + favorite = favorite, + lastUpdate = lastUpdate ?: 0, + dateAdded = dateAdded, + viewerFlags = viewerFlags, + chapterFlags = chapterFlags, + coverLastModified = coverLastModified, + url = url, + title = title, + artist = artist, + author = author, + description = description, + genre = genre, + status = status, + thumbnailUrl = thumbnailUrl, + initialized = initialized, + ) to Chapter( + id = chapterId, + mangaId = mangaId, + read = read, + bookmark = bookmark, + lastPageRead = lastPageRead, + dateFetch = dateFetch, + sourceOrder = sourceOrder, + url = chapterUrl, + name = name, + dateUpload = dateUpload, + chapterNumber = chapterNumber, + scanlator = scanlator, + ) + } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt index d49d7a067a..b4c5aa2f73 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt @@ -1,15 +1,11 @@ package eu.kanade.tachiyomi.data.database.queries import com.pushtorefresh.storio.sqlite.queries.Query -import com.pushtorefresh.storio.sqlite.queries.RawQuery import eu.kanade.tachiyomi.data.database.DbProvider import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.MangaChapter import eu.kanade.tachiyomi.data.database.resolvers.ChapterProgressPutResolver -import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver import eu.kanade.tachiyomi.data.database.tables.ChapterTable -import java.util.Date interface ChapterQueries : DbProvider { @@ -24,18 +20,6 @@ interface ChapterQueries : DbProvider { ) .prepare() - fun getRecentChapters(date: Date) = db.get() - .listOfObjects(MangaChapter::class.java) - .withQuery( - RawQuery.builder() - .query(getRecentsQuery()) - .args(date.time) - .observesTables(ChapterTable.TABLE) - .build(), - ) - .withGetResolver(MangaChapterGetResolver.INSTANCE) - .prepare() - fun getChapter(id: Long) = db.get() .`object`(Chapter::class.java) .withQuery( 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 9e361fe999..4ae9592416 100644 --- 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 @@ -38,19 +38,6 @@ val libraryQuery = ON MC.${MangaCategory.COL_MANGA_ID} = M.${Manga.COL_ID} """ -/** - * Query to get the recent chapters of manga from the library up to a date. - */ -fun getRecentsQuery() = - """ - 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} - 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 -""" - fun getLastReadMangaQuery() = """ SELECT ${Manga.TABLE}.*, MAX(${History.TABLE}.${History.COL_LAST_READ}) AS max diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/base/BaseChapterItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/base/BaseChapterItem.kt index 2be19f9c27..53917f4e6c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/base/BaseChapterItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/base/BaseChapterItem.kt @@ -2,16 +2,14 @@ package eu.kanade.tachiyomi.ui.manga.chapter.base import eu.davidea.flexibleadapter.items.AbstractHeaderItem import eu.davidea.flexibleadapter.items.AbstractSectionableItem -import eu.kanade.tachiyomi.data.database.models.Chapter +import eu.kanade.domain.chapter.model.Chapter import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.source.model.Page abstract class BaseChapterItem>( val chapter: Chapter, header: H? = null, -) : - AbstractSectionableItem(header), - Chapter by chapter { +) : AbstractSectionableItem(header) { private var _status: Download.State = Download.State.NOT_DOWNLOADED @@ -36,12 +34,14 @@ abstract class BaseChapterItem> override fun equals(other: Any?): Boolean { if (this === other) return true if (other is BaseChapterItem<*, *>) { - return chapter.id!! == other.chapter.id!! + return chapter.id == other.chapter.id && chapter.read == other.chapter.read } return false } override fun hashCode(): Int { - return chapter.id!!.hashCode() + var result = chapter.id.hashCode() + result = 31 * result + chapter.read.hashCode() + return result } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesController.kt index 33e1433003..a76a192a03 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesController.kt @@ -11,7 +11,6 @@ import androidx.recyclerview.widget.LinearLayoutManager import dev.chrisbanes.insetter.applyInsetter 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.DownloadService import eu.kanade.tachiyomi.data.download.model.Download @@ -30,8 +29,10 @@ import eu.kanade.tachiyomi.util.system.notificationManager import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.view.onAnimationsFinished import eu.kanade.tachiyomi.widget.ActionModeWithToolbar +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import logcat.LogPriority import reactivecircus.flowbinding.recyclerview.scrollStateChanges import reactivecircus.flowbinding.swiperefreshlayout.refreshes @@ -107,6 +108,24 @@ class UpdatesController : binding.swipeRefresh.isRefreshing = false } .launchIn(viewScope) + + viewScope.launch { + presenter.updates.collectLatest { updatesItems -> + destroyActionModeIfNeeded() + if (adapter == null) { + adapter = UpdatesAdapter(this@UpdatesController, binding.recycler.context, updatesItems) + binding.recycler.adapter = adapter + adapter!!.fastScroller = binding.fastScroller + } else { + adapter?.updateDataSet(updatesItems) + } + binding.swipeRefresh.isRefreshing = false + binding.fastScroller.isVisible = true + binding.recycler.onAnimationsFinished { + (activity as? MainActivity)?.ready = true + } + } + } } override fun onDestroyView(view: View) { @@ -191,7 +210,7 @@ class UpdatesController : */ private fun openChapter(item: UpdatesItem) { val activity = activity ?: return - val intent = ReaderActivity.newIntent(activity, item.manga, item.chapter) + val intent = ReaderActivity.newIntent(activity, item.manga.id, item.chapter.id) startActivity(intent) } @@ -204,26 +223,6 @@ class UpdatesController : destroyActionModeIfNeeded() } - /** - * Populate adapter with chapters - * @param chapters list of [Any] - */ - fun onNextRecentChapters(chapters: List>) { - destroyActionModeIfNeeded() - if (adapter == null) { - adapter = UpdatesAdapter(this@UpdatesController, binding.recycler.context, chapters) - binding.recycler.adapter = adapter - adapter!!.fastScroller = binding.fastScroller - } else { - adapter?.updateDataSet(chapters) - } - binding.swipeRefresh.isRefreshing = false - binding.fastScroller.isVisible = true - binding.recycler.onAnimationsFinished { - (activity as? MainActivity)?.ready = true - } - } - override fun onUpdateEmptyView(size: Int) { if (size > 0) { binding.emptyView.hide() @@ -317,8 +316,8 @@ class UpdatesController : } override fun startDownloadNow(position: Int) { - val chapter = adapter?.getItem(position) as? UpdatesItem ?: return - presenter.startDownloadingNow(chapter) + val item = adapter?.getItem(position) as? UpdatesItem ?: return + presenter.startDownloadingNow(item.chapter) } private fun bookmarkChapters(chapters: List, bookmarked: Boolean) { @@ -357,8 +356,8 @@ class UpdatesController : if (chapters.isEmpty()) return toolbar.findToolbarItem(R.id.action_download)?.isVisible = chapters.any { !it.isDownloaded } toolbar.findToolbarItem(R.id.action_delete)?.isVisible = chapters.any { it.isDownloaded } - toolbar.findToolbarItem(R.id.action_bookmark)?.isVisible = chapters.any { !it.bookmark } - toolbar.findToolbarItem(R.id.action_remove_bookmark)?.isVisible = chapters.all { it.bookmark } + toolbar.findToolbarItem(R.id.action_bookmark)?.isVisible = chapters.any { !it.chapter.bookmark } + toolbar.findToolbarItem(R.id.action_remove_bookmark)?.isVisible = chapters.all { it.chapter.bookmark } toolbar.findToolbarItem(R.id.action_mark_as_read)?.isVisible = chapters.any { !it.chapter.read } toolbar.findToolbarItem(R.id.action_mark_as_unread)?.isVisible = chapters.all { it.chapter.read } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesHolder.kt index 1efa5b81fa..a6af17986a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesHolder.kt @@ -44,12 +44,12 @@ class UpdatesHolder(private val view: View, private val adapter: UpdatesAdapter) } else { binding.mangaTitle.setTextColor(adapter.unreadColor) binding.chapterTitle.setTextColor( - if (item.bookmark) adapter.bookmarkedColor else adapter.unreadColorSecondary, + if (item.chapter.bookmark) adapter.bookmarkedColor else adapter.unreadColorSecondary, ) } // Set bookmark status - binding.bookmarkIcon.isVisible = item.bookmark + binding.bookmarkIcon.isVisible = item.chapter.bookmark // Set chapter status binding.download.isVisible = item.manga.source != LocalSource.ID diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesItem.kt index d5d9006166..de3d77952e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesItem.kt @@ -4,9 +4,9 @@ import android.view.View import androidx.recyclerview.widget.RecyclerView import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.domain.chapter.model.Chapter +import eu.kanade.domain.manga.model.Manga 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.ui.manga.chapter.base.BaseChapterItem import eu.kanade.tachiyomi.ui.recent.DateSectionItem diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesPresenter.kt index 0dc601bcd1..3318177812 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesPresenter.kt @@ -1,17 +1,28 @@ 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.Chapter -import eu.kanade.tachiyomi.data.database.models.MangaChapter +import eu.kanade.data.DatabaseHandler +import eu.kanade.data.manga.mangaChapterMapper +import eu.kanade.domain.chapter.interactor.UpdateChapter +import eu.kanade.domain.chapter.model.Chapter +import eu.kanade.domain.chapter.model.ChapterUpdate +import eu.kanade.domain.chapter.model.toDbChapter +import eu.kanade.domain.manga.model.Manga +import eu.kanade.domain.manga.model.toDbManga 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 eu.kanade.tachiyomi.ui.recent.DateSectionItem +import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.toDateKey import eu.kanade.tachiyomi.util.system.logcat +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.map import logcat.LogPriority import rx.Observable import rx.android.schedulers.AndroidSchedulers @@ -25,24 +36,22 @@ import java.util.TreeMap class UpdatesPresenter : BasePresenter() { val preferences: PreferencesHelper by injectLazy() - private val db: DatabaseHelper by injectLazy() private val downloadManager: DownloadManager by injectLazy() private val sourceManager: SourceManager by injectLazy() + private val handler: DatabaseHandler by injectLazy() + private val updateChapter: UpdateChapter by injectLazy() + private val relativeTime: Int = preferences.relativeTime().get() private val dateFormat: DateFormat = preferences.dateFormat() - /** - * List containing chapter and manga information - */ - private var chapters: List = emptyList() + private val _updates: MutableStateFlow> = MutableStateFlow(listOf()) + val updates: StateFlow> = _updates.asStateFlow() override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) getUpdatesObservable() - .observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache(UpdatesController::onNextRecentChapters) downloadManager.queue.getStatusObservable() .observeOn(Schedulers.io()) @@ -72,43 +81,47 @@ class UpdatesPresenter : BasePresenter() { * * @return observable containing recent chapters and date */ - private fun getUpdatesObservable(): Observable> { + private fun getUpdatesObservable() { // Set date limit for recent chapters - val cal = Calendar.getInstance().apply { - time = Date() - add(Calendar.MONTH, -3) - } - - return db.getRecentChapters(cal.time).asRxObservable() - // Convert to a list of recent chapters. - .map { mangaChapters -> - val map = TreeMap> { d1, d2 -> d2.compareTo(d1) } - val byDay = mangaChapters - .groupByTo(map) { it.chapter.date_fetch.toDateKey() } - byDay.flatMap { entry -> - val dateItem = DateSectionItem(entry.key, relativeTime, dateFormat) - entry.value - .sortedWith(compareBy({ it.chapter.date_fetch }, { it.chapter.chapter_number })).asReversed() - .map { UpdatesItem(it.chapter, it.manga, dateItem) } - } + presenterScope.launchIO { + val cal = Calendar.getInstance().apply { + time = Date() + add(Calendar.MONTH, -3) } - .doOnNext { list -> - list.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 + handler + .subscribeToList { + mangasQueries.getRecentlyUpdated(after = cal.timeInMillis, mangaChapterMapper) + } + .map { mangaChapter -> + val map = TreeMap>> { d1, d2 -> d2.compareTo(d1) } + val byDate = mangaChapter.groupByTo(map) { it.second.dateFetch.toDateKey() } + byDate.flatMap { entry -> + val dateItem = DateSectionItem(entry.key, relativeTime, dateFormat) + entry.value + .sortedWith(compareBy({ it.second.dateFetch }, { it.second.chapterNumber })).asReversed() + .map { UpdatesItem(it.second, it.first, dateItem) } } } - setDownloadedChapters(list) - chapters = list + .collectLatest { list -> + list.forEach { item -> + // Find an active download for this chapter. + val download = downloadManager.queue.find { it.chapter.id == item.chapter.id } - // Set unread chapter count for bottom bar badge - preferences.unreadUpdatesCount().set(list.count { !it.read }) - } + // 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(list) + + _updates.value = list + + // Set unread chapter count for bottom bar badge + preferences.unreadUpdatesCount().set(list.count { !it.chapter.read }) + } + } } /** @@ -135,6 +148,7 @@ class UpdatesPresenter : BasePresenter() { private fun onDownloadStatusChange(download: Download) { // Assign the download to the model object. if (download.status == Download.State.QUEUE) { + val chapters = (view?.adapter?.currentItems ?: emptyList()).filterIsInstance() val chapter = chapters.find { it.chapter.id == download.chapter.id } if (chapter != null && chapter.download == null) { chapter.download = download @@ -153,17 +167,16 @@ class UpdatesPresenter : BasePresenter() { * @param read read status */ fun markChapterRead(items: List, read: Boolean) { - val chapters = items.map { it.chapter } - chapters.forEach { - it.read = read - if (!read) { - it.last_page_read = 0 + presenterScope.launchIO { + val toUpdate = items.map { + ChapterUpdate( + read = read, + lastPageRead = if (!read) 0 else null, + id = it.chapter.id, + ) } + updateChapter.awaitAll(toUpdate) } - - Observable.fromCallable { db.updateChaptersProgress(chapters).executeAsBlocking() } - .subscribeOn(Schedulers.io()) - .subscribe() } /** @@ -190,14 +203,15 @@ class UpdatesPresenter : BasePresenter() { * @param bookmarked bookmark status */ fun bookmarkChapters(items: List, bookmarked: Boolean) { - val chapters = items.map { it.chapter } - chapters.forEach { - it.bookmark = bookmarked + presenterScope.launchIO { + val toUpdate = items.map { + ChapterUpdate( + bookmark = bookmarked, + id = it.chapter.id, + ) + } + updateChapter.awaitAll(toUpdate) } - - Observable.fromCallable { db.updateChaptersProgress(chapters).executeAsBlocking() } - .subscribeOn(Schedulers.io()) - .subscribe() } /** @@ -205,7 +219,7 @@ class UpdatesPresenter : BasePresenter() { * @param items list of recent chapters seleted. */ fun downloadChapters(items: List) { - items.forEach { downloadManager.downloadChapters(it.manga, listOf(it.chapter)) } + items.forEach { downloadManager.downloadChapters(it.manga.toDbManga(), listOf(it.chapter.toDbChapter())) } } /** @@ -216,9 +230,9 @@ class UpdatesPresenter : BasePresenter() { private fun deleteChaptersInternal(chapterItems: List) { val itemsByManga = chapterItems.groupBy { it.manga.id } for ((_, items) in itemsByManga) { - val manga = items.first().manga + val manga = items.first().manga.toDbManga() val source = sourceManager.get(manga.source) ?: continue - val chapters = items.map { it.chapter } + val chapters = items.map { it.chapter.toDbChapter() } downloadManager.deleteChapters(chapters, manga, source) items.forEach { diff --git a/app/src/main/sqldelight/data/mangas.sq b/app/src/main/sqldelight/data/mangas.sq index 19318b2054..ad4246f4fe 100644 --- a/app/src/main/sqldelight/data/mangas.sq +++ b/app/src/main/sqldelight/data/mangas.sq @@ -76,6 +76,16 @@ FROM mangas WHERE favorite = 0 GROUP BY source; +getRecentlyUpdated: +SELECT * +FROM mangas M +JOIN chapters C +ON M._id = C.manga_id +WHERE M.favorite = 1 +AND C.date_upload > :after +AND C.date_fetch > M.date_added +ORDER BY C.date_upload DESC; + deleteMangasNotInLibraryBySourceIds: DELETE FROM mangas WHERE favorite = 0 AND source IN :sourceIds;