Cache chapters list (close #812)

master
Koitharu 1 year ago
parent 3f66c142b8
commit 22e2411c77
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -29,8 +29,9 @@ class AutoFixUseCase @Inject constructor(
) { ) {
suspend operator fun invoke(mangaId: Long): Pair<Manga, Manga?> { suspend operator fun invoke(mangaId: Long): Pair<Manga, Manga?> {
val seed = checkNotNull(mangaDataRepository.findMangaById(mangaId)) { "Manga $mangaId not found" } val seed = checkNotNull(
.getDetailsSafe() mangaDataRepository.findMangaById(mangaId, withChapters = true),
) { "Manga $mangaId not found" }.getDetailsSafe()
if (seed.isHealthy()) { if (seed.isHealthy()) {
return seed to null // no fix required return seed to null // no fix required
} }

@ -12,11 +12,13 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koitharu.kotatsu.bookmarks.data.BookmarkEntity import org.koitharu.kotatsu.bookmarks.data.BookmarkEntity
import org.koitharu.kotatsu.bookmarks.data.BookmarksDao import org.koitharu.kotatsu.bookmarks.data.BookmarksDao
import org.koitharu.kotatsu.core.db.dao.ChaptersDao
import org.koitharu.kotatsu.core.db.dao.MangaDao import org.koitharu.kotatsu.core.db.dao.MangaDao
import org.koitharu.kotatsu.core.db.dao.MangaSourcesDao import org.koitharu.kotatsu.core.db.dao.MangaSourcesDao
import org.koitharu.kotatsu.core.db.dao.PreferencesDao import org.koitharu.kotatsu.core.db.dao.PreferencesDao
import org.koitharu.kotatsu.core.db.dao.TagsDao import org.koitharu.kotatsu.core.db.dao.TagsDao
import org.koitharu.kotatsu.core.db.dao.TrackLogsDao import org.koitharu.kotatsu.core.db.dao.TrackLogsDao
import org.koitharu.kotatsu.core.db.entity.ChapterEntity
import org.koitharu.kotatsu.core.db.entity.MangaEntity import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
@ -36,6 +38,7 @@ import org.koitharu.kotatsu.core.db.migrations.Migration1To2
import org.koitharu.kotatsu.core.db.migrations.Migration20To21 import org.koitharu.kotatsu.core.db.migrations.Migration20To21
import org.koitharu.kotatsu.core.db.migrations.Migration21To22 import org.koitharu.kotatsu.core.db.migrations.Migration21To22
import org.koitharu.kotatsu.core.db.migrations.Migration22To23 import org.koitharu.kotatsu.core.db.migrations.Migration22To23
import org.koitharu.kotatsu.core.db.migrations.Migration23To24
import org.koitharu.kotatsu.core.db.migrations.Migration2To3 import org.koitharu.kotatsu.core.db.migrations.Migration2To3
import org.koitharu.kotatsu.core.db.migrations.Migration3To4 import org.koitharu.kotatsu.core.db.migrations.Migration3To4
import org.koitharu.kotatsu.core.db.migrations.Migration4To5 import org.koitharu.kotatsu.core.db.migrations.Migration4To5
@ -63,14 +66,14 @@ import org.koitharu.kotatsu.tracker.data.TrackEntity
import org.koitharu.kotatsu.tracker.data.TrackLogEntity import org.koitharu.kotatsu.tracker.data.TrackLogEntity
import org.koitharu.kotatsu.tracker.data.TracksDao import org.koitharu.kotatsu.tracker.data.TracksDao
const val DATABASE_VERSION = 23 const val DATABASE_VERSION = 24
@Database( @Database(
entities = [ entities = [
MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class, MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class, ChapterEntity::class,
FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class, FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class, TrackEntity::class,
TrackEntity::class, TrackLogEntity::class, SuggestionEntity::class, BookmarkEntity::class, TrackLogEntity::class, SuggestionEntity::class, BookmarkEntity::class, ScrobblingEntity::class,
ScrobblingEntity::class, MangaSourceEntity::class, StatsEntity::class, LocalMangaIndexEntity::class, MangaSourceEntity::class, StatsEntity::class, LocalMangaIndexEntity::class,
], ],
version = DATABASE_VERSION, version = DATABASE_VERSION,
) )
@ -103,6 +106,8 @@ abstract class MangaDatabase : RoomDatabase() {
abstract fun getStatsDao(): StatsDao abstract fun getStatsDao(): StatsDao
abstract fun getLocalMangaIndexDao(): LocalMangaIndexDao abstract fun getLocalMangaIndexDao(): LocalMangaIndexDao
abstract fun getChaptersDao(): ChaptersDao
} }
fun getDatabaseMigrations(context: Context): Array<Migration> = arrayOf( fun getDatabaseMigrations(context: Context): Array<Migration> = arrayOf(
@ -128,6 +133,7 @@ fun getDatabaseMigrations(context: Context): Array<Migration> = arrayOf(
Migration20To21(), Migration20To21(),
Migration21To22(), Migration21To22(),
Migration22To23(), Migration22To23(),
Migration23To24(),
) )
fun MangaDatabase(context: Context): MangaDatabase = Room fun MangaDatabase(context: Context): MangaDatabase = Room

@ -7,3 +7,4 @@ const val TABLE_FAVOURITE_CATEGORIES = "favourite_categories"
const val TABLE_HISTORY = "history" const val TABLE_HISTORY = "history"
const val TABLE_MANGA_TAGS = "manga_tags" const val TABLE_MANGA_TAGS = "manga_tags"
const val TABLE_SOURCES = "sources" const val TABLE_SOURCES = "sources"
const val TABLE_CHAPTERS = "chapters"

@ -0,0 +1,29 @@
package org.koitharu.kotatsu.core.db.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Transaction
import org.koitharu.kotatsu.core.db.entity.ChapterEntity
@Dao
abstract class ChaptersDao {
@Query("SELECT * FROM chapters WHERE manga_id = :mangaId ORDER BY `index` ASC")
abstract suspend fun findAll(mangaId: Long): List<ChapterEntity>
@Query("DELETE FROM chapters WHERE manga_id = :mangaId")
abstract suspend fun deleteAll(mangaId: Long)
@Query("DELETE FROM chapters WHERE manga_id NOT IN (SELECT manga_id FROM history WHERE deleted_at = 0) AND manga_id NOT IN (SELECT manga_id FROM favourites WHERE deleted_at = 0)")
abstract suspend fun gc()
@Transaction
open suspend fun replaceAll(mangaId: Long, entities: Collection<ChapterEntity>) {
deleteAll(mangaId)
insert(entities)
}
@Insert
protected abstract suspend fun insert(entities: Collection<ChapterEntity>)
}

@ -20,6 +20,9 @@ abstract class MangaDao {
@Query("SELECT * FROM manga WHERE manga_id = :id") @Query("SELECT * FROM manga WHERE manga_id = :id")
abstract suspend fun find(id: Long): MangaWithTags? abstract suspend fun find(id: Long): MangaWithTags?
@Query("SELECT EXISTS(SELECT * FROM manga WHERE manga_id = :id)")
abstract suspend operator fun contains(id: Long): Boolean
@Transaction @Transaction
@Query("SELECT * FROM manga WHERE public_url = :publicUrl") @Query("SELECT * FROM manga WHERE public_url = :publicUrl")
abstract suspend fun findByPublicUrl(publicUrl: String): MangaWithTags? abstract suspend fun findByPublicUrl(publicUrl: String): MangaWithTags?

@ -0,0 +1,32 @@
package org.koitharu.kotatsu.core.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import org.koitharu.kotatsu.core.db.TABLE_CHAPTERS
@Entity(
tableName = TABLE_CHAPTERS,
primaryKeys = ["manga_id", "chapter_id"],
foreignKeys = [
ForeignKey(
entity = MangaEntity::class,
parentColumns = ["manga_id"],
childColumns = ["manga_id"],
onDelete = ForeignKey.CASCADE,
),
],
)
data class ChapterEntity(
@ColumnInfo(name = "chapter_id") val chapterId: Long,
@ColumnInfo(name = "manga_id") val mangaId: Long,
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "number") val number: Float,
@ColumnInfo(name = "volume") val volume: Int,
@ColumnInfo(name = "url") val url: String,
@ColumnInfo(name = "scanlator") val scanlator: String?,
@ColumnInfo(name = "upload_date") val uploadDate: Long,
@ColumnInfo(name = "branch") val branch: String?,
@ColumnInfo(name = "source") val source: String,
@ColumnInfo(name = "index") val index: Int,
)

@ -3,6 +3,7 @@ package org.koitharu.kotatsu.core.db.entity
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.util.ext.longHashCode import org.koitharu.kotatsu.core.util.ext.longHashCode
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaState import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
@ -21,7 +22,7 @@ fun Collection<TagEntity>.toMangaTags() = mapToSet(TagEntity::toMangaTag)
fun Collection<TagEntity>.toMangaTagsList() = map(TagEntity::toMangaTag) fun Collection<TagEntity>.toMangaTagsList() = map(TagEntity::toMangaTag)
fun MangaEntity.toManga(tags: Set<MangaTag>) = Manga( fun MangaEntity.toManga(tags: Set<MangaTag>, chapters: List<ChapterEntity>?) = Manga(
id = this.id, id = this.id,
title = this.title, title = this.title,
altTitle = this.altTitle, altTitle = this.altTitle,
@ -35,12 +36,27 @@ fun MangaEntity.toManga(tags: Set<MangaTag>) = Manga(
author = this.author, author = this.author,
source = MangaSource(this.source), source = MangaSource(this.source),
tags = tags, tags = tags,
chapters = chapters?.toMangaChapters(),
) )
fun MangaWithTags.toManga() = manga.toManga(tags.toMangaTags()) fun MangaWithTags.toManga(chapters: List<ChapterEntity>? = null) = manga.toManga(tags.toMangaTags(), chapters)
fun Collection<MangaWithTags>.toMangaList() = map { it.toManga() } fun Collection<MangaWithTags>.toMangaList() = map { it.toManga() }
fun ChapterEntity.toMangaChapter() = MangaChapter(
id = chapterId,
name = name,
number = number,
volume = volume,
url = url,
scanlator = scanlator,
uploadDate = uploadDate,
branch = branch,
source = MangaSource(source),
)
fun Collection<ChapterEntity>.toMangaChapters() = map { it.toMangaChapter() }
// Model to entity // Model to entity
fun Manga.toEntity() = MangaEntity( fun Manga.toEntity() = MangaEntity(
@ -67,6 +83,22 @@ fun MangaTag.toEntity() = TagEntity(
fun Collection<MangaTag>.toEntities() = map(MangaTag::toEntity) fun Collection<MangaTag>.toEntities() = map(MangaTag::toEntity)
fun Iterable<IndexedValue<MangaChapter>>.toEntities(mangaId: Long) = map { (index, chapter) ->
ChapterEntity(
chapterId = chapter.id,
mangaId = mangaId,
name = chapter.name,
number = chapter.number,
volume = chapter.volume,
url = chapter.url,
scanlator = chapter.scanlator,
uploadDate = chapter.uploadDate,
branch = chapter.branch,
source = chapter.source.name,
index = index,
)
}
// Other // Other
fun SortOrder(name: String, fallback: SortOrder): SortOrder = runCatching { fun SortOrder(name: String, fallback: SortOrder): SortOrder = runCatching {

@ -0,0 +1,11 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration23To24 : Migration(23, 24) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("CREATE TABLE IF NOT EXISTS `chapters` (`chapter_id` INTEGER NOT NULL, `manga_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `number` REAL NOT NULL, `volume` INTEGER NOT NULL, `url` TEXT NOT NULL, `scanlator` TEXT, `upload_date` INTEGER NOT NULL, `branch` TEXT, `source` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`manga_id`, `chapter_id`), FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
}
}

@ -71,8 +71,13 @@ class MangaDataRepository @Inject constructor(
.distinctUntilChanged() .distinctUntilChanged()
} }
suspend fun findMangaById(mangaId: Long): Manga? { suspend fun findMangaById(mangaId: Long, withChapters: Boolean): Manga? {
return db.getMangaDao().find(mangaId)?.toManga() val chapters = if (withChapters) {
db.getChaptersDao().findAll(mangaId).takeUnless { it.isEmpty() }
} else {
null
}
return db.getMangaDao().find(mangaId)?.toManga(chapters)
} }
suspend fun findMangaByPublicUrl(publicUrl: String): Manga? { suspend fun findMangaByPublicUrl(publicUrl: String): Manga? {
@ -81,7 +86,7 @@ class MangaDataRepository @Inject constructor(
suspend fun resolveIntent(intent: MangaIntent): Manga? = when { suspend fun resolveIntent(intent: MangaIntent): Manga? = when {
intent.manga != null -> intent.manga intent.manga != null -> intent.manga
intent.mangaId != 0L -> findMangaById(intent.mangaId) intent.mangaId != 0L -> findMangaById(intent.mangaId, true)
intent.uri != null -> resolverProvider.get().resolve(intent.uri) intent.uri != null -> resolverProvider.get().resolve(intent.uri)
else -> null else -> null
} }
@ -98,10 +103,26 @@ class MangaDataRepository @Inject constructor(
val tags = manga.tags.toEntities() val tags = manga.tags.toEntities()
db.getTagsDao().upsert(tags) db.getTagsDao().upsert(tags)
db.getMangaDao().upsert(manga.toEntity(), tags) db.getMangaDao().upsert(manga.toEntity(), tags)
if (!manga.isLocal) {
manga.chapters?.let { chapters ->
db.getChaptersDao().replaceAll(manga.id, chapters.withIndex().toEntities(manga.id))
}
}
} }
} }
} }
suspend fun updateChapters(manga: Manga) {
val chapters = manga.chapters
if (!chapters.isNullOrEmpty() && manga.id in db.getMangaDao()) {
db.getChaptersDao().replaceAll(manga.id, chapters.withIndex().toEntities(manga.id))
}
}
suspend fun gcChapters() {
db.getChaptersDao().gc()
}
suspend fun findTags(source: MangaSource): Set<MangaTag> { suspend fun findTags(source: MangaSource): Set<MangaTag> {
return db.getTagsDao().findTags(source.name).toMangaTags() return db.getTagsDao().findTags(source.name).toMangaTags()
} }

@ -43,7 +43,14 @@ class DetailsLoadUseCase @Inject constructor(
operator fun invoke(intent: MangaIntent): Flow<MangaDetails> = channelFlow { operator fun invoke(intent: MangaIntent): Flow<MangaDetails> = channelFlow {
val manga = requireNotNull(mangaDataRepository.resolveIntent(intent)) { val manga = requireNotNull(mangaDataRepository.resolveIntent(intent)) {
"Cannot resolve intent $intent" "Cannot resolve intent $intent"
}.let { m ->
if (m.chapters.isNullOrEmpty()) {
getCachedDetails(m.id) ?: m
} else {
m
}
} }
send(MangaDetails(manga, null, null, false))
val local = if (!manga.isLocal) { val local = if (!manga.isLocal) {
async { async {
localMangaRepository.findSavedManga(manga) localMangaRepository.findSavedManga(manga)
@ -51,9 +58,9 @@ class DetailsLoadUseCase @Inject constructor(
} else { } else {
null null
} }
send(MangaDetails(manga, null, null, false))
try { try {
val details = getDetails(manga) val details = getDetails(manga)
launch { mangaDataRepository.updateChapters(details) }
launch { updateTracker(details) } launch { updateTracker(details) }
send( send(
MangaDetails( MangaDetails(
@ -122,4 +129,8 @@ class DetailsLoadUseCase @Inject constructor(
}.onFailure { e -> }.onFailure { e ->
e.printStackTraceDebug() e.printStackTraceDebug()
} }
private suspend fun getCachedDetails(mangaId: Long): Manga? = runCatchingCancellable {
mangaDataRepository.findMangaById(mangaId, withChapters = true)
}.getOrNull()
} }

@ -226,14 +226,15 @@ class DetailsViewModel @Inject constructor(
private fun doLoad() = launchLoadingJob(Dispatchers.Default) { private fun doLoad() = launchLoadingJob(Dispatchers.Default) {
detailsLoadUseCase.invoke(intent) detailsLoadUseCase.invoke(intent)
.onEachWhile { .onEachWhile {
if (it.allChapters.isEmpty()) { if (it.allChapters.isNotEmpty()) {
return@onEachWhile false val manga = it.toManga()
// find default branch
val hist = historyRepository.getOne(manga)
selectedBranch.value = manga.getPreferredBranch(hist)
true
} else {
false
} }
val manga = it.toManga()
// find default branch
val hist = historyRepository.getOne(manga)
selectedBranch.value = manga.getPreferredBranch(hist)
true
}.collect { }.collect {
mangaDetails.value = it mangaDetails.value = it
} }

@ -289,7 +289,7 @@ class DownloadsViewModel @Inject constructor(
} }
return cacheMutex.withLock { return cacheMutex.withLock {
mangaCache.getOrElse(mangaId) { mangaCache.getOrElse(mangaId) {
mangaDataRepository.findMangaById(mangaId)?.also { mangaDataRepository.findMangaById(mangaId, withChapters = true)?.also {
mangaCache[mangaId] = it mangaCache[mangaId] = it
} ?: return null } ?: return null
} }

@ -120,7 +120,7 @@ class DownloadWorker @AssistedInject constructor(
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
setForeground(getForegroundInfo()) setForeground(getForegroundInfo())
val manga = mangaDataRepository.findMangaById(task.mangaId) ?: return Result.failure() val manga = mangaDataRepository.findMangaById(task.mangaId, withChapters = true) ?: return Result.failure()
publishState(DownloadState(manga = manga, isIndeterminate = true).also { lastPublishedState = it }) publishState(DownloadState(manga = manga, isIndeterminate = true).also { lastPublishedState = it })
val downloadedIds = getDoneChapters(manga) val downloadedIds = getDoneChapters(manga)
return try { return try {

@ -16,6 +16,6 @@ fun FavouriteCategoryEntity.toFavouriteCategory(id: Long = categoryId.toLong())
isVisibleInLibrary = isVisibleInLibrary, isVisibleInLibrary = isVisibleInLibrary,
) )
fun FavouriteManga.toManga() = manga.toManga(tags.toMangaTags()) fun FavouriteManga.toManga() = manga.toManga(tags.toMangaTags(), null)
fun Collection<FavouriteManga>.toMangaList() = map { it.toManga() } fun Collection<FavouriteManga>.toMangaList() = map { it.toManga() }

@ -199,6 +199,7 @@ class FavouritesRepository @Inject constructor(
db.getFavouritesDao().deleteAll(id) db.getFavouritesDao().deleteAll(id)
db.getFavouriteCategoriesDao().delete(id) db.getFavouriteCategoriesDao().delete(id)
} }
db.getChaptersDao().gc()
} }
} }
@ -238,6 +239,7 @@ class FavouritesRepository @Inject constructor(
for (id in ids) { for (id in ids) {
db.getFavouritesDao().delete(mangaId = id) db.getFavouritesDao().delete(mangaId = id)
} }
db.getChaptersDao().gc()
} }
return ReversibleHandle { recoverToFavourites(ids) } return ReversibleHandle { recoverToFavourites(ids) }
} }
@ -247,6 +249,7 @@ class FavouritesRepository @Inject constructor(
for (id in ids) { for (id in ids) {
db.getFavouritesDao().delete(categoryId = categoryId, mangaId = id) db.getFavouritesDao().delete(categoryId = categoryId, mangaId = id)
} }
db.getChaptersDao().gc()
} }
return ReversibleHandle { recoverToCategory(categoryId, ids) } return ReversibleHandle { recoverToCategory(categoryId, ids) }
} }

@ -32,7 +32,7 @@ class LocalFavoritesObserver @Inject constructor(
limit: Int limit: Int
): Flow<List<Manga>> = db.getFavouritesDao().observeAll(categoryId, order, filterOptions, limit).mapToLocal() ): Flow<List<Manga>> = db.getFavouritesDao().observeAll(categoryId, order, filterOptions, limit).mapToLocal()
override fun toManga(e: FavouriteManga) = e.manga.toManga(e.tags.toMangaTags()) override fun toManga(e: FavouriteManga) = e.manga.toManga(e.tags.toMangaTags(), null)
override fun toResult(e: FavouriteManga, manga: Manga) = manga override fun toResult(e: FavouriteManga, manga: Manga) = manga
} }

@ -24,7 +24,7 @@ class HistoryLocalObserver @Inject constructor(
limit: Int limit: Int
) = db.getHistoryDao().observeAll(order, filterOptions, limit).mapToLocal() ) = db.getHistoryDao().observeAll(order, filterOptions, limit).mapToLocal()
override fun toManga(e: HistoryWithManga) = e.manga.toManga(e.tags.toMangaTags()) override fun toManga(e: HistoryWithManga) = e.manga.toManga(e.tags.toMangaTags(), null)
override fun toResult(e: HistoryWithManga, manga: Manga) = MangaWithHistory( override fun toResult(e: HistoryWithManga, manga: Manga) = MangaWithHistory(
manga = manga, manga = manga,

@ -49,7 +49,7 @@ class HistoryRepository @Inject constructor(
suspend fun getList(offset: Int, limit: Int): List<Manga> { suspend fun getList(offset: Int, limit: Int): List<Manga> {
val entities = db.getHistoryDao().findAll(offset, limit) val entities = db.getHistoryDao().findAll(offset, limit)
return entities.map { it.manga.toManga(it.tags.toMangaTags()) } return entities.map { it.toManga() }
} }
suspend fun search(query: String, limit: Int): List<Manga> { suspend fun search(query: String, limit: Int): List<Manga> {
@ -63,25 +63,25 @@ class HistoryRepository @Inject constructor(
suspend fun getLastOrNull(): Manga? { suspend fun getLastOrNull(): Manga? {
val entity = db.getHistoryDao().findAll(0, 1).firstOrNull() ?: return null val entity = db.getHistoryDao().findAll(0, 1).firstOrNull() ?: return null
return entity.manga.toManga(entity.tags.toMangaTags()) return entity.toManga()
} }
fun observeLast(): Flow<Manga?> { fun observeLast(): Flow<Manga?> {
return db.getHistoryDao().observeAll(1).map { return db.getHistoryDao().observeAll(1).map {
val first = it.firstOrNull() val first = it.firstOrNull()
first?.manga?.toManga(first.tags.toMangaTags()) first?.toManga()
} }
} }
fun observeAll(): Flow<List<Manga>> { fun observeAll(): Flow<List<Manga>> {
return db.getHistoryDao().observeAll().mapItems { return db.getHistoryDao().observeAll().mapItems {
it.manga.toManga(it.tags.toMangaTags()) it.toManga()
} }
} }
fun observeAll(limit: Int): Flow<List<Manga>> { fun observeAll(limit: Int): Flow<List<Manga>> {
return db.getHistoryDao().observeAll(limit).mapItems { return db.getHistoryDao().observeAll(limit).mapItems {
it.manga.toManga(it.tags.toMangaTags()) it.toManga()
} }
} }
@ -95,7 +95,7 @@ class HistoryRepository @Inject constructor(
} }
return db.getHistoryDao().observeAll(order, filterOptions, limit).mapItems { return db.getHistoryDao().observeAll(order, filterOptions, limit).mapItems {
MangaWithHistory( MangaWithHistory(
it.manga.toManga(it.tags.toMangaTags()), it.toManga(),
it.history.toMangaHistory(), it.history.toMangaHistory(),
) )
} }
@ -156,16 +156,19 @@ class HistoryRepository @Inject constructor(
db.getHistoryDao().clear() db.getHistoryDao().clear()
} }
suspend fun delete(manga: Manga) { suspend fun delete(manga: Manga) = db.withTransaction {
db.getHistoryDao().delete(manga.id) db.getHistoryDao().delete(manga.id)
mangaRepository.gcChapters()
} }
suspend fun deleteAfter(minDate: Long) { suspend fun deleteAfter(minDate: Long) = db.withTransaction {
db.getHistoryDao().deleteAfter(minDate) db.getHistoryDao().deleteAfter(minDate)
mangaRepository.gcChapters()
} }
suspend fun deleteNotFavorite() { suspend fun deleteNotFavorite() = db.withTransaction {
db.getHistoryDao().deleteNotFavorite() db.getHistoryDao().deleteNotFavorite()
mangaRepository.gcChapters()
} }
suspend fun delete(ids: Collection<Long>): ReversibleHandle { suspend fun delete(ids: Collection<Long>): ReversibleHandle {
@ -173,6 +176,7 @@ class HistoryRepository @Inject constructor(
for (id in ids) { for (id in ids) {
db.getHistoryDao().delete(id) db.getHistoryDao().delete(id)
} }
mangaRepository.gcChapters()
} }
return ReversibleHandle { return ReversibleHandle {
recover(ids) recover(ids)
@ -185,7 +189,7 @@ class HistoryRepository @Inject constructor(
*/ */
suspend fun deleteOrSwap(manga: Manga, alternative: Manga?) { suspend fun deleteOrSwap(manga: Manga, alternative: Manga?) {
if (alternative == null || db.getMangaDao().update(alternative.toEntity()) <= 0) { if (alternative == null || db.getMangaDao().update(alternative.toEntity()) <= 0) {
db.getHistoryDao().delete(manga.id) delete(manga)
} }
} }
@ -229,4 +233,6 @@ class HistoryRepository @Inject constructor(
db.getHistoryDao().update(newEntity) db.getHistoryDao().update(newEntity)
return newEntity return newEntity
} }
private fun HistoryWithManga.toManga() = manga.toManga(tags.toMangaTags(), null)
} }

@ -32,17 +32,17 @@ class CoverRestoreInterceptor @Inject constructor(
val result = chain.proceed() val result = chain.proceed()
if (result is ErrorResult && result.throwable.shouldRestore()) { if (result is ErrorResult && result.throwable.shouldRestore()) {
request.extras[mangaKey]?.let { request.extras[mangaKey]?.let {
if (restoreManga(it)) { return if (restoreManga(it)) {
return chain.withRequest(request.newBuilder().build()).proceed() chain.withRequest(request.newBuilder().build()).proceed()
} else { } else {
return result result
} }
} }
request.extras[bookmarkKey]?.let { request.extras[bookmarkKey]?.let {
if (restoreBookmark(it)) { return if (restoreBookmark(it)) {
return chain.withRequest(request.newBuilder().build()).proceed() chain.withRequest(request.newBuilder().build()).proceed()
} else { } else {
return result result
} }
} }
} }
@ -66,7 +66,7 @@ class CoverRestoreInterceptor @Inject constructor(
} }
private suspend fun restoreMangaImpl(manga: Manga): Boolean { private suspend fun restoreMangaImpl(manga: Manga): Boolean {
if (dataRepository.findMangaById(manga.id) == null || manga.isLocal) { if (dataRepository.findMangaById(manga.id, withChapters = false) == null || manga.isLocal) {
return false return false
} }
val repo = repositoryFactory.create(manga.source) val repo = repositoryFactory.create(manga.source)

@ -33,7 +33,7 @@ class StatsRepository @Inject constructor(
var other = StatsRecord(null, 0) var other = StatsRecord(null, 0)
val total = stats.values.sum() val total = stats.values.sum()
for ((mangaEntity, duration) in stats) { for ((mangaEntity, duration) in stats) {
val manga = mangaEntity.toManga(emptySet()) val manga = mangaEntity.toManga(emptySet(), null)
val percent = duration.toDouble() / total val percent = duration.toDouble() / total
if (percent < 0.05) { if (percent < 0.05) {
other = other.copy(duration = other.duration + duration) other = other.copy(duration = other.duration + duration)

@ -15,6 +15,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.suggestions.data.SuggestionEntity import org.koitharu.kotatsu.suggestions.data.SuggestionEntity
import org.koitharu.kotatsu.suggestions.data.SuggestionWithManga
import javax.inject.Inject import javax.inject.Inject
class SuggestionRepository @Inject constructor( class SuggestionRepository @Inject constructor(
@ -23,25 +24,23 @@ class SuggestionRepository @Inject constructor(
fun observeAll(): Flow<List<Manga>> { fun observeAll(): Flow<List<Manga>> {
return db.getSuggestionDao().observeAll().mapItems { return db.getSuggestionDao().observeAll().mapItems {
it.manga.toManga(it.tags.toMangaTags()) it.toManga()
} }
} }
fun observeAll(limit: Int, filterOptions: Set<ListFilterOption>): Flow<List<Manga>> { fun observeAll(limit: Int, filterOptions: Set<ListFilterOption>): Flow<List<Manga>> {
return db.getSuggestionDao().observeAll(limit, filterOptions).mapItems { return db.getSuggestionDao().observeAll(limit, filterOptions).mapItems {
it.manga.toManga(it.tags.toMangaTags()) it.toManga()
} }
} }
suspend fun getRandom(): Manga? { suspend fun getRandom(): Manga? {
return db.getSuggestionDao().getRandom()?.let { return db.getSuggestionDao().getRandom()?.toManga()
it.manga.toManga(it.tags.toMangaTags())
}
} }
suspend fun getRandomList(limit: Int): List<Manga> { suspend fun getRandomList(limit: Int): List<Manga> {
return db.getSuggestionDao().getRandom(limit).map { return db.getSuggestionDao().getRandom(limit).map {
it.manga.toManga(it.tags.toMangaTags()) it.toManga()
} }
} }
@ -80,4 +79,6 @@ class SuggestionRepository @Inject constructor(
} }
} }
} }
private fun SuggestionWithManga.toManga() = manga.toManga(tags.toMangaTags(), null)
} }

@ -10,7 +10,7 @@ fun TrackLogWithManga.toTrackingLogItem(): TrackingLogItem {
return TrackingLogItem( return TrackingLogItem(
id = trackLog.id, id = trackLog.id,
chapters = chaptersList, chapters = chaptersList,
manga = manga.toManga(tags.toMangaTags()), manga = manga.toManga(tags.toMangaTags(), null),
createdAt = Instant.ofEpochMilli(trackLog.createdAt), createdAt = Instant.ofEpochMilli(trackLog.createdAt),
isNew = trackLog.isUnread, isNew = trackLog.isUnread,
) )

@ -60,7 +60,7 @@ class TrackingRepository @Inject constructor(
return db.getTracksDao().observeUpdatedManga(limit, filterOptions) return db.getTracksDao().observeUpdatedManga(limit, filterOptions)
.mapItems { .mapItems {
MangaTracking( MangaTracking(
manga = it.manga.toManga(it.tags.toMangaTags()), manga = it.manga.toManga(it.tags.toMangaTags(), null),
lastChapterId = it.track.lastChapterId, lastChapterId = it.track.lastChapterId,
lastCheck = it.track.lastCheckTime.toInstantOrNull(), lastCheck = it.track.lastCheckTime.toInstantOrNull(),
lastChapterDate = it.track.lastChapterDate.toInstantOrNull(), lastChapterDate = it.track.lastChapterDate.toInstantOrNull(),
@ -73,7 +73,7 @@ class TrackingRepository @Inject constructor(
suspend fun getTracks(offset: Int, limit: Int): List<MangaTracking> { suspend fun getTracks(offset: Int, limit: Int): List<MangaTracking> {
return db.getTracksDao().findAll(offset = offset, limit = limit).map { return db.getTracksDao().findAll(offset = offset, limit = limit).map {
MangaTracking( MangaTracking(
manga = it.manga.toManga(emptySet()), manga = it.manga.toManga(emptySet(), null),
lastChapterId = it.track.lastChapterId, lastChapterId = it.track.lastChapterId,
lastCheck = it.track.lastCheckTime.toInstantOrNull(), lastCheck = it.track.lastCheckTime.toInstantOrNull(),
lastChapterDate = it.track.lastChapterDate.toInstantOrNull(), lastChapterDate = it.track.lastChapterDate.toInstantOrNull(),

@ -25,7 +25,7 @@ class TrackerDebugViewModel @Inject constructor(
private fun List<TrackWithManga>.toUiList(): List<TrackDebugItem> = map { private fun List<TrackWithManga>.toUiList(): List<TrackDebugItem> = map {
TrackDebugItem( TrackDebugItem(
manga = it.manga.toManga(emptySet()), manga = it.manga.toManga(emptySet(), null),
lastChapterId = it.track.lastChapterId, lastChapterId = it.track.lastChapterId,
newChapters = it.track.newChapters, newChapters = it.track.newChapters,
lastCheckTime = it.track.lastCheckTime.toInstantOrNull(), lastCheckTime = it.track.lastCheckTime.toInstantOrNull(),

@ -43,7 +43,7 @@ transition = "1.5.1"
viewpager2 = "1.1.0" viewpager2 = "1.1.0"
webkit = "1.12.1" webkit = "1.12.1"
workRuntime = "2.10.0" workRuntime = "2.10.0"
workinspector = "1.0" workinspector = "1.2"
[libraries] [libraries]
acra-dialog = { module = "ch.acra:acra-dialog", version.ref = "acra" } acra-dialog = { module = "ch.acra:acra-dialog", version.ref = "acra" }

Loading…
Cancel
Save