Build manga and chapter updates with a lambda and track assigned fields

MangaUpdate and ChapterUpdate are built with a lambda that records which fields were assigned, so an update writes only those fields.
This commit is contained in:
AntsyLich
2026-09-27 22:36:13 +06:00
parent 306befb214
commit f8fff318ba
21 changed files with 155 additions and 172 deletions
@@ -21,11 +21,10 @@ class SetReadStatus(
) {
private val mapper = { chapter: Chapter, read: Boolean ->
ChapterUpdate(
read = read,
lastPageRead = if (!read) 0 else null,
id = chapter.id,
)
ChapterUpdate(chapter.id) {
this.read = read
if (!read) lastPageRead = 0
}
}
suspend fun await(read: Boolean, vararg chapters: Chapter): Result = withNonCancellableContext {
@@ -14,20 +14,18 @@ class SetMangaViewerFlags(
suspend fun awaitSetReadingMode(id: Long, flag: Long) {
val manga = mangaRepository.getMangaById(id)
mangaRepository.update(
MangaUpdate(
id = id,
viewerFlags = manga.viewerFlags.setFlag(flag, ReadingMode.MASK.toLong()),
),
MangaUpdate(id) {
viewerFlags = manga.viewerFlags.setFlag(flag, ReadingMode.MASK.toLong())
},
)
}
suspend fun awaitSetOrientation(id: Long, flag: Long) {
val manga = mangaRepository.getMangaById(id)
mangaRepository.update(
MangaUpdate(
id = id,
viewerFlags = manga.viewerFlags.setFlag(flag, ReaderOrientation.MASK.toLong()),
),
MangaUpdate(id) {
viewerFlags = manga.viewerFlags.setFlag(flag, ReaderOrientation.MASK.toLong())
},
)
}
@@ -31,20 +31,19 @@ class UpdateManga(
window: Pair<Long, Long> = fetchInterval.getWindow(dateTime.date, timeZone),
): Boolean {
return mangaRepository.update(
fetchInterval.toMangaUpdate(manga, dateTime, timeZone, window),
fetchInterval.withFetchInterval(manga, dateTime, timeZone, window),
)
}
suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean {
return mangaRepository.update(MangaUpdate(id = mangaId, lastUpdate = Clock.System.now().toEpochMilliseconds()))
return mangaRepository.update(MangaUpdate(mangaId) { lastUpdate = Clock.System.now().toEpochMilliseconds() })
}
suspend fun awaitUpdateCoverLastModified(mangaId: Long): Boolean {
return mangaRepository.update(
MangaUpdate(
id = mangaId,
coverLastModified = Clock.System.now().toEpochMilliseconds(),
),
MangaUpdate(mangaId) {
coverLastModified = Clock.System.now().toEpochMilliseconds()
},
)
}
@@ -54,7 +53,10 @@ class UpdateManga(
false -> 0
}
return mangaRepository.update(
MangaUpdate(id = mangaId, favorite = favorite, dateAdded = dateAdded),
MangaUpdate(mangaId) {
this.favorite = favorite
this.dateAdded = dateAdded
},
)
}
}
@@ -8,7 +8,7 @@ import logcat.LogPriority
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.chapter.interactor.UpdateChapter
import tachiyomi.domain.chapter.model.toChapterUpdate
import tachiyomi.domain.chapter.model.ChapterUpdate
import tachiyomi.domain.track.interactor.InsertTrack
import tachiyomi.domain.track.model.Track
import kotlin.math.max
@@ -35,7 +35,7 @@ class SyncChapterProgressWithTrack(
val chapterUpdates = sortedChapters
.filter { chapter -> chapter.chapterNumber <= remoteTrack.lastChapterRead && !chapter.read }
.map { it.copy(read = true).toChapterUpdate() }
.map { ChapterUpdate(it.id) { read = true } }
// only take into account continuous reading
val localLastRead = sortedChapters.takeWhile { it.read }.lastOrNull()?.chapterNumber ?: 0F
@@ -59,7 +59,7 @@ class MangaRestorer(
}
restoreRepository.restoreManga(entries) {
fetchInterval.toMangaUpdate(it, now, timeZone, currentFetchWindow)
fetchInterval.withFetchInterval(it, now, timeZone, currentFetchWindow)
}
}
}
@@ -25,7 +25,7 @@ import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.domain.chapter.interactor.GetChapter
import tachiyomi.domain.chapter.interactor.UpdateChapter
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.chapter.model.toChapterUpdate
import tachiyomi.domain.chapter.model.ChapterUpdate
import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.model.Manga
@@ -192,7 +192,6 @@ class NotificationReceiver : BroadcastReceiver() {
launchIO {
val toUpdate = chapterUrls.mapNotNull { getChapter.await(it, mangaId) }
.map {
val chapter = it.copy(read = true)
if (downloadPreferences.removeAfterMarkedAsRead.get()) {
val manga = getManga.await(mangaId)
if (manga != null) {
@@ -202,7 +201,7 @@ class NotificationReceiver : BroadcastReceiver() {
}
}
}
chapter.toChapterUpdate()
ChapterUpdate(it.id) { read = true }
}
updateChapter.awaitAll(toUpdate)
}
@@ -51,8 +51,8 @@ import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga
import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaUpdate
import tachiyomi.domain.manga.model.MangaWithChapterCount
import tachiyomi.domain.manga.model.toMangaUpdate
import tachiyomi.domain.source.interactor.GetRemoteManga
import tachiyomi.domain.source.service.SourceManager
import kotlin.time.Clock
@@ -233,22 +233,23 @@ class BrowseSourceViewModel(
*/
fun changeMangaFavorite(manga: Manga) {
viewModelScope.launch {
var new = manga.copy(
favorite = !manga.favorite,
dateAdded = when (manga.favorite) {
true -> 0
false -> Clock.System.now().toEpochMilliseconds()
},
)
if (!new.favorite) {
new = new.removeCovers(coverCache)
val update = if (manga.favorite) {
val coverLastModified = manga.removeCovers(coverCache).coverLastModified
MangaUpdate(manga.id) {
favorite = false
dateAdded = 0
if (coverLastModified != manga.coverLastModified) this.coverLastModified = coverLastModified
}
} else {
setMangaDefaultChapterFlags.await(manga)
addTracks.bindEnhancedTrackers(manga, sourceManager.getOrStub(manga.source))
MangaUpdate(manga.id) {
favorite = true
dateAdded = Clock.System.now().toEpochMilliseconds()
}
}
updateManga.await(new.toMangaUpdate())
updateManga.await(update)
}
}
@@ -571,10 +571,9 @@ class LibraryViewModel(
if (deleteFromLibrary) {
val toDelete = mangas.map {
it.removeCovers(coverCache)
MangaUpdate(
favorite = false,
id = it.id,
)
MangaUpdate(it.id) {
favorite = false
}
}
updateManga.awaitAll(toDelete)
}
@@ -812,7 +812,7 @@ class MangaViewModel(
viewModelScope.launchIO {
chapters
.filterNot { it.bookmark == bookmarked }
.map { ChapterUpdate(id = it.id, bookmark = bookmarked) }
.map { ChapterUpdate(it.id) { bookmark = bookmarked } }
.let { updateChapter.awaitAll(it) }
}
toggleAllSelection(false)
@@ -583,11 +583,10 @@ class ReaderViewModel(
}
updateChapter.await(
ChapterUpdate(
id = readerChapter.chapter.id!!,
read = readerChapter.chapter.read,
lastPageRead = readerChapter.chapter.last_page_read.toLong(),
),
ChapterUpdate(readerChapter.chapter.id!!) {
read = readerChapter.chapter.read
lastPageRead = readerChapter.chapter.last_page_read.toLong()
},
)
}
}
@@ -608,7 +607,7 @@ class ReaderViewModel(
chapter.isRecognizedNumber &&
chapter.chapterNumber.toFloat() == readerChapter.chapter.chapter_number
) {
ChapterUpdate(id = chapter.id, read = true)
ChapterUpdate(chapter.id) { read = true }
} else {
null
}
@@ -683,10 +682,9 @@ class ReaderViewModel(
viewModelScope.launchNonCancellable {
updateChapter.await(
ChapterUpdate(
id = chapter.id!!,
bookmark = bookmarked,
),
ChapterUpdate(chapter.id!!) {
bookmark = bookmarked
},
)
}
@@ -303,7 +303,7 @@ class UpdatesViewModel(
viewModelScope.launchIO {
updates
.filterNot { it.update.bookmark == bookmark }
.map { ChapterUpdate(id = it.update.chapterId, bookmark = bookmark) }
.map { ChapterUpdate(it.update.chapterId) { this.bookmark = bookmark } }
.let { updateChapter.awaitAll(it) }
}
toggleAllSelection(false)
@@ -15,7 +15,7 @@ import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.interactor.SetMangaCategories
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.chapter.interactor.UpdateChapter
import tachiyomi.domain.chapter.model.toChapterUpdate
import tachiyomi.domain.chapter.model.ChapterUpdate
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaUpdate
import tachiyomi.domain.source.service.SourceManager
@@ -58,28 +58,23 @@ class MigrateMangaUseCase(
.filter { it.read }
.maxOfOrNull { it.chapterNumber }
val updatedMangaChapters = mangaChapters.map { mangaChapter ->
var updatedChapter = mangaChapter
if (updatedChapter.isRecognizedNumber) {
val chapterUpdates = mangaChapters
.filter { it.isRecognizedNumber }
.map { mangaChapter ->
val prevChapter = prevMangaChapters
.find { it.isRecognizedNumber && it.chapterNumber == updatedChapter.chapterNumber }
.find { it.isRecognizedNumber && it.chapterNumber == mangaChapter.chapterNumber }
if (prevChapter != null) {
updatedChapter = updatedChapter.copy(
dateFetch = prevChapter.dateFetch,
bookmark = prevChapter.bookmark,
)
}
ChapterUpdate(mangaChapter.id) {
if (prevChapter != null) {
dateFetch = prevChapter.dateFetch
bookmark = prevChapter.bookmark
}
if (maxChapterRead != null && updatedChapter.chapterNumber <= maxChapterRead) {
updatedChapter = updatedChapter.copy(read = true)
if (maxChapterRead != null && mangaChapter.chapterNumber <= maxChapterRead) {
read = true
}
}
}
updatedChapter
}
val chapterUpdates = updatedMangaChapters.map { it.toChapterUpdate() }
updateChapter.awaitAll(chapterUpdates)
}
@@ -115,20 +110,18 @@ class MigrateMangaUseCase(
coverCache.setCustomCoverToCache(target, coverCache.getCustomCoverFile(current.id).inputStream())
}
val currentMangaUpdate = MangaUpdate(
id = current.id,
favorite = false,
dateAdded = 0,
)
val currentMangaUpdate = MangaUpdate(current.id) {
favorite = false
dateAdded = 0
}
.takeIf { replace }
val targetMangaUpdate = MangaUpdate(
id = target.id,
favorite = true,
chapterFlags = current.chapterFlags,
viewerFlags = current.viewerFlags,
dateAdded = if (replace) current.dateAdded else Clock.System.now().toEpochMilliseconds(),
notes = if (MigrationFlag.NOTES in flags) current.notes else null,
)
val targetMangaUpdate = MangaUpdate(target.id) {
favorite = true
chapterFlags = current.chapterFlags
viewerFlags = current.viewerFlags
dateAdded = if (replace) current.dateAdded else Clock.System.now().toEpochMilliseconds()
if (MigrationFlag.NOTES in flags) notes = current.notes
}
updateManga.awaitAll(listOfNotNull(currentMangaUpdate, targetMangaUpdate))
} catch (e: Throwable) {
@@ -82,7 +82,6 @@ class ChapterRepositoryImpl(
database.transaction {
chapterUpdates.forEach { chapterUpdate ->
database.chaptersQueries.update(
mangaId = chapterUpdate.mangaId,
read = chapterUpdate.read,
bookmark = chapterUpdate.bookmark,
lastPageRead = chapterUpdate.lastPageRead,
@@ -245,18 +245,20 @@ class MangaRepositoryImpl(
private suspend fun partialUpdate(vararg mangaUpdates: MangaUpdate) {
database.transaction {
mangaUpdates.forEach { value ->
database.mangasQueries.update(
favorite = value.favorite,
lastUpdate = value.lastUpdate,
nextUpdate = value.nextUpdate,
calculateInterval = value.fetchInterval?.toLong(),
viewer = value.viewerFlags,
chapterFlags = value.chapterFlags,
coverLastModified = value.coverLastModified,
dateAdded = value.dateAdded,
mangaId = value.id,
notes = value.notes,
)
with(value) {
database.mangasQueries.update(
favorite = favorite,
lastUpdate = lastUpdate,
nextUpdate = nextUpdate,
calculateInterval = fetchInterval?.toLong(),
viewer = viewerFlags,
chapterFlags = chapterFlags,
coverLastModified = coverLastModified,
dateAdded = dateAdded,
mangaId = id,
notes = notes,
)
}
}
}
}
@@ -79,8 +79,7 @@ RETURNING _id;
update:
UPDATE chapters
SET manga_id = coalesce(:mangaId, manga_id),
read = coalesce(:read, read),
SET read = coalesce(:read, read),
bookmark = coalesce(:bookmark, bookmark),
last_page_read = coalesce(:lastPageRead, last_page_read),
date_fetch = coalesce(:dateFetch, date_fetch)
@@ -0,0 +1,15 @@
package mihon.domain.common
import kotlin.properties.Delegates
import kotlin.reflect.KProperty
abstract class PartialUpdate {
private val assigned = mutableSetOf<String>()
fun isSet(property: KProperty<*>): Boolean = property.name in assigned
protected fun <T> field(initial: T) = Delegates.observable(initial) { property, _, _ ->
assigned += property.name
}
}
@@ -1,21 +1,14 @@
package tachiyomi.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,
)
import mihon.domain.common.PartialUpdate
fun Chapter.toChapterUpdate(): ChapterUpdate {
return ChapterUpdate(
id = id,
mangaId = mangaId,
read = read,
bookmark = bookmark,
lastPageRead = lastPageRead,
dateFetch = dateFetch,
)
class ChapterUpdate(val id: Long, block: ChapterUpdate.() -> Unit) : PartialUpdate() {
var read: Boolean? by field(null)
var bookmark: Boolean? by field(null)
var lastPageRead: Long? by field(null)
var dateFetch: Long? by field(null)
init {
block()
}
}
@@ -22,7 +22,7 @@ class FetchInterval(
private val getChaptersByMangaId: GetChaptersByMangaId,
) {
suspend fun toMangaUpdate(
suspend fun withFetchInterval(
manga: Manga,
dateTime: LocalDateTime,
timeZone: TimeZone,
@@ -39,7 +39,10 @@ class FetchInterval(
}
val nextUpdate = calculateNextUpdate(manga, interval, dateTime, timeZone, currentWindow)
return MangaUpdate(id = manga.id, nextUpdate = nextUpdate, fetchInterval = interval)
return MangaUpdate(manga.id) {
this.nextUpdate = nextUpdate
fetchInterval = interval
}
}
fun getWindow(localDateTime: LocalDate, timeZone: TimeZone): Pair<Long, Long> {
@@ -12,37 +12,33 @@ class SetMangaChapterFlags(
suspend fun awaitSetDownloadedFilter(manga: Manga, flag: Long): Boolean {
return mangaRepository.update(
MangaUpdate(
id = manga.id,
chapterFlags = manga.chapterFlags.setFlag(flag, Manga.CHAPTER_DOWNLOADED_MASK),
),
MangaUpdate(manga.id) {
chapterFlags = manga.chapterFlags.setFlag(flag, Manga.CHAPTER_DOWNLOADED_MASK)
},
)
}
suspend fun awaitSetUnreadFilter(manga: Manga, flag: Long): Boolean {
return mangaRepository.update(
MangaUpdate(
id = manga.id,
chapterFlags = manga.chapterFlags.setFlag(flag, Manga.CHAPTER_UNREAD_MASK),
),
MangaUpdate(manga.id) {
chapterFlags = manga.chapterFlags.setFlag(flag, Manga.CHAPTER_UNREAD_MASK)
},
)
}
suspend fun awaitSetBookmarkFilter(manga: Manga, flag: Long): Boolean {
return mangaRepository.update(
MangaUpdate(
id = manga.id,
chapterFlags = manga.chapterFlags.setFlag(flag, Manga.CHAPTER_BOOKMARKED_MASK),
),
MangaUpdate(manga.id) {
chapterFlags = manga.chapterFlags.setFlag(flag, Manga.CHAPTER_BOOKMARKED_MASK)
},
)
}
suspend fun awaitSetDisplayMode(manga: Manga, flag: Long): Boolean {
return mangaRepository.update(
MangaUpdate(
id = manga.id,
chapterFlags = manga.chapterFlags.setFlag(flag, Manga.CHAPTER_DISPLAY_MASK),
),
MangaUpdate(manga.id) {
chapterFlags = manga.chapterFlags.setFlag(flag, Manga.CHAPTER_DISPLAY_MASK)
},
)
}
@@ -64,10 +60,9 @@ class SetMangaChapterFlags(
}
}
return mangaRepository.update(
MangaUpdate(
id = manga.id,
chapterFlags = newFlags,
),
MangaUpdate(manga.id) {
chapterFlags = newFlags
},
)
}
@@ -81,15 +76,14 @@ class SetMangaChapterFlags(
displayMode: Long,
): Boolean {
return mangaRepository.update(
MangaUpdate(
id = mangaId,
MangaUpdate(mangaId) {
chapterFlags = 0L.setFlag(unreadFilter, Manga.CHAPTER_UNREAD_MASK)
.setFlag(downloadedFilter, Manga.CHAPTER_DOWNLOADED_MASK)
.setFlag(bookmarkedFilter, Manga.CHAPTER_BOOKMARKED_MASK)
.setFlag(sortingMode, Manga.CHAPTER_SORTING_MASK)
.setFlag(sortingDirection, Manga.CHAPTER_SORT_DIR_MASK)
.setFlag(displayMode, Manga.CHAPTER_DISPLAY_MASK),
),
.setFlag(displayMode, Manga.CHAPTER_DISPLAY_MASK)
},
)
}
@@ -11,10 +11,9 @@ class UpdateMangaNotes(
suspend operator fun invoke(mangaId: Long, notes: String): Boolean {
return mangaRepository.update(
MangaUpdate(
id = mangaId,
notes = notes,
),
MangaUpdate(mangaId) {
this.notes = notes
},
)
}
}
@@ -1,29 +1,19 @@
package tachiyomi.domain.manga.model
data class MangaUpdate(
val id: Long,
val favorite: Boolean? = null,
val lastUpdate: Long? = null,
val nextUpdate: Long? = null,
val fetchInterval: Int? = null,
val dateAdded: Long? = null,
val viewerFlags: Long? = null,
val chapterFlags: Long? = null,
val coverLastModified: Long? = null,
val notes: String? = null,
)
import mihon.domain.common.PartialUpdate
fun Manga.toMangaUpdate(): MangaUpdate {
return MangaUpdate(
id = id,
favorite = favorite,
lastUpdate = lastUpdate,
nextUpdate = nextUpdate,
fetchInterval = fetchInterval,
dateAdded = dateAdded,
viewerFlags = viewerFlags,
chapterFlags = chapterFlags,
coverLastModified = coverLastModified,
notes = notes,
)
class MangaUpdate(val id: Long, block: MangaUpdate.() -> Unit) : PartialUpdate() {
var favorite: Boolean? by field(null)
var lastUpdate: Long? by field(null)
var nextUpdate: Long? by field(null)
var fetchInterval: Int? by field(null)
var dateAdded: Long? by field(null)
var viewerFlags: Long? by field(null)
var chapterFlags: Long? by field(null)
var coverLastModified: Long? by field(null)
var notes: String? by field(null)
init {
block()
}
}