diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/MangaTypeMapping.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/MangaTypeMapping.kt index c1645b9050..a45e6af26c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/MangaTypeMapping.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/MangaTypeMapping.kt @@ -70,7 +70,7 @@ class MangaPutResolver : DefaultPutResolver() { put(COL_CHAPTER_FLAGS, obj.chapter_flags) put(COL_DATE_ADDED, obj.date_added) put(COL_FILTERED_SCANLATORS, obj.filtered_scanlators) - put(COL_UPDATE_STRATEGY, obj.update_strategy.let(updateStrategyAdapter::encode)) + put(COL_UPDATE_STRATEGY, obj.update_strategy.let(updateStrategyAdapter::encode).toInt()) } } @@ -94,9 +94,9 @@ interface BaseMangaGetResolver { hide_title = cursor.getInt(cursor.getColumnIndex(COL_HIDE_TITLE)) == 1 date_added = cursor.getLong(cursor.getColumnIndex(COL_DATE_ADDED)) filtered_scanlators = cursor.getString(cursor.getColumnIndex(COL_FILTERED_SCANLATORS)) - update_strategy = cursor.getInt(cursor.getColumnIndex(COL_UPDATE_STRATEGY)).let( - updateStrategyAdapter::decode, - ) + update_strategy = cursor.getInt(cursor.getColumnIndex(COL_UPDATE_STRATEGY)).let { + updateStrategyAdapter.decode(it.toLong()) + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/LibraryManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/LibraryManga.kt index 5d79535cf2..0c0fb29f98 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/LibraryManga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/LibraryManga.kt @@ -77,7 +77,7 @@ data class LibraryManga( this.chapter_flags = chapterFlags.toInt() this.date_added = dateAdded ?: 0L this.filtered_scanlators = filteredScanlators - this.update_strategy = updateStrategy.toInt().let(updateStrategyAdapter::decode) + this.update_strategy = updateStrategy.let(updateStrategyAdapter::decode) this.read = readCount.roundToInt() this.unread = maxOf((total - readCount).roundToInt(), 0) this.totalChapters = readCount.roundToInt() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt index d040ea6f42..ed197846f4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt @@ -419,7 +419,7 @@ interface Manga : SManga { this.hide_title = hideTitle > 0 this.date_added = dateAdded ?: 0L this.filtered_scanlators = filteredScanlators - this.update_strategy = updateStrategy.toInt().let(updateStrategyAdapter::decode) + this.update_strategy = updateStrategy.let(updateStrategyAdapter::decode) } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt index d40d924e89..16dc7449c1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt @@ -91,8 +91,6 @@ interface MangaQueries : DbProvider { fun insertManga(manga: Manga) = db.put().`object`(manga).prepare() - fun insertMangas(mangas: List) = db.put().objects(mangas).prepare() - fun updateChapterFlags(manga: Manga) = db.put() .`object`(manga) .withPutResolver(MangaFlagsPutResolver(MangaTable.COL_CHAPTER_FLAGS, Manga::chapter_flags)) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt index b32885f8f6..991c8f7a95 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt @@ -60,6 +60,8 @@ import uy.kohesive.injekt.injectLazy import yokai.domain.category.interactor.GetCategories import yokai.domain.chapter.interactor.GetChapters import yokai.domain.manga.interactor.GetLibraryManga +import yokai.domain.manga.interactor.UpdateManga +import yokai.domain.manga.models.MangaUpdate import yokai.util.isLewd import java.util.* import java.util.concurrent.* @@ -86,6 +88,7 @@ class LibraryPresenter( private val getCategories: GetCategories by injectLazy() private val getLibraryManga: GetLibraryManga by injectLazy() private val getChapters: GetChapters by injectLazy() + private val updateManga: UpdateManga by injectLazy() private val libraryManga: List = emptyList() @@ -1131,9 +1134,9 @@ class LibraryPresenter( presenterScope.launch { // Create a set of the list val mangaToDelete = mangas.distinctBy { it.id } - mangaToDelete.forEach { it.favorite = false } + .mapNotNull { if (it.id != null) MangaUpdate(it.id!!, favorite = false) else null } - db.insertMangas(mangaToDelete).executeOnIO() + withIOContext { updateManga.updateAll(mangaToDelete) } getLibrary() } } @@ -1168,11 +1171,11 @@ class LibraryPresenter( fun reAddMangas(mangas: List) { presenterScope.launch { val mangaToAdd = mangas.distinctBy { it.id } - mangaToAdd.forEach { it.favorite = true } - db.insertMangas(mangaToAdd).executeOnIO() + .mapNotNull { if (it.id != null) MangaUpdate(it.id!!, favorite = true) else null } + + withIOContext { updateManga.updateAll(mangaToAdd) } (view as? FilteredLibraryController)?.updateStatsPage() getLibrary() - mangaToAdd.forEach { db.insertManga(it).executeAsBlocking() } } } diff --git a/app/src/main/java/yokai/core/di/DomainModule.kt b/app/src/main/java/yokai/core/di/DomainModule.kt index a39dabc8ad..06117fe46f 100644 --- a/app/src/main/java/yokai/core/di/DomainModule.kt +++ b/app/src/main/java/yokai/core/di/DomainModule.kt @@ -30,6 +30,8 @@ import yokai.domain.library.custom.interactor.GetCustomManga import yokai.domain.library.custom.interactor.RelinkCustomManga import yokai.domain.manga.MangaRepository import yokai.domain.manga.interactor.GetLibraryManga +import yokai.domain.manga.interactor.InsertManga +import yokai.domain.manga.interactor.UpdateManga class DomainModule : InjektModule { override fun InjektRegistrar.registerInjectables() { @@ -51,6 +53,8 @@ class DomainModule : InjektModule { addSingletonFactory { MangaRepositoryImpl(get()) } addFactory { GetLibraryManga(get()) } + addFactory { InsertManga(get()) } + addFactory { UpdateManga(get()) } addSingletonFactory { ChapterRepositoryImpl(get()) } addFactory { GetAvailableScanlators(get()) } diff --git a/app/src/main/java/yokai/data/manga/MangaRepositoryImpl.kt b/app/src/main/java/yokai/data/manga/MangaRepositoryImpl.kt index 71f190e9ff..65c88d485b 100644 --- a/app/src/main/java/yokai/data/manga/MangaRepositoryImpl.kt +++ b/app/src/main/java/yokai/data/manga/MangaRepositoryImpl.kt @@ -1,10 +1,14 @@ package yokai.data.manga +import co.touchlab.kermit.Logger import eu.kanade.tachiyomi.data.database.models.LibraryManga import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.util.system.toInt import kotlinx.coroutines.flow.Flow import yokai.data.DatabaseHandler +import yokai.data.updateStrategyAdapter import yokai.domain.manga.MangaRepository +import yokai.domain.manga.models.MangaUpdate class MangaRepositoryImpl(private val handler: DatabaseHandler) : MangaRepository { override suspend fun getManga(): List = @@ -18,4 +22,77 @@ class MangaRepositoryImpl(private val handler: DatabaseHandler) : MangaRepositor override fun getLibraryMangaAsFlow(): Flow> = handler.subscribeToList { library_viewQueries.findAll(LibraryManga::mapper) } + + override suspend fun update(update: MangaUpdate): Boolean { + return try { + partialUpdate(update) + true + } catch (e: Exception) { + Logger.e { "Failed to update manga with id '${update.id}'" } + false + } + } + + override suspend fun updateAll(updates: List): Boolean { + return try { + partialUpdate(*updates.toTypedArray()) + true + } catch (e: Exception) { + Logger.e(e) { "Failed to bulk update manga" } + false + } + } + + private suspend fun partialUpdate(vararg updates: MangaUpdate) { + handler.await(inTransaction = true) { + updates.forEach { update -> + mangasQueries.update( + source = update.source, + url = update.url, + artist = update.artist, + author = update.author, + description = update.description, + genre = update.genres?.joinToString(), + title = update.title, + status = update.status?.toLong(), + thumbnailUrl = update.thumbnailUrl, + favorite = update.favorite?.toInt()?.toLong(), + lastUpdate = update.lastUpdate, + initialized = update.initialized, + viewer = update.viewerFlags?.toLong(), + hideTitle = update.hideTitle?.toInt()?.toLong(), + chapterFlags = update.chapterFlags?.toLong(), + dateAdded = update.dateAdded, + filteredScanlators = update.filteredScanlators, + updateStrategy = update.updateStrategy?.let(updateStrategyAdapter::encode), + mangaId = update.id, + ) + } + } + } + + override suspend fun insert(manga: Manga) = + handler.awaitOneOrNullExecutable(inTransaction = true) { + mangasQueries.insert( + source = manga.source, + url = manga.url, + artist = manga.artist, + author = manga.author, + description = manga.description, + genre = manga.genre, + title = manga.title, + status = manga.status.toLong(), + thumbnailUrl = manga.thumbnail_url, + favorite = manga.favorite.toInt().toLong(), + lastUpdate = manga.last_update, + initialized = manga.initialized, + viewer = manga.viewer_flags.toLong(), + hideTitle = manga.hide_title.toInt().toLong(), + chapterFlags = manga.chapter_flags.toLong(), + dateAdded = manga.date_added, + filteredScanlators = manga.filtered_scanlators, + updateStrategy = manga.update_strategy.let(updateStrategyAdapter::encode), + ) + mangasQueries.selectLastInsertedRowId() + } } diff --git a/app/src/main/java/yokai/domain/manga/MangaRepository.kt b/app/src/main/java/yokai/domain/manga/MangaRepository.kt index 24f67af320..6388153d27 100644 --- a/app/src/main/java/yokai/domain/manga/MangaRepository.kt +++ b/app/src/main/java/yokai/domain/manga/MangaRepository.kt @@ -3,10 +3,14 @@ package yokai.domain.manga import eu.kanade.tachiyomi.data.database.models.LibraryManga import eu.kanade.tachiyomi.data.database.models.Manga import kotlinx.coroutines.flow.Flow +import yokai.domain.manga.models.MangaUpdate interface MangaRepository { suspend fun getManga(): List fun getMangaAsFlow(): Flow> suspend fun getLibraryManga(): List fun getLibraryMangaAsFlow(): Flow> + suspend fun update(update: MangaUpdate): Boolean + suspend fun updateAll(updates: List): Boolean + suspend fun insert(manga: Manga): Long? } diff --git a/app/src/main/java/yokai/domain/manga/interactor/InsertManga.kt b/app/src/main/java/yokai/domain/manga/interactor/InsertManga.kt new file mode 100644 index 0000000000..255771fdcd --- /dev/null +++ b/app/src/main/java/yokai/domain/manga/interactor/InsertManga.kt @@ -0,0 +1,10 @@ +package yokai.domain.manga.interactor + +import eu.kanade.tachiyomi.data.database.models.Manga +import yokai.domain.manga.MangaRepository + +class InsertManga ( + private val mangaRepository: MangaRepository, +) { + suspend fun await(manga: Manga) = mangaRepository.insert(manga) +} diff --git a/app/src/main/java/yokai/domain/manga/interactor/UpdateManga.kt b/app/src/main/java/yokai/domain/manga/interactor/UpdateManga.kt new file mode 100644 index 0000000000..310206e517 --- /dev/null +++ b/app/src/main/java/yokai/domain/manga/interactor/UpdateManga.kt @@ -0,0 +1,11 @@ +package yokai.domain.manga.interactor + +import yokai.domain.manga.MangaRepository +import yokai.domain.manga.models.MangaUpdate + +class UpdateManga ( + private val mangaRepository: MangaRepository, +) { + suspend fun update(update: MangaUpdate) = mangaRepository.update(update) + suspend fun updateAll(updates: List) = mangaRepository.updateAll(updates) +} diff --git a/data/src/androidMain/kotlin/yokai/data/AndroidDatabaseHandler.kt b/data/src/androidMain/kotlin/yokai/data/AndroidDatabaseHandler.kt index c32259d91b..977a065c66 100644 --- a/data/src/androidMain/kotlin/yokai/data/AndroidDatabaseHandler.kt +++ b/data/src/androidMain/kotlin/yokai/data/AndroidDatabaseHandler.kt @@ -1,5 +1,6 @@ package yokai.data +import app.cash.sqldelight.ExecutableQuery import app.cash.sqldelight.Query import app.cash.sqldelight.coroutines.asFlow import app.cash.sqldelight.coroutines.mapToList @@ -38,6 +39,13 @@ class AndroidDatabaseHandler( return dispatch(inTransaction) { block(db).executeAsOne() } } + override suspend fun awaitOneExecutable( + inTransaction: Boolean, + block: suspend Database.() -> ExecutableQuery, + ): T { + return dispatch(inTransaction) { block(db).executeAsOne() } + } + override suspend fun awaitOneOrNull( inTransaction: Boolean, block: suspend Database.() -> Query @@ -45,6 +53,13 @@ class AndroidDatabaseHandler( return dispatch(inTransaction) { block(db).executeAsOneOrNull() } } + override suspend fun awaitOneOrNullExecutable( + inTransaction: Boolean, + block: suspend Database.() -> ExecutableQuery, + ): T? { + return dispatch(inTransaction) { block(db).executeAsOneOrNull() } + } + override fun subscribeToList(block: Database.() -> Query): Flow> { return block(db).asFlow().mapToList(queryDispatcher) } diff --git a/data/src/commonMain/kotlin/yokai/data/DatabaseAdapter.kt b/data/src/commonMain/kotlin/yokai/data/DatabaseAdapter.kt index 1e9660278f..ebbe1d45fb 100644 --- a/data/src/commonMain/kotlin/yokai/data/DatabaseAdapter.kt +++ b/data/src/commonMain/kotlin/yokai/data/DatabaseAdapter.kt @@ -6,13 +6,13 @@ import java.util.* // TODO: Move to yokai.data.DatabaseAdapter -val updateStrategyAdapter = object : ColumnAdapter { +val updateStrategyAdapter = object : ColumnAdapter { private val enumValues by lazy { UpdateStrategy.entries } - override fun decode(databaseValue: Int): UpdateStrategy = - enumValues.getOrElse(databaseValue) { UpdateStrategy.ALWAYS_UPDATE } + override fun decode(databaseValue: Long): UpdateStrategy = + enumValues.getOrElse(databaseValue.toInt()) { UpdateStrategy.ALWAYS_UPDATE } - override fun encode(value: UpdateStrategy): Int = value.ordinal + override fun encode(value: UpdateStrategy): Long = value.ordinal.toLong() } val dateAdapter = object : ColumnAdapter { diff --git a/data/src/commonMain/kotlin/yokai/data/DatabaseHandler.kt b/data/src/commonMain/kotlin/yokai/data/DatabaseHandler.kt index 46c04660df..bc7c145d98 100644 --- a/data/src/commonMain/kotlin/yokai/data/DatabaseHandler.kt +++ b/data/src/commonMain/kotlin/yokai/data/DatabaseHandler.kt @@ -1,5 +1,6 @@ package yokai.data +import app.cash.sqldelight.ExecutableQuery import app.cash.sqldelight.Query import kotlinx.coroutines.flow.Flow @@ -16,11 +17,21 @@ interface DatabaseHandler { block: suspend Database.() -> Query ): T + suspend fun awaitOneExecutable( + inTransaction: Boolean = false, + block: suspend Database.() -> ExecutableQuery, + ): T + suspend fun awaitOneOrNull( inTransaction: Boolean = false, block: suspend Database.() -> Query ): T? + suspend fun awaitOneOrNullExecutable( + inTransaction: Boolean = false, + block: suspend Database.() -> ExecutableQuery, + ): T? + fun subscribeToList(block: Database.() -> Query): Flow> fun subscribeToOne(block: Database.() -> Query): Flow diff --git a/data/src/commonMain/sqldelight/tachiyomi/data/mangas.sq b/data/src/commonMain/sqldelight/tachiyomi/data/mangas.sq index b9473301be..171b2ac5bf 100644 --- a/data/src/commonMain/sqldelight/tachiyomi/data/mangas.sq +++ b/data/src/commonMain/sqldelight/tachiyomi/data/mangas.sq @@ -29,3 +29,32 @@ CREATE INDEX library_favorite_index ON mangas(favorite) WHERE favorite = 1; findAll: SELECT * FROM mangas; + +insert: +INSERT INTO mangas (source, url, artist, author, description, genre, title, status, thumbnail_url, favorite, last_update, initialized, viewer, hide_title, chapter_flags, date_added, filtered_scanlators, update_strategy) +VALUES (:source, :url, :artist, :author, :description, :genre, :title, :status, :thumbnailUrl, :favorite, :lastUpdate, :initialized, :viewer, :hideTitle, :chapterFlags, :dateAdded, :filteredScanlators, :updateStrategy); + +update: +UPDATE mangas SET + source = coalesce(:source, source), + url = coalesce(:url, url), + artist = coalesce(:artist, artist), + author = coalesce(:author, author), + description = coalesce(:description, description), + genre = coalesce(:genre, genre), + title = coalesce(:title, title), + status = coalesce(:status, status), + thumbnail_url = coalesce(:thumbnailUrl, thumbnail_url), + favorite = coalesce(:favorite, favorite), + last_update = coalesce(:lastUpdate, last_update), + initialized = coalesce(:initialized, initialized), + viewer = coalesce(:viewer, viewer), + hide_title = coalesce(:hideTitle, hide_title), + chapter_flags = coalesce(:chapterFlags, chapter_flags), + date_added = coalesce(:dateAdded, date_added), + filtered_scanlators = coalesce(:filteredScanlators, filtered_scanlators), + update_strategy = coalesce(:updateStrategy, update_strategy) +WHERE _id = :mangaId; + +selectLastInsertedRowId: +SELECT last_insert_rowid(); diff --git a/domain/src/commonMain/kotlin/yokai/domain/manga/models/MangaUpdate.kt b/domain/src/commonMain/kotlin/yokai/domain/manga/models/MangaUpdate.kt new file mode 100644 index 0000000000..b8852c5080 --- /dev/null +++ b/domain/src/commonMain/kotlin/yokai/domain/manga/models/MangaUpdate.kt @@ -0,0 +1,25 @@ +package yokai.domain.manga.models + +import eu.kanade.tachiyomi.source.model.UpdateStrategy + +data class MangaUpdate( + val id: Long, + val url: String? = null, + val title: String? = null, + val artist: String? = null, + val author: String? = null, + val description: String? = null, + val genres: List? = null, + val status: Int? = null, + val thumbnailUrl: String? = null, + val updateStrategy: UpdateStrategy? = null, + val initialized: Boolean? = null, + var source: Long? = null, + var favorite: Boolean? = null, + var lastUpdate: Long? = null, + var dateAdded: Long? = null, + var viewerFlags: Int? = null, + var chapterFlags: Int? = null, + var hideTitle: Boolean? = null, + var filteredScanlators: String? = null, +)