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 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(
id = id,
chapterId = chapterId,
readAt = readAt,
readDuration = readDuration,
)
}
val historyWithRelationsMapper: (Long, Long, Long, String, String?, Float, Date?) -> HistoryWithRelations = {
historyId, mangaId, chapterId, title, thumbnailUrl, chapterNumber, readAt ->
val historyWithRelationsMapper: (Long, Long, Long, String, String?, Float, Date?, Long) -> HistoryWithRelations = {
historyId, mangaId, chapterId, title, thumbnailUrl, chapterNumber, readAt, readDuration ->
HistoryWithRelations(
id = historyId,
chapterId = chapterId,
@ -22,5 +23,6 @@ val historyWithRelationsMapper: (Long, Long, Long, String, String?, Float, Date?
thumbnailUrl = thumbnailUrl ?: "",
chapterNumber = chapterNumber,
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.manga.mangaMapper
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.repository.HistoryRepository
import eu.kanade.domain.manga.model.Manga
@ -89,4 +90,28 @@ class HistoryRepositoryImpl(
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
import eu.kanade.data.chapter.ChapterRepositoryImpl
import eu.kanade.data.history.HistoryRepositoryImpl
import eu.kanade.data.manga.MangaRepositoryImpl
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.GetExtensionSources
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.RemoveHistoryById
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.manga.interactor.GetFavoritesBySourceId
import eu.kanade.domain.manga.interactor.ResetViewerFlags
@ -38,9 +42,13 @@ class DomainModule : InjektModule {
addFactory { GetNextChapterForManga(get()) }
addFactory { ResetViewerFlags(get()) }
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
addFactory { UpdateChapter(get()) }
addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) }
addFactory { DeleteHistoryTable(get()) }
addFactory { GetHistory(get()) }
addFactory { UpsertHistory(get()) }
addFactory { RemoveHistoryById(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
data class History(
val id: Long?,
val id: Long,
val chapterId: Long,
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 chapterNumber: Float,
val readAt: Date?,
val readDuration: Long,
)

View file

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

View file

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

View file

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

View file

@ -21,7 +21,7 @@ class HistoryImpl : History {
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
}

View file

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

View file

@ -4,9 +4,12 @@ import android.app.Application
import android.net.Uri
import android.os.Bundle
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.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@ -62,6 +65,8 @@ class ReaderPresenter(
private val coverCache: CoverCache = Injekt.get(),
private val preferences: PreferencesHelper = Injekt.get(),
private val delayedTrackingStore: DelayedTrackingStore = Injekt.get(),
private val upsertHistory: UpsertHistory = Injekt.get(),
private val updateChapter: UpdateChapter = Injekt.get(),
) : BasePresenter<ReaderActivity>() {
/**
@ -80,6 +85,11 @@ class ReaderPresenter(
*/
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.
*/
@ -168,8 +178,7 @@ class ReaderPresenter(
val currentChapters = viewerChaptersRelay.value
if (currentChapters != null) {
currentChapters.unref()
saveChapterProgress(currentChapters.currChapter)
saveChapterHistory(currentChapters.currChapter)
saveReadingProgress(currentChapters.currChapter)
}
}
@ -200,7 +209,9 @@ class ReaderPresenter(
*/
fun onSaveInstanceStateNonConfigurationChange() {
val currentChapter = getCurrentChapter() ?: return
saveChapterProgress(currentChapter)
launchIO {
saveChapterProgress(currentChapter)
}
}
/**
@ -397,7 +408,7 @@ class ReaderPresenter(
if (selectedChapter != currentChapters.currChapter) {
logcat { "Setting ${selectedChapter.chapter.url} as active" }
onChapterChanged(currentChapters.currChapter)
saveReadingProgress(currentChapters.currChapter)
loadNewChapter(selectedChapter)
}
}
@ -429,43 +440,57 @@ class ReaderPresenter(
}
}
/**
* Called when a chapter changed from [fromChapter] to [toChapter]. It updates [fromChapter]
* on the database.
*/
private fun onChapterChanged(fromChapter: ReaderChapter) {
saveChapterProgress(fromChapter)
saveChapterHistory(fromChapter)
fun saveCurrentChapterReadingProgress() {
getCurrentChapter()?.let { saveReadingProgress(it) }
}
/**
* 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
*/
private fun saveChapterProgress(chapter: ReaderChapter) {
private suspend fun saveChapterProgress(readerChapter: ReaderChapter) {
if (!incognitoMode || hasTrackers) {
db.updateChapterProgress(chapter.chapter).asRxCompletable()
.onErrorComplete()
.subscribeOn(Schedulers.io())
.subscribe()
val chapter = readerChapter.chapter
updateChapter.await(
ChapterUpdate(
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) {
val history = History.create(chapter.chapter).apply { last_read = Date().time }
db.upsertHistoryLastRead(history).asRxCompletable()
.onErrorComplete()
.subscribeOn(Schedulers.io())
.subscribe()
val chapterId = readerChapter.chapter.id!!
val readAt = Date()
val sessionReadDuration = chapterReadStartTime?.let { readAt.time - it } ?: 0
upsertHistory.await(
HistoryUpdate(chapterId, readAt, sessionReadDuration),
).also {
chapterReadStartTime = null
}
}
}
fun saveProgress() {
getCurrentChapter()?.let { onChapterChanged(it) }
fun setReadStartTime() {
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.
* 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
* 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.
*/
fun shareImage(page: ReaderPage) {

View file

@ -27,3 +27,18 @@ getChapterByMangaId:
SELECT *
FROM chapters
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;
CREATE TABLE history(
history_id INTEGER NOT NULL PRIMARY KEY,
history_chapter_id INTEGER NOT NULL UNIQUE,
history_last_read INTEGER AS Date,
history_time_read INTEGER AS Date,
FOREIGN KEY(history_chapter_id) REFERENCES chapters (_id)
_id INTEGER NOT NULL PRIMARY KEY,
chapter_id INTEGER NOT NULL UNIQUE,
last_read INTEGER AS Date,
time_read INTEGER NOT NULL,
FOREIGN KEY(chapter_id) REFERENCES chapters (_id)
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:
UPDATE history
SET history_last_read = 0
WHERE history_id = :historyId;
SET last_read = 0
WHERE _id = :historyId;
resetHistoryByMangaId:
UPDATE history
SET history_last_read = 0
WHERE history_id IN (
SELECT H.history_id
SET last_read = 0
WHERE _id IN (
SELECT H._id
FROM mangas M
INNER JOIN chapters C
ON M._id = C.manga_id
INNER JOIN history H
ON C._id = H.history_chapter_id
ON C._id = H.chapter_id
WHERE M._id = :mangaId
);
@ -34,4 +34,14 @@ DELETE FROM history;
removeResettedHistory:
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
SELECT
history.history_id AS id,
mangas._id AS mangaId,
chapters._id AS chapterId,
mangas.title,
mangas.thumbnail_url AS thumnailUrl,
chapters.chapter_number AS chapterNumber,
history.history_last_read AS readAt,
max_last_read.history_last_read AS maxReadAt,
max_last_read.history_chapter_id AS maxReadAtChapterId
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.history_chapter_id
ON chapters._id = history.chapter_id
JOIN (
SELECT chapters.manga_id,chapters._id AS history_chapter_id, MAX(history.history_last_read) AS history_last_read
FROM chapters JOIN history
ON chapters._id = history.history_chapter_id
GROUP BY chapters.manga_id
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;
@ -35,9 +36,10 @@ id,
mangaId,
chapterId,
title,
thumnailUrl,
thumbnailUrl,
chapterNumber,
readAt
readAt,
readDuration
FROM historyView
WHERE historyView.readAt > 0
AND maxReadAtChapterId = historyView.chapterId