Reader: Save reading progress with SQLDelight (#7185)

* Use SQLDelight in reader to update history

* Move chapter progress to sqldelight

* Review Changes

Co-Authored-By: inorichi <len@kanade.eu>

* Review Changes 2

Co-authored-by: FourTOne5 <59261191+FourTOne5@users.noreply.github.com>
Co-authored-by: inorichi <len@kanade.eu>
This commit is contained in:
AntsyLich 2022-05-28 19:09:27 +06:00 committed by GitHub
parent 6b14f38cfa
commit 809da49301
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 309 additions and 67 deletions

View File

@ -0,0 +1,3 @@
package eu.kanade.data
fun Boolean.toLong() = if (this) 1L else 0L

View File

@ -0,0 +1,36 @@
package eu.kanade.data.chapter
import eu.kanade.data.DatabaseHandler
import eu.kanade.data.toLong
import eu.kanade.domain.chapter.model.ChapterUpdate
import eu.kanade.domain.chapter.repository.ChapterRepository
import eu.kanade.tachiyomi.util.system.logcat
import logcat.LogPriority
class ChapterRepositoryImpl(
private val databaseHandler: DatabaseHandler,
) : ChapterRepository {
override suspend fun update(chapterUpdate: ChapterUpdate) {
try {
databaseHandler.await {
chaptersQueries.update(
chapterUpdate.mangaId,
chapterUpdate.url,
chapterUpdate.name,
chapterUpdate.scanlator,
chapterUpdate.read?.toLong(),
chapterUpdate.bookmark?.toLong(),
chapterUpdate.lastPageRead,
chapterUpdate.chapterNumber?.toDouble(),
chapterUpdate.sourceOrder,
chapterUpdate.dateFetch,
chapterUpdate.dateUpload,
chapterId = chapterUpdate.id,
)
}
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
}
}
}

View File

@ -4,16 +4,17 @@ import eu.kanade.domain.history.model.History
import eu.kanade.domain.history.model.HistoryWithRelations import eu.kanade.domain.history.model.HistoryWithRelations
import java.util.Date import java.util.Date
val historyMapper: (Long, Long, Date?, Date?) -> History = { id, chapterId, readAt, _ -> val historyMapper: (Long, Long, Date?, Long) -> History = { id, chapterId, readAt, readDuration ->
History( History(
id = id, id = id,
chapterId = chapterId, chapterId = chapterId,
readAt = readAt, readAt = readAt,
readDuration = readDuration,
) )
} }
val historyWithRelationsMapper: (Long, Long, Long, String, String?, Float, Date?) -> HistoryWithRelations = { val historyWithRelationsMapper: (Long, Long, Long, String, String?, Float, Date?, Long) -> HistoryWithRelations = {
historyId, mangaId, chapterId, title, thumbnailUrl, chapterNumber, readAt -> historyId, mangaId, chapterId, title, thumbnailUrl, chapterNumber, readAt, readDuration ->
HistoryWithRelations( HistoryWithRelations(
id = historyId, id = historyId,
chapterId = chapterId, chapterId = chapterId,
@ -22,5 +23,6 @@ val historyWithRelationsMapper: (Long, Long, Long, String, String?, Float, Date?
thumbnailUrl = thumbnailUrl ?: "", thumbnailUrl = thumbnailUrl ?: "",
chapterNumber = chapterNumber, chapterNumber = chapterNumber,
readAt = readAt, readAt = readAt,
readDuration = readDuration,
) )
} }

View File

@ -5,6 +5,7 @@ import eu.kanade.data.DatabaseHandler
import eu.kanade.data.chapter.chapterMapper import eu.kanade.data.chapter.chapterMapper
import eu.kanade.data.manga.mangaMapper import eu.kanade.data.manga.mangaMapper
import eu.kanade.domain.chapter.model.Chapter import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.domain.history.model.HistoryUpdate
import eu.kanade.domain.history.model.HistoryWithRelations import eu.kanade.domain.history.model.HistoryWithRelations
import eu.kanade.domain.history.repository.HistoryRepository import eu.kanade.domain.history.repository.HistoryRepository
import eu.kanade.domain.manga.model.Manga import eu.kanade.domain.manga.model.Manga
@ -89,4 +90,28 @@ class HistoryRepositoryImpl(
false false
} }
} }
override suspend fun upsertHistory(historyUpdate: HistoryUpdate) {
try {
try {
handler.await {
historyQueries.insert(
historyUpdate.chapterId,
historyUpdate.readAt,
historyUpdate.sessionReadDuration,
)
}
} catch (e: Exception) {
handler.await {
historyQueries.update(
historyUpdate.readAt,
historyUpdate.sessionReadDuration,
historyUpdate.chapterId,
)
}
}
} catch (e: Exception) {
logcat(LogPriority.ERROR, throwable = e)
}
}
} }

View File

@ -1,8 +1,11 @@
package eu.kanade.domain package eu.kanade.domain
import eu.kanade.data.chapter.ChapterRepositoryImpl
import eu.kanade.data.history.HistoryRepositoryImpl import eu.kanade.data.history.HistoryRepositoryImpl
import eu.kanade.data.manga.MangaRepositoryImpl import eu.kanade.data.manga.MangaRepositoryImpl
import eu.kanade.data.source.SourceRepositoryImpl import eu.kanade.data.source.SourceRepositoryImpl
import eu.kanade.domain.chapter.interactor.UpdateChapter
import eu.kanade.domain.chapter.repository.ChapterRepository
import eu.kanade.domain.extension.interactor.GetExtensionLanguages import eu.kanade.domain.extension.interactor.GetExtensionLanguages
import eu.kanade.domain.extension.interactor.GetExtensionSources import eu.kanade.domain.extension.interactor.GetExtensionSources
import eu.kanade.domain.extension.interactor.GetExtensionUpdates import eu.kanade.domain.extension.interactor.GetExtensionUpdates
@ -12,6 +15,7 @@ import eu.kanade.domain.history.interactor.GetHistory
import eu.kanade.domain.history.interactor.GetNextChapterForManga import eu.kanade.domain.history.interactor.GetNextChapterForManga
import eu.kanade.domain.history.interactor.RemoveHistoryById import eu.kanade.domain.history.interactor.RemoveHistoryById
import eu.kanade.domain.history.interactor.RemoveHistoryByMangaId import eu.kanade.domain.history.interactor.RemoveHistoryByMangaId
import eu.kanade.domain.history.interactor.UpsertHistory
import eu.kanade.domain.history.repository.HistoryRepository import eu.kanade.domain.history.repository.HistoryRepository
import eu.kanade.domain.manga.interactor.GetFavoritesBySourceId import eu.kanade.domain.manga.interactor.GetFavoritesBySourceId
import eu.kanade.domain.manga.interactor.ResetViewerFlags import eu.kanade.domain.manga.interactor.ResetViewerFlags
@ -38,9 +42,13 @@ class DomainModule : InjektModule {
addFactory { GetNextChapterForManga(get()) } addFactory { GetNextChapterForManga(get()) }
addFactory { ResetViewerFlags(get()) } addFactory { ResetViewerFlags(get()) }
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
addFactory { UpdateChapter(get()) }
addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) } addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) }
addFactory { DeleteHistoryTable(get()) } addFactory { DeleteHistoryTable(get()) }
addFactory { GetHistory(get()) } addFactory { GetHistory(get()) }
addFactory { UpsertHistory(get()) }
addFactory { RemoveHistoryById(get()) } addFactory { RemoveHistoryById(get()) }
addFactory { RemoveHistoryByMangaId(get()) } addFactory { RemoveHistoryByMangaId(get()) }

View File

@ -0,0 +1,13 @@
package eu.kanade.domain.chapter.interactor
import eu.kanade.domain.chapter.model.ChapterUpdate
import eu.kanade.domain.chapter.repository.ChapterRepository
class UpdateChapter(
private val chapterRepository: ChapterRepository,
) {
suspend fun await(chapterUpdate: ChapterUpdate) {
chapterRepository.update(chapterUpdate)
}
}

View File

@ -0,0 +1,16 @@
package eu.kanade.domain.chapter.model
data class ChapterUpdate(
val id: Long,
val mangaId: Long? = null,
val read: Boolean? = null,
val bookmark: Boolean? = null,
val lastPageRead: Long? = null,
val dateFetch: Long? = null,
val sourceOrder: Long? = null,
val url: String? = null,
val name: String? = null,
val dateUpload: Long? = null,
val chapterNumber: Float? = null,
val scanlator: String? = null,
)

View File

@ -0,0 +1,8 @@
package eu.kanade.domain.chapter.repository
import eu.kanade.domain.chapter.model.ChapterUpdate
interface ChapterRepository {
suspend fun update(chapterUpdate: ChapterUpdate)
}

View File

@ -0,0 +1,13 @@
package eu.kanade.domain.history.interactor
import eu.kanade.domain.history.model.HistoryUpdate
import eu.kanade.domain.history.repository.HistoryRepository
class UpsertHistory(
private val historyRepository: HistoryRepository,
) {
suspend fun await(historyUpdate: HistoryUpdate) {
historyRepository.upsertHistory(historyUpdate)
}
}

View File

@ -3,7 +3,8 @@ package eu.kanade.domain.history.model
import java.util.Date import java.util.Date
data class History( data class History(
val id: Long?, val id: Long,
val chapterId: Long, val chapterId: Long,
val readAt: Date?, val readAt: Date?,
val readDuration: Long,
) )

View File

@ -0,0 +1,9 @@
package eu.kanade.domain.history.model
import java.util.Date
data class HistoryUpdate(
val chapterId: Long,
val readAt: Date,
val sessionReadDuration: Long,
)

View File

@ -10,4 +10,5 @@ data class HistoryWithRelations(
val thumbnailUrl: String, val thumbnailUrl: String,
val chapterNumber: Float, val chapterNumber: Float,
val readAt: Date?, val readAt: Date?,
val readDuration: Long,
) )

View File

@ -2,6 +2,7 @@ package eu.kanade.domain.history.repository
import androidx.paging.PagingSource import androidx.paging.PagingSource
import eu.kanade.domain.chapter.model.Chapter import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.domain.history.model.HistoryUpdate
import eu.kanade.domain.history.model.HistoryWithRelations import eu.kanade.domain.history.model.HistoryWithRelations
interface HistoryRepository { interface HistoryRepository {
@ -15,4 +16,6 @@ interface HistoryRepository {
suspend fun resetHistoryByMangaId(mangaId: Long) suspend fun resetHistoryByMangaId(mangaId: Long)
suspend fun deleteAllHistory(): Boolean suspend fun deleteAllHistory(): Boolean
suspend fun upsertHistory(historyUpdate: HistoryUpdate)
} }

View File

@ -55,8 +55,7 @@ class AppModule(val app: Application) : InjektModule {
Database( Database(
driver = get(), driver = get(),
historyAdapter = History.Adapter( historyAdapter = History.Adapter(
history_last_readAdapter = dateAdapter, last_readAdapter = dateAdapter,
history_time_readAdapter = dateAdapter,
), ),
mangasAdapter = Mangas.Adapter( mangasAdapter = Mangas.Adapter(
genreAdapter = listOfStringsAdapter, genreAdapter = listOfStringsAdapter,

View File

@ -23,7 +23,7 @@ interface History : Serializable {
var last_read: Long var last_read: Long
/** /**
* Total time chapter was read - todo not yet implemented * Total time chapter was read
*/ */
var time_read: Long var time_read: Long

View File

@ -21,7 +21,7 @@ class HistoryImpl : History {
override var last_read: Long = 0 override var last_read: Long = 0
/** /**
* Total time chapter was read - todo not yet implemented * Total time chapter was read
*/ */
override var time_read: Long = 0 override var time_read: Long = 0
} }

View File

@ -232,7 +232,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
} }
override fun onPause() { override fun onPause() {
presenter.saveProgress() presenter.saveCurrentChapterReadingProgress()
super.onPause() super.onPause()
} }
@ -242,6 +242,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
*/ */
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
presenter.setReadStartTime()
setMenuVisibility(menuVisible, animate = false) setMenuVisibility(menuVisible, animate = false)
} }

View File

@ -4,9 +4,12 @@ import android.app.Application
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import com.jakewharton.rxrelay.BehaviorRelay import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.domain.chapter.interactor.UpdateChapter
import eu.kanade.domain.chapter.model.ChapterUpdate
import eu.kanade.domain.history.interactor.UpsertHistory
import eu.kanade.domain.history.model.HistoryUpdate
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
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.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@ -62,6 +65,8 @@ class ReaderPresenter(
private val coverCache: CoverCache = Injekt.get(), private val coverCache: CoverCache = Injekt.get(),
private val preferences: PreferencesHelper = Injekt.get(), private val preferences: PreferencesHelper = Injekt.get(),
private val delayedTrackingStore: DelayedTrackingStore = Injekt.get(), private val delayedTrackingStore: DelayedTrackingStore = Injekt.get(),
private val upsertHistory: UpsertHistory = Injekt.get(),
private val updateChapter: UpdateChapter = Injekt.get(),
) : BasePresenter<ReaderActivity>() { ) : BasePresenter<ReaderActivity>() {
/** /**
@ -80,6 +85,11 @@ class ReaderPresenter(
*/ */
private var loader: ChapterLoader? = null private var loader: ChapterLoader? = null
/**
* The time the chapter was started reading
*/
private var chapterReadStartTime: Long? = null
/** /**
* Subscription to prevent setting chapters as active from multiple threads. * Subscription to prevent setting chapters as active from multiple threads.
*/ */
@ -168,8 +178,7 @@ class ReaderPresenter(
val currentChapters = viewerChaptersRelay.value val currentChapters = viewerChaptersRelay.value
if (currentChapters != null) { if (currentChapters != null) {
currentChapters.unref() currentChapters.unref()
saveChapterProgress(currentChapters.currChapter) saveReadingProgress(currentChapters.currChapter)
saveChapterHistory(currentChapters.currChapter)
} }
} }
@ -200,7 +209,9 @@ class ReaderPresenter(
*/ */
fun onSaveInstanceStateNonConfigurationChange() { fun onSaveInstanceStateNonConfigurationChange() {
val currentChapter = getCurrentChapter() ?: return val currentChapter = getCurrentChapter() ?: return
saveChapterProgress(currentChapter) launchIO {
saveChapterProgress(currentChapter)
}
} }
/** /**
@ -397,7 +408,7 @@ class ReaderPresenter(
if (selectedChapter != currentChapters.currChapter) { if (selectedChapter != currentChapters.currChapter) {
logcat { "Setting ${selectedChapter.chapter.url} as active" } logcat { "Setting ${selectedChapter.chapter.url} as active" }
onChapterChanged(currentChapters.currChapter) saveReadingProgress(currentChapters.currChapter)
loadNewChapter(selectedChapter) loadNewChapter(selectedChapter)
} }
} }
@ -429,43 +440,57 @@ class ReaderPresenter(
} }
} }
/** fun saveCurrentChapterReadingProgress() {
* Called when a chapter changed from [fromChapter] to [toChapter]. It updates [fromChapter] getCurrentChapter()?.let { saveReadingProgress(it) }
* on the database.
*/
private fun onChapterChanged(fromChapter: ReaderChapter) {
saveChapterProgress(fromChapter)
saveChapterHistory(fromChapter)
} }
/** /**
* Saves this [chapter] progress (last read page and whether it's read). * Called when reader chapter is changed in reader or when activity is paused.
*/
private fun saveReadingProgress(readerChapter: ReaderChapter) {
launchIO {
saveChapterProgress(readerChapter)
saveChapterHistory(readerChapter)
}
}
/**
* Saves this [readerChapter] progress (last read page and whether it's read).
* If incognito mode isn't on or has at least 1 tracker * If incognito mode isn't on or has at least 1 tracker
*/ */
private fun saveChapterProgress(chapter: ReaderChapter) { private suspend fun saveChapterProgress(readerChapter: ReaderChapter) {
if (!incognitoMode || hasTrackers) { if (!incognitoMode || hasTrackers) {
db.updateChapterProgress(chapter.chapter).asRxCompletable() val chapter = readerChapter.chapter
.onErrorComplete() updateChapter.await(
.subscribeOn(Schedulers.io()) ChapterUpdate(
.subscribe() id = chapter.id!!,
read = chapter.read,
bookmark = chapter.bookmark,
lastPageRead = chapter.last_page_read.toLong(),
),
)
} }
} }
/** /**
* Saves this [chapter] last read history if incognito mode isn't on. * Saves this [readerChapter] last read history if incognito mode isn't on.
*/ */
private fun saveChapterHistory(chapter: ReaderChapter) { private suspend fun saveChapterHistory(readerChapter: ReaderChapter) {
if (!incognitoMode) { if (!incognitoMode) {
val history = History.create(chapter.chapter).apply { last_read = Date().time } val chapterId = readerChapter.chapter.id!!
db.upsertHistoryLastRead(history).asRxCompletable() val readAt = Date()
.onErrorComplete() val sessionReadDuration = chapterReadStartTime?.let { readAt.time - it } ?: 0
.subscribeOn(Schedulers.io())
.subscribe() upsertHistory.await(
HistoryUpdate(chapterId, readAt, sessionReadDuration),
).also {
chapterReadStartTime = null
}
} }
} }
fun saveProgress() { fun setReadStartTime() {
getCurrentChapter()?.let { onChapterChanged(it) } chapterReadStartTime = Date().time
} }
/** /**
@ -633,7 +658,7 @@ class ReaderPresenter(
* Shares the image of this [page] and notifies the UI with the path of the file to share. * Shares the image of this [page] and notifies the UI with the path of the file to share.
* The image must be first copied to the internal partition because there are many possible * The image must be first copied to the internal partition because there are many possible
* formats it can come from, like a zipped chapter, in which case it's not possible to directly * formats it can come from, like a zipped chapter, in which case it's not possible to directly
* get a path to the file and it has to be decompresssed somewhere first. Only the last shared * get a path to the file and it has to be decompressed somewhere first. Only the last shared
* image will be kept so it won't be taking lots of internal disk space. * image will be kept so it won't be taking lots of internal disk space.
*/ */
fun shareImage(page: ReaderPage) { fun shareImage(page: ReaderPage) {

View File

@ -26,4 +26,19 @@ WHERE _id = :id;
getChapterByMangaId: getChapterByMangaId:
SELECT * SELECT *
FROM chapters FROM chapters
WHERE manga_id = :mangaId; WHERE manga_id = :mangaId;
update:
UPDATE chapters
SET manga_id = coalesce(:mangaId, manga_id),
url = coalesce(:url, url),
name = coalesce(:name, name),
scanlator = coalesce(:scanlator, scanlator),
read = coalesce(:read, read),
bookmark = coalesce(:bookmark, bookmark),
last_page_read = coalesce(:lastPageRead, last_page_read),
chapter_number = coalesce(:chapterNumber, chapter_number),
source_order = coalesce(:sourceOrder, source_order),
date_fetch = coalesce(:dateFetch, date_fetch),
date_upload = coalesce(:dateUpload, date_upload)
WHERE _id = :chapterId;

View File

@ -1,31 +1,31 @@
import java.util.Date; import java.util.Date;
CREATE TABLE history( CREATE TABLE history(
history_id INTEGER NOT NULL PRIMARY KEY, _id INTEGER NOT NULL PRIMARY KEY,
history_chapter_id INTEGER NOT NULL UNIQUE, chapter_id INTEGER NOT NULL UNIQUE,
history_last_read INTEGER AS Date, last_read INTEGER AS Date,
history_time_read INTEGER AS Date, time_read INTEGER NOT NULL,
FOREIGN KEY(history_chapter_id) REFERENCES chapters (_id) FOREIGN KEY(chapter_id) REFERENCES chapters (_id)
ON DELETE CASCADE ON DELETE CASCADE
); );
CREATE INDEX history_history_chapter_id_index ON history(history_chapter_id); CREATE INDEX history_history_chapter_id_index ON history(chapter_id);
resetHistoryById: resetHistoryById:
UPDATE history UPDATE history
SET history_last_read = 0 SET last_read = 0
WHERE history_id = :historyId; WHERE _id = :historyId;
resetHistoryByMangaId: resetHistoryByMangaId:
UPDATE history UPDATE history
SET history_last_read = 0 SET last_read = 0
WHERE history_id IN ( WHERE _id IN (
SELECT H.history_id SELECT H._id
FROM mangas M FROM mangas M
INNER JOIN chapters C INNER JOIN chapters C
ON M._id = C.manga_id ON M._id = C.manga_id
INNER JOIN history H INNER JOIN history H
ON C._id = H.history_chapter_id ON C._id = H.chapter_id
WHERE M._id = :mangaId WHERE M._id = :mangaId
); );
@ -34,4 +34,14 @@ DELETE FROM history;
removeResettedHistory: removeResettedHistory:
DELETE FROM history DELETE FROM history
WHERE history_last_read = 0; WHERE last_read = 0;
insert:
INSERT INTO history(chapter_id, last_read, time_read)
VALUES (:chapterId, :readAt, :readDuration);
update:
UPDATE history
SET last_read = :readAt,
time_read = time_read + :sessionReadDuration
WHERE chapter_id = :chapterId;

View File

@ -0,0 +1,52 @@
import java.util.Date;
DROP INDEX IF EXISTS history_history_chapter_id_index;
DROP VIEW IF EXISTS historyView;
/**
* [last_read] was made not-null
* [time_read] was kept as long and made non-null
* `history` prefix was removed from table name
*/
ALTER TABLE history RENAME TO history_temp;
CREATE TABLE history(
_id INTEGER NOT NULL PRIMARY KEY,
chapter_id INTEGER NOT NULL UNIQUE,
last_read INTEGER AS Date NOT NULL,
time_read INTEGER NOT NULL,
FOREIGN KEY(chapter_id) REFERENCES chapters (_id)
ON DELETE CASCADE
);
INSERT INTO history
SELECT history_id, history_chapter_id, coalesce(history_last_read, 0), coalesce(history_time_read, 0)
FROM history_temp;
/**
* [history.time_read] was added as a column in [historyView]
*/
CREATE VIEW historyView AS
SELECT
history._id AS id,
mangas._id AS mangaId,
chapters._id AS chapterId,
mangas.title,
mangas.thumbnail_url AS thumbnailUrl,
chapters.chapter_number AS chapterNumber,
history.last_read AS readAt,
history.time_read AS readDuration,
max_last_read.last_read AS maxReadAt,
max_last_read.chapter_id AS maxReadAtChapterId
FROM mangas
JOIN chapters
ON mangas._id = chapters.manga_id
JOIN history
ON chapters._id = history.chapter_id
JOIN (
SELECT chapters.manga_id,chapters._id AS chapter_id, MAX(history.last_read) AS last_read
FROM chapters JOIN history
ON chapters._id = history.chapter_id
GROUP BY chapters.manga_id
) AS max_last_read
ON chapters.manga_id = max_last_read.manga_id;
CREATE INDEX history_history_chapter_id_index ON history(chapter_id);

View File

@ -1,24 +1,25 @@
CREATE VIEW historyView AS CREATE VIEW historyView AS
SELECT SELECT
history.history_id AS id, history._id AS id,
mangas._id AS mangaId, mangas._id AS mangaId,
chapters._id AS chapterId, chapters._id AS chapterId,
mangas.title, mangas.title,
mangas.thumbnail_url AS thumnailUrl, mangas.thumbnail_url AS thumbnailUrl,
chapters.chapter_number AS chapterNumber, chapters.chapter_number AS chapterNumber,
history.history_last_read AS readAt, history.last_read AS readAt,
max_last_read.history_last_read AS maxReadAt, history.time_read AS readDuration,
max_last_read.history_chapter_id AS maxReadAtChapterId max_last_read.last_read AS maxReadAt,
max_last_read.chapter_id AS maxReadAtChapterId
FROM mangas FROM mangas
JOIN chapters JOIN chapters
ON mangas._id = chapters.manga_id ON mangas._id = chapters.manga_id
JOIN history JOIN history
ON chapters._id = history.history_chapter_id ON chapters._id = history.chapter_id
JOIN ( JOIN (
SELECT chapters.manga_id,chapters._id AS history_chapter_id, MAX(history.history_last_read) AS history_last_read SELECT chapters.manga_id,chapters._id AS chapter_id, MAX(history.last_read) AS last_read
FROM chapters JOIN history FROM chapters JOIN history
ON chapters._id = history.history_chapter_id ON chapters._id = history.chapter_id
GROUP BY chapters.manga_id GROUP BY chapters.manga_id
) AS max_last_read ) AS max_last_read
ON chapters.manga_id = max_last_read.manga_id; ON chapters.manga_id = max_last_read.manga_id;
@ -35,9 +36,10 @@ id,
mangaId, mangaId,
chapterId, chapterId,
title, title,
thumnailUrl, thumbnailUrl,
chapterNumber, chapterNumber,
readAt readAt,
readDuration
FROM historyView FROM historyView
WHERE historyView.readAt > 0 WHERE historyView.readAt > 0
AND maxReadAtChapterId = historyView.chapterId AND maxReadAtChapterId = historyView.chapterId