Update sqldelight-androidx-driver and remove transaction management (#3270)
`sqldelight-androidx-driver` has it's own transaction management
This commit is contained in:
@@ -203,6 +203,8 @@ dependencies {
|
|||||||
|
|
||||||
implementation(libs.bundles.kotlinx.coroutines)
|
implementation(libs.bundles.kotlinx.coroutines)
|
||||||
|
|
||||||
|
implementation(libs.sqldelight.async)
|
||||||
|
|
||||||
// AndroidX libraries
|
// AndroidX libraries
|
||||||
implementation(libs.androidx.annotation)
|
implementation(libs.androidx.annotation)
|
||||||
implementation(libs.androidx.appCompat)
|
implementation(libs.androidx.appCompat)
|
||||||
|
|||||||
@@ -1,24 +1,26 @@
|
|||||||
package eu.kanade.domain.manga.interactor
|
package eu.kanade.domain.manga.interactor
|
||||||
|
|
||||||
|
import app.cash.sqldelight.async.coroutines.awaitAsList
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import tachiyomi.data.DatabaseHandler
|
import tachiyomi.data.Database
|
||||||
|
import tachiyomi.data.subscribeToList
|
||||||
|
|
||||||
class GetExcludedScanlators(
|
class GetExcludedScanlators(
|
||||||
private val handler: DatabaseHandler,
|
private val database: Database,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun await(mangaId: Long): Set<String> {
|
suspend fun await(mangaId: Long): Set<String> {
|
||||||
return handler.awaitList {
|
return database.excluded_scanlatorsQueries
|
||||||
excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId)
|
.getExcludedScanlatorsByMangaId(mangaId)
|
||||||
}
|
.awaitAsList()
|
||||||
.toSet()
|
.toSet()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun subscribe(mangaId: Long): Flow<Set<String>> {
|
fun subscribe(mangaId: Long): Flow<Set<String>> {
|
||||||
return handler.subscribeToList {
|
return database.excluded_scanlatorsQueries
|
||||||
excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId)
|
.getExcludedScanlatorsByMangaId(mangaId)
|
||||||
}
|
.subscribeToList()
|
||||||
.map { it.toSet() }
|
.map { it.toSet() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
package eu.kanade.domain.manga.interactor
|
package eu.kanade.domain.manga.interactor
|
||||||
|
|
||||||
import tachiyomi.data.DatabaseHandler
|
import app.cash.sqldelight.async.coroutines.awaitAsList
|
||||||
|
import tachiyomi.data.Database
|
||||||
|
|
||||||
class SetExcludedScanlators(
|
class SetExcludedScanlators(
|
||||||
private val handler: DatabaseHandler,
|
private val database: Database,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun await(mangaId: Long, excludedScanlators: Set<String>) {
|
suspend fun await(mangaId: Long, excludedScanlators: Set<String>) {
|
||||||
handler.await(inTransaction = true) {
|
database.transaction {
|
||||||
val currentExcluded = handler.awaitList {
|
val currentExcluded = database.excluded_scanlatorsQueries
|
||||||
excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId)
|
.getExcludedScanlatorsByMangaId(mangaId)
|
||||||
}.toSet()
|
.awaitAsList()
|
||||||
|
.toSet()
|
||||||
val toAdd = excludedScanlators.minus(currentExcluded)
|
val toAdd = excludedScanlators.minus(currentExcluded)
|
||||||
for (scanlator in toAdd) {
|
for (scanlator in toAdd) {
|
||||||
excluded_scanlatorsQueries.insert(mangaId, scanlator)
|
database.excluded_scanlatorsQueries.insert(mangaId, scanlator)
|
||||||
}
|
}
|
||||||
val toRemove = currentExcluded.minus(excludedScanlators)
|
val toRemove = currentExcluded.minus(excludedScanlators)
|
||||||
excluded_scanlatorsQueries.remove(mangaId, toRemove)
|
database.excluded_scanlatorsQueries.remove(mangaId, toRemove)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-10
@@ -1,5 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.create.creators
|
package eu.kanade.tachiyomi.data.backup.create.creators
|
||||||
|
|
||||||
|
import app.cash.sqldelight.async.coroutines.awaitAsList
|
||||||
|
import app.cash.sqldelight.async.coroutines.awaitAsOne
|
||||||
import eu.kanade.tachiyomi.data.backup.create.BackupOptions
|
import eu.kanade.tachiyomi.data.backup.create.BackupOptions
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupChapter
|
import eu.kanade.tachiyomi.data.backup.models.BackupChapter
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
|
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
|
||||||
@@ -7,7 +9,7 @@ import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
|||||||
import eu.kanade.tachiyomi.data.backup.models.backupChapterMapper
|
import eu.kanade.tachiyomi.data.backup.models.backupChapterMapper
|
||||||
import eu.kanade.tachiyomi.data.backup.models.backupTrackMapper
|
import eu.kanade.tachiyomi.data.backup.models.backupTrackMapper
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
||||||
import tachiyomi.data.DatabaseHandler
|
import tachiyomi.data.Database
|
||||||
import tachiyomi.domain.category.interactor.GetCategories
|
import tachiyomi.domain.category.interactor.GetCategories
|
||||||
import tachiyomi.domain.history.interactor.GetHistory
|
import tachiyomi.domain.history.interactor.GetHistory
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
@@ -15,7 +17,7 @@ import uy.kohesive.injekt.Injekt
|
|||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class MangaBackupCreator(
|
class MangaBackupCreator(
|
||||||
private val handler: DatabaseHandler = Injekt.get(),
|
private val database: Database = Injekt.get(),
|
||||||
private val getCategories: GetCategories = Injekt.get(),
|
private val getCategories: GetCategories = Injekt.get(),
|
||||||
private val getHistory: GetHistory = Injekt.get(),
|
private val getHistory: GetHistory = Injekt.get(),
|
||||||
) {
|
) {
|
||||||
@@ -30,19 +32,19 @@ class MangaBackupCreator(
|
|||||||
// Entry for this manga
|
// Entry for this manga
|
||||||
val mangaObject = manga.toBackupManga()
|
val mangaObject = manga.toBackupManga()
|
||||||
|
|
||||||
mangaObject.excludedScanlators = handler.awaitList {
|
mangaObject.excludedScanlators = database.excluded_scanlatorsQueries
|
||||||
excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(manga.id)
|
.getExcludedScanlatorsByMangaId(manga.id)
|
||||||
}
|
.awaitAsList()
|
||||||
|
|
||||||
if (options.chapters) {
|
if (options.chapters) {
|
||||||
// Backup all the chapters
|
// Backup all the chapters
|
||||||
handler.awaitList {
|
database.chaptersQueries
|
||||||
chaptersQueries.getChaptersByMangaId(
|
.getChaptersByMangaId(
|
||||||
mangaId = manga.id,
|
mangaId = manga.id,
|
||||||
applyScanlatorFilter = 0, // false
|
applyScanlatorFilter = 0, // false
|
||||||
mapper = backupChapterMapper,
|
mapper = backupChapterMapper,
|
||||||
)
|
)
|
||||||
}
|
.awaitAsList()
|
||||||
.takeUnless(List<BackupChapter>::isEmpty)
|
.takeUnless(List<BackupChapter>::isEmpty)
|
||||||
?.let { mangaObject.chapters = it }
|
?.let { mangaObject.chapters = it }
|
||||||
}
|
}
|
||||||
@@ -56,7 +58,9 @@ class MangaBackupCreator(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (options.tracking) {
|
if (options.tracking) {
|
||||||
val tracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id, backupTrackMapper) }
|
val tracks = database.manga_syncQueries
|
||||||
|
.getTracksByMangaId(manga.id, backupTrackMapper)
|
||||||
|
.awaitAsList()
|
||||||
if (tracks.isNotEmpty()) {
|
if (tracks.isNotEmpty()) {
|
||||||
mangaObject.tracking = tracks
|
mangaObject.tracking = tracks
|
||||||
}
|
}
|
||||||
@@ -66,7 +70,9 @@ class MangaBackupCreator(
|
|||||||
val historyByMangaId = getHistory.await(manga.id)
|
val historyByMangaId = getHistory.await(manga.id)
|
||||||
if (historyByMangaId.isNotEmpty()) {
|
if (historyByMangaId.isNotEmpty()) {
|
||||||
val history = historyByMangaId.map { history ->
|
val history = historyByMangaId.map { history ->
|
||||||
val chapter = handler.awaitOne { chaptersQueries.getChapterById(history.chapterId) }
|
val chapter = database.chaptersQueries
|
||||||
|
.getChapterById(history.chapterId)
|
||||||
|
.awaitAsOne()
|
||||||
BackupHistory(chapter.url, history.readAt?.time ?: 0L, history.readDuration)
|
BackupHistory(chapter.url, history.readAt?.time ?: 0L, history.readDuration)
|
||||||
}
|
}
|
||||||
if (history.isNotEmpty()) {
|
if (history.isNotEmpty()) {
|
||||||
|
|||||||
+6
-6
@@ -1,14 +1,15 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.restore.restorers
|
package eu.kanade.tachiyomi.data.backup.restore.restorers
|
||||||
|
|
||||||
|
import app.cash.sqldelight.async.coroutines.awaitAsOne
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
||||||
import tachiyomi.data.DatabaseHandler
|
import tachiyomi.data.Database
|
||||||
import tachiyomi.domain.category.interactor.GetCategories
|
import tachiyomi.domain.category.interactor.GetCategories
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class CategoriesRestorer(
|
class CategoriesRestorer(
|
||||||
private val handler: DatabaseHandler = Injekt.get(),
|
private val database: Database = Injekt.get(),
|
||||||
private val getCategories: GetCategories = Injekt.get(),
|
private val getCategories: GetCategories = Injekt.get(),
|
||||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||||
) {
|
) {
|
||||||
@@ -25,10 +26,9 @@ class CategoriesRestorer(
|
|||||||
val dbCategory = dbCategoriesByName[it.name]
|
val dbCategory = dbCategoriesByName[it.name]
|
||||||
if (dbCategory != null) return@map dbCategory
|
if (dbCategory != null) return@map dbCategory
|
||||||
val order = nextOrder++
|
val order = nextOrder++
|
||||||
handler.awaitOneExecutable {
|
database.categoriesQueries
|
||||||
categoriesQueries.insert(it.name, order, it.flags)
|
.insert(it.name, order, it.flags)
|
||||||
categoriesQueries.selectLastInsertedRowId()
|
.awaitAsOne()
|
||||||
}
|
|
||||||
.let { id -> it.toCategory(id).copy(order = order) }
|
.let { id -> it.toCategory(id).copy(order = order) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+9
-11
@@ -2,12 +2,12 @@ package eu.kanade.tachiyomi.data.backup.restore.restorers
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupExtensionRepos
|
import eu.kanade.tachiyomi.data.backup.models.BackupExtensionRepos
|
||||||
import mihon.domain.extensionrepo.interactor.GetExtensionRepo
|
import mihon.domain.extensionrepo.interactor.GetExtensionRepo
|
||||||
import tachiyomi.data.DatabaseHandler
|
import tachiyomi.data.Database
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class ExtensionRepoRestorer(
|
class ExtensionRepoRestorer(
|
||||||
private val handler: DatabaseHandler = Injekt.get(),
|
private val database: Database = Injekt.get(),
|
||||||
private val getExtensionRepos: GetExtensionRepo = Injekt.get(),
|
private val getExtensionRepos: GetExtensionRepo = Injekt.get(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@@ -26,15 +26,13 @@ class ExtensionRepoRestorer(
|
|||||||
} else if (shaExists != null) {
|
} else if (shaExists != null) {
|
||||||
error("${shaExists.name} has the same signing key fingerprint")
|
error("${shaExists.name} has the same signing key fingerprint")
|
||||||
} else {
|
} else {
|
||||||
handler.await {
|
database.extension_reposQueries.insert(
|
||||||
extension_reposQueries.insert(
|
backupRepo.baseUrl,
|
||||||
backupRepo.baseUrl,
|
backupRepo.name,
|
||||||
backupRepo.name,
|
backupRepo.shortName,
|
||||||
backupRepo.shortName,
|
backupRepo.website,
|
||||||
backupRepo.website,
|
backupRepo.signingKeyFingerprint,
|
||||||
backupRepo.signingKeyFingerprint,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+104
-105
@@ -1,12 +1,15 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.restore.restorers
|
package eu.kanade.tachiyomi.data.backup.restore.restorers
|
||||||
|
|
||||||
|
import app.cash.sqldelight.async.coroutines.awaitAsList
|
||||||
|
import app.cash.sqldelight.async.coroutines.awaitAsOne
|
||||||
|
import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull
|
||||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupChapter
|
import eu.kanade.tachiyomi.data.backup.models.BackupChapter
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
|
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupTracking
|
import eu.kanade.tachiyomi.data.backup.models.BackupTracking
|
||||||
import tachiyomi.data.DatabaseHandler
|
import tachiyomi.data.Database
|
||||||
import tachiyomi.data.UpdateStrategyColumnAdapter
|
import tachiyomi.data.UpdateStrategyColumnAdapter
|
||||||
import tachiyomi.domain.category.interactor.GetCategories
|
import tachiyomi.domain.category.interactor.GetCategories
|
||||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||||
@@ -24,7 +27,7 @@ import java.util.Date
|
|||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
class MangaRestorer(
|
class MangaRestorer(
|
||||||
private val handler: DatabaseHandler = Injekt.get(),
|
private val database: Database = Injekt.get(),
|
||||||
private val getCategories: GetCategories = Injekt.get(),
|
private val getCategories: GetCategories = Injekt.get(),
|
||||||
private val getMangaByUrlAndSourceId: GetMangaByUrlAndSourceId = Injekt.get(),
|
private val getMangaByUrlAndSourceId: GetMangaByUrlAndSourceId = Injekt.get(),
|
||||||
private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(),
|
private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(),
|
||||||
@@ -43,7 +46,9 @@ class MangaRestorer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun sortByNew(backupMangas: List<BackupManga>): List<BackupManga> {
|
suspend fun sortByNew(backupMangas: List<BackupManga>): List<BackupManga> {
|
||||||
val urlsBySource = handler.awaitList { mangasQueries.getAllMangaSourceAndUrl() }
|
val urlsBySource = database.mangasQueries
|
||||||
|
.getAllMangaSourceAndUrl()
|
||||||
|
.awaitAsList()
|
||||||
.groupBy({ it.source }, { it.url })
|
.groupBy({ it.source }, { it.url })
|
||||||
|
|
||||||
return backupMangas
|
return backupMangas
|
||||||
@@ -57,7 +62,7 @@ class MangaRestorer(
|
|||||||
backupManga: BackupManga,
|
backupManga: BackupManga,
|
||||||
backupCategories: List<BackupCategory>,
|
backupCategories: List<BackupCategory>,
|
||||||
) {
|
) {
|
||||||
handler.await(inTransaction = true) {
|
database.transaction {
|
||||||
val dbManga = findExistingManga(backupManga)
|
val dbManga = findExistingManga(backupManga)
|
||||||
val manga = backupManga.getMangaImpl()
|
val manga = backupManga.getMangaImpl()
|
||||||
val restoredManga = if (dbManga == null) {
|
val restoredManga = if (dbManga == null) {
|
||||||
@@ -105,33 +110,31 @@ class MangaRestorer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateManga(manga: Manga): Manga {
|
private suspend fun updateManga(manga: Manga): Manga {
|
||||||
handler.await(true) {
|
database.mangasQueries.update(
|
||||||
mangasQueries.update(
|
source = manga.source,
|
||||||
source = manga.source,
|
url = manga.url,
|
||||||
url = manga.url,
|
artist = manga.artist,
|
||||||
artist = manga.artist,
|
author = manga.author,
|
||||||
author = manga.author,
|
description = manga.description,
|
||||||
description = manga.description,
|
genre = manga.genre?.joinToString(separator = ", "),
|
||||||
genre = manga.genre?.joinToString(separator = ", "),
|
title = manga.title,
|
||||||
title = manga.title,
|
status = manga.status,
|
||||||
status = manga.status,
|
thumbnailUrl = manga.thumbnailUrl,
|
||||||
thumbnailUrl = manga.thumbnailUrl,
|
favorite = manga.favorite,
|
||||||
favorite = manga.favorite,
|
lastUpdate = manga.lastUpdate,
|
||||||
lastUpdate = manga.lastUpdate,
|
nextUpdate = null,
|
||||||
nextUpdate = null,
|
calculateInterval = null,
|
||||||
calculateInterval = null,
|
initialized = manga.initialized,
|
||||||
initialized = manga.initialized,
|
viewer = manga.viewerFlags,
|
||||||
viewer = manga.viewerFlags,
|
chapterFlags = manga.chapterFlags,
|
||||||
chapterFlags = manga.chapterFlags,
|
coverLastModified = manga.coverLastModified,
|
||||||
coverLastModified = manga.coverLastModified,
|
dateAdded = manga.dateAdded,
|
||||||
dateAdded = manga.dateAdded,
|
mangaId = manga.id,
|
||||||
mangaId = manga.id,
|
updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode),
|
||||||
updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode),
|
version = manga.version,
|
||||||
version = manga.version,
|
isSyncing = 1,
|
||||||
isSyncing = 1,
|
notes = manga.notes,
|
||||||
notes = manga.notes,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
return manga
|
return manga
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,9 +192,9 @@ class MangaRestorer(
|
|||||||
this.copy(id = 0L, mangaId = 0L, dateFetch = 0L, dateUpload = 0L, lastModifiedAt = 0L, version = 0L)
|
this.copy(id = 0L, mangaId = 0L, dateFetch = 0L, dateUpload = 0L, lastModifiedAt = 0L, version = 0L)
|
||||||
|
|
||||||
private suspend fun insertNewChapters(chapters: List<Chapter>) {
|
private suspend fun insertNewChapters(chapters: List<Chapter>) {
|
||||||
handler.await(true) {
|
database.transaction {
|
||||||
chapters.forEach { chapter ->
|
chapters.forEach { chapter ->
|
||||||
chaptersQueries.insert(
|
database.chaptersQueries.insert(
|
||||||
chapter.mangaId,
|
chapter.mangaId,
|
||||||
chapter.url,
|
chapter.url,
|
||||||
chapter.name,
|
chapter.name,
|
||||||
@@ -210,9 +213,9 @@ class MangaRestorer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateExistingChapters(chapters: List<Chapter>) {
|
private suspend fun updateExistingChapters(chapters: List<Chapter>) {
|
||||||
handler.await(true) {
|
database.transaction {
|
||||||
chapters.forEach { chapter ->
|
chapters.forEach { chapter ->
|
||||||
chaptersQueries.update(
|
database.chaptersQueries.update(
|
||||||
mangaId = null,
|
mangaId = null,
|
||||||
url = null,
|
url = null,
|
||||||
name = null,
|
name = null,
|
||||||
@@ -238,32 +241,30 @@ class MangaRestorer(
|
|||||||
* @return id of [Manga], null if not found
|
* @return id of [Manga], null if not found
|
||||||
*/
|
*/
|
||||||
private suspend fun insertManga(manga: Manga): Long {
|
private suspend fun insertManga(manga: Manga): Long {
|
||||||
return handler.awaitOneExecutable(true) {
|
return database.mangasQueries.insert(
|
||||||
mangasQueries.insert(
|
source = manga.source,
|
||||||
source = manga.source,
|
url = manga.url,
|
||||||
url = manga.url,
|
artist = manga.artist,
|
||||||
artist = manga.artist,
|
author = manga.author,
|
||||||
author = manga.author,
|
description = manga.description,
|
||||||
description = manga.description,
|
genre = manga.genre,
|
||||||
genre = manga.genre,
|
title = manga.title,
|
||||||
title = manga.title,
|
status = manga.status,
|
||||||
status = manga.status,
|
thumbnailUrl = manga.thumbnailUrl,
|
||||||
thumbnailUrl = manga.thumbnailUrl,
|
favorite = manga.favorite,
|
||||||
favorite = manga.favorite,
|
lastUpdate = manga.lastUpdate,
|
||||||
lastUpdate = manga.lastUpdate,
|
nextUpdate = 0L,
|
||||||
nextUpdate = 0L,
|
calculateInterval = 0L,
|
||||||
calculateInterval = 0L,
|
initialized = manga.initialized,
|
||||||
initialized = manga.initialized,
|
viewerFlags = manga.viewerFlags,
|
||||||
viewerFlags = manga.viewerFlags,
|
chapterFlags = manga.chapterFlags,
|
||||||
chapterFlags = manga.chapterFlags,
|
coverLastModified = manga.coverLastModified,
|
||||||
coverLastModified = manga.coverLastModified,
|
dateAdded = manga.dateAdded,
|
||||||
dateAdded = manga.dateAdded,
|
updateStrategy = manga.updateStrategy,
|
||||||
updateStrategy = manga.updateStrategy,
|
version = manga.version,
|
||||||
version = manga.version,
|
notes = manga.notes,
|
||||||
notes = manga.notes,
|
)
|
||||||
)
|
.awaitAsOne()
|
||||||
mangasQueries.selectLastInsertedRowId()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun restoreMangaDetails(
|
private suspend fun restoreMangaDetails(
|
||||||
@@ -309,10 +310,10 @@ class MangaRestorer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mangaCategoriesToUpdate.isNotEmpty()) {
|
if (mangaCategoriesToUpdate.isNotEmpty()) {
|
||||||
handler.await(true) {
|
database.transaction {
|
||||||
mangas_categoriesQueries.deleteMangaCategoryByMangaId(manga.id)
|
database.mangas_categoriesQueries.deleteMangaCategoryByMangaId(manga.id)
|
||||||
mangaCategoriesToUpdate.forEach { (mangaId, categoryId) ->
|
mangaCategoriesToUpdate.forEach { (mangaId, categoryId) ->
|
||||||
mangas_categoriesQueries.insert(mangaId, categoryId)
|
database.mangas_categoriesQueries.insert(mangaId, categoryId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -320,11 +321,15 @@ class MangaRestorer(
|
|||||||
|
|
||||||
private suspend fun restoreHistory(backupHistory: List<BackupHistory>) {
|
private suspend fun restoreHistory(backupHistory: List<BackupHistory>) {
|
||||||
val toUpdate = backupHistory.mapNotNull { history ->
|
val toUpdate = backupHistory.mapNotNull { history ->
|
||||||
val dbHistory = handler.awaitOneOrNull { historyQueries.getHistoryByChapterUrl(history.url) }
|
val dbHistory = database.historyQueries
|
||||||
|
.getHistoryByChapterUrl(history.url)
|
||||||
|
.awaitAsOneOrNull()
|
||||||
val item = history.getHistoryImpl()
|
val item = history.getHistoryImpl()
|
||||||
|
|
||||||
if (dbHistory == null) {
|
if (dbHistory == null) {
|
||||||
val chapter = handler.awaitOneOrNull { chaptersQueries.getChapterByUrl(history.url) }
|
val chapter = database.chaptersQueries
|
||||||
|
.getChapterByUrl(history.url)
|
||||||
|
.awaitAsOneOrNull()
|
||||||
return@mapNotNull if (chapter == null) {
|
return@mapNotNull if (chapter == null) {
|
||||||
// Chapter doesn't exist; skip
|
// Chapter doesn't exist; skip
|
||||||
null
|
null
|
||||||
@@ -345,15 +350,14 @@ class MangaRestorer(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toUpdate.isNotEmpty()) {
|
if (toUpdate.isEmpty()) return
|
||||||
handler.await(true) {
|
database.transaction {
|
||||||
toUpdate.forEach {
|
toUpdate.forEach {
|
||||||
historyQueries.upsert(
|
database.historyQueries.upsert(
|
||||||
it.chapterId,
|
it.chapterId,
|
||||||
it.readAt,
|
it.readAt,
|
||||||
it.readDuration,
|
it.readDuration,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -388,26 +392,26 @@ class MangaRestorer(
|
|||||||
if (newTracks.isNotEmpty()) {
|
if (newTracks.isNotEmpty()) {
|
||||||
insertTrack.awaitAll(newTracks)
|
insertTrack.awaitAll(newTracks)
|
||||||
}
|
}
|
||||||
if (existingTracks.isNotEmpty()) {
|
|
||||||
handler.await(true) {
|
if (existingTracks.isEmpty()) return
|
||||||
existingTracks.forEach { track ->
|
database.transaction {
|
||||||
manga_syncQueries.update(
|
existingTracks.forEach { track ->
|
||||||
track.mangaId,
|
database.manga_syncQueries.update(
|
||||||
track.trackerId,
|
track.mangaId,
|
||||||
track.remoteId,
|
track.trackerId,
|
||||||
track.libraryId,
|
track.remoteId,
|
||||||
track.title,
|
track.libraryId,
|
||||||
track.lastChapterRead,
|
track.title,
|
||||||
track.totalChapters,
|
track.lastChapterRead,
|
||||||
track.status,
|
track.totalChapters,
|
||||||
track.score,
|
track.status,
|
||||||
track.remoteUrl,
|
track.score,
|
||||||
track.startDate,
|
track.remoteUrl,
|
||||||
track.finishDate,
|
track.startDate,
|
||||||
track.private,
|
track.finishDate,
|
||||||
track.id,
|
track.private,
|
||||||
)
|
track.id,
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -422,16 +426,11 @@ class MangaRestorer(
|
|||||||
*/
|
*/
|
||||||
private suspend fun restoreExcludedScanlators(manga: Manga, excludedScanlators: List<String>) {
|
private suspend fun restoreExcludedScanlators(manga: Manga, excludedScanlators: List<String>) {
|
||||||
if (excludedScanlators.isEmpty()) return
|
if (excludedScanlators.isEmpty()) return
|
||||||
val existingExcludedScanlators = handler.awaitList {
|
val existingExcludedScanlators = database.excluded_scanlatorsQueries
|
||||||
excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(manga.id)
|
.getExcludedScanlatorsByMangaId(manga.id)
|
||||||
}
|
.awaitAsList()
|
||||||
val toInsert = excludedScanlators.filter { it !in existingExcludedScanlators }
|
val toInsert = excludedScanlators.filter { it !in existingExcludedScanlators }
|
||||||
if (toInsert.isNotEmpty()) {
|
if (toInsert.isEmpty()) return
|
||||||
handler.await {
|
toInsert.forEach { database.excluded_scanlatorsQueries.insert(manga.id, it) }
|
||||||
toInsert.forEach {
|
|
||||||
excluded_scanlatorsQueries.insert(manga.id, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,9 +26,7 @@ import nl.adaptivity.xmlutil.XmlDeclMode
|
|||||||
import nl.adaptivity.xmlutil.core.XmlVersion
|
import nl.adaptivity.xmlutil.core.XmlVersion
|
||||||
import nl.adaptivity.xmlutil.serialization.XML
|
import nl.adaptivity.xmlutil.serialization.XML
|
||||||
import tachiyomi.core.common.storage.AndroidStorageFolderProvider
|
import tachiyomi.core.common.storage.AndroidStorageFolderProvider
|
||||||
import tachiyomi.data.AndroidDatabaseHandler
|
|
||||||
import tachiyomi.data.Database
|
import tachiyomi.data.Database
|
||||||
import tachiyomi.data.DatabaseHandler
|
|
||||||
import tachiyomi.data.DateColumnAdapter
|
import tachiyomi.data.DateColumnAdapter
|
||||||
import tachiyomi.data.History
|
import tachiyomi.data.History
|
||||||
import tachiyomi.data.Mangas
|
import tachiyomi.data.Mangas
|
||||||
@@ -81,7 +79,6 @@ class AppModule(val app: Application) : InjektModule {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
addSingletonFactory<DatabaseHandler> { AndroidDatabaseHandler(get(), get()) }
|
|
||||||
|
|
||||||
addSingletonFactory {
|
addSingletonFactory {
|
||||||
Json {
|
Json {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ android {
|
|||||||
packageName.set("tachiyomi.data")
|
packageName.set("tachiyomi.data")
|
||||||
dialect(libs.sqldelight.sqliteDialect338)
|
dialect(libs.sqldelight.sqliteDialect338)
|
||||||
schemaOutputDirectory.set(project.file("./src/main/sqldelight"))
|
schemaOutputDirectory.set(project.file("./src/main/sqldelight"))
|
||||||
|
generateAsync.set(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,49 @@
|
|||||||
package mihon.data.repository
|
package mihon.data.repository
|
||||||
|
|
||||||
import android.database.SQLException
|
import android.database.SQLException
|
||||||
|
import app.cash.sqldelight.async.coroutines.awaitAsList
|
||||||
|
import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import mihon.domain.extensionrepo.exception.SaveExtensionRepoException
|
import mihon.domain.extensionrepo.exception.SaveExtensionRepoException
|
||||||
import mihon.domain.extensionrepo.model.ExtensionRepo
|
import mihon.domain.extensionrepo.model.ExtensionRepo
|
||||||
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
|
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
|
||||||
import tachiyomi.data.DatabaseHandler
|
import tachiyomi.data.Database
|
||||||
|
import tachiyomi.data.subscribeToList
|
||||||
|
import tachiyomi.data.subscribeToOne
|
||||||
|
|
||||||
class ExtensionRepoRepositoryImpl(
|
class ExtensionRepoRepositoryImpl(
|
||||||
private val handler: DatabaseHandler,
|
private val database: Database,
|
||||||
) : ExtensionRepoRepository {
|
) : ExtensionRepoRepository {
|
||||||
override fun subscribeAll(): Flow<List<ExtensionRepo>> {
|
override fun subscribeAll(): Flow<List<ExtensionRepo>> {
|
||||||
return handler.subscribeToList { extension_reposQueries.findAll(::mapExtensionRepo) }
|
return database.extension_reposQueries
|
||||||
|
.findAll(::mapExtensionRepo)
|
||||||
|
.subscribeToList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getAll(): List<ExtensionRepo> {
|
override suspend fun getAll(): List<ExtensionRepo> {
|
||||||
return handler.awaitList { extension_reposQueries.findAll(::mapExtensionRepo) }
|
return database.extension_reposQueries
|
||||||
|
.findAll(::mapExtensionRepo)
|
||||||
|
.awaitAsList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getRepo(baseUrl: String): ExtensionRepo? {
|
override suspend fun getRepo(baseUrl: String): ExtensionRepo? {
|
||||||
return handler.awaitOneOrNull { extension_reposQueries.findOne(baseUrl, ::mapExtensionRepo) }
|
return database.extension_reposQueries
|
||||||
|
.findOne(baseUrl, ::mapExtensionRepo)
|
||||||
|
.awaitAsOneOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getRepoBySigningKeyFingerprint(fingerprint: String): ExtensionRepo? {
|
override suspend fun getRepoBySigningKeyFingerprint(fingerprint: String): ExtensionRepo? {
|
||||||
return handler.awaitOneOrNull {
|
return database.extension_reposQueries
|
||||||
extension_reposQueries.findOneBySigningKeyFingerprint(fingerprint, ::mapExtensionRepo)
|
.findOneBySigningKeyFingerprint(fingerprint, ::mapExtensionRepo)
|
||||||
}
|
.awaitAsOneOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCount(): Flow<Int> {
|
override fun getCount(): Flow<Int> {
|
||||||
return handler.subscribeToOne { extension_reposQueries.count() }.map { it.toInt() }
|
return database.extension_reposQueries
|
||||||
|
.count()
|
||||||
|
.subscribeToOne()
|
||||||
|
.map { it.toInt() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun insertRepo(
|
override suspend fun insertRepo(
|
||||||
@@ -41,7 +54,13 @@ class ExtensionRepoRepositoryImpl(
|
|||||||
signingKeyFingerprint: String,
|
signingKeyFingerprint: String,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
handler.await { extension_reposQueries.insert(baseUrl, name, shortName, website, signingKeyFingerprint) }
|
database.extension_reposQueries.insert(
|
||||||
|
baseUrl,
|
||||||
|
name,
|
||||||
|
shortName,
|
||||||
|
website,
|
||||||
|
signingKeyFingerprint,
|
||||||
|
)
|
||||||
} catch (ex: SQLException) {
|
} catch (ex: SQLException) {
|
||||||
throw SaveExtensionRepoException(ex)
|
throw SaveExtensionRepoException(ex)
|
||||||
}
|
}
|
||||||
@@ -55,26 +74,30 @@ class ExtensionRepoRepositoryImpl(
|
|||||||
signingKeyFingerprint: String,
|
signingKeyFingerprint: String,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
handler.await { extension_reposQueries.upsert(baseUrl, name, shortName, website, signingKeyFingerprint) }
|
database.extension_reposQueries.upsert(
|
||||||
|
baseUrl,
|
||||||
|
name,
|
||||||
|
shortName,
|
||||||
|
website,
|
||||||
|
signingKeyFingerprint,
|
||||||
|
)
|
||||||
} catch (ex: SQLException) {
|
} catch (ex: SQLException) {
|
||||||
throw SaveExtensionRepoException(ex)
|
throw SaveExtensionRepoException(ex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun replaceRepo(newRepo: ExtensionRepo) {
|
override suspend fun replaceRepo(newRepo: ExtensionRepo) {
|
||||||
handler.await {
|
database.extension_reposQueries.replace(
|
||||||
extension_reposQueries.replace(
|
newRepo.baseUrl,
|
||||||
newRepo.baseUrl,
|
newRepo.name,
|
||||||
newRepo.name,
|
newRepo.shortName,
|
||||||
newRepo.shortName,
|
newRepo.website,
|
||||||
newRepo.website,
|
newRepo.signingKeyFingerprint,
|
||||||
newRepo.signingKeyFingerprint,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun deleteRepo(baseUrl: String) {
|
override suspend fun deleteRepo(baseUrl: String) {
|
||||||
return handler.await { extension_reposQueries.delete(baseUrl) }
|
database.extension_reposQueries.delete(baseUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun mapExtensionRepo(
|
private fun mapExtensionRepo(
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
package tachiyomi.data
|
|
||||||
|
|
||||||
import androidx.paging.PagingSource
|
|
||||||
import app.cash.sqldelight.ExecutableQuery
|
|
||||||
import app.cash.sqldelight.Query
|
|
||||||
import app.cash.sqldelight.coroutines.asFlow
|
|
||||||
import app.cash.sqldelight.coroutines.mapToList
|
|
||||||
import app.cash.sqldelight.coroutines.mapToOne
|
|
||||||
import app.cash.sqldelight.coroutines.mapToOneOrNull
|
|
||||||
import app.cash.sqldelight.db.SqlDriver
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
class AndroidDatabaseHandler(
|
|
||||||
val db: Database,
|
|
||||||
private val driver: SqlDriver,
|
|
||||||
val queryDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
|
||||||
val transactionDispatcher: CoroutineDispatcher = queryDispatcher,
|
|
||||||
) : DatabaseHandler {
|
|
||||||
|
|
||||||
val suspendingTransactionId = ThreadLocal<Int>()
|
|
||||||
|
|
||||||
override suspend fun <T> await(inTransaction: Boolean, block: suspend Database.() -> T): T {
|
|
||||||
return dispatch(inTransaction, block)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun <T : Any> awaitList(
|
|
||||||
inTransaction: Boolean,
|
|
||||||
block: suspend Database.() -> Query<T>,
|
|
||||||
): List<T> {
|
|
||||||
return dispatch(inTransaction) { block(db).executeAsList() }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun <T : Any> awaitOne(
|
|
||||||
inTransaction: Boolean,
|
|
||||||
block: suspend Database.() -> Query<T>,
|
|
||||||
): T {
|
|
||||||
return dispatch(inTransaction) { block(db).executeAsOne() }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun <T : Any> awaitOneExecutable(
|
|
||||||
inTransaction: Boolean,
|
|
||||||
block: suspend Database.() -> ExecutableQuery<T>,
|
|
||||||
): T {
|
|
||||||
return dispatch(inTransaction) { block(db).executeAsOne() }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun <T : Any> awaitOneOrNull(
|
|
||||||
inTransaction: Boolean,
|
|
||||||
block: suspend Database.() -> Query<T>,
|
|
||||||
): T? {
|
|
||||||
return dispatch(inTransaction) { block(db).executeAsOneOrNull() }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun <T : Any> awaitOneOrNullExecutable(
|
|
||||||
inTransaction: Boolean,
|
|
||||||
block: suspend Database.() -> ExecutableQuery<T>,
|
|
||||||
): T? {
|
|
||||||
return dispatch(inTransaction) { block(db).executeAsOneOrNull() }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun <T : Any> subscribeToList(block: Database.() -> Query<T>): Flow<List<T>> {
|
|
||||||
return block(db).asFlow().mapToList(queryDispatcher)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun <T : Any> subscribeToOne(block: Database.() -> Query<T>): Flow<T> {
|
|
||||||
return block(db).asFlow().mapToOne(queryDispatcher)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun <T : Any> subscribeToOneOrNull(block: Database.() -> Query<T>): Flow<T?> {
|
|
||||||
return block(db).asFlow().mapToOneOrNull(queryDispatcher)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun <T : Any> subscribeToPagingSource(
|
|
||||||
countQuery: Database.() -> Query<Long>,
|
|
||||||
queryProvider: Database.(Long, Long) -> Query<T>,
|
|
||||||
): PagingSource<Long, T> {
|
|
||||||
return QueryPagingSource(
|
|
||||||
handler = this,
|
|
||||||
countQuery = countQuery,
|
|
||||||
queryProvider = { limit, offset ->
|
|
||||||
queryProvider.invoke(db, limit, offset)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun <T> dispatch(inTransaction: Boolean, block: suspend Database.() -> T): T {
|
|
||||||
// Create a transaction if needed and run the calling block inside it.
|
|
||||||
if (inTransaction) {
|
|
||||||
return withTransaction { block(db) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we're currently in the transaction thread, there's no need to dispatch our query.
|
|
||||||
if (driver.currentTransaction() != null) {
|
|
||||||
return block(db)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the current database context and run the calling block.
|
|
||||||
val context = getCurrentDatabaseContext()
|
|
||||||
return withContext(context) { block(db) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package tachiyomi.data
|
|
||||||
|
|
||||||
import androidx.paging.PagingSource
|
|
||||||
import app.cash.sqldelight.ExecutableQuery
|
|
||||||
import app.cash.sqldelight.Query
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
interface DatabaseHandler {
|
|
||||||
|
|
||||||
suspend fun <T> await(inTransaction: Boolean = false, block: suspend Database.() -> T): T
|
|
||||||
|
|
||||||
suspend fun <T : Any> awaitList(
|
|
||||||
inTransaction: Boolean = false,
|
|
||||||
block: suspend Database.() -> Query<T>,
|
|
||||||
): List<T>
|
|
||||||
|
|
||||||
suspend fun <T : Any> awaitOne(
|
|
||||||
inTransaction: Boolean = false,
|
|
||||||
block: suspend Database.() -> Query<T>,
|
|
||||||
): T
|
|
||||||
|
|
||||||
suspend fun <T : Any> awaitOneExecutable(
|
|
||||||
inTransaction: Boolean = false,
|
|
||||||
block: suspend Database.() -> ExecutableQuery<T>,
|
|
||||||
): T
|
|
||||||
|
|
||||||
suspend fun <T : Any> awaitOneOrNull(
|
|
||||||
inTransaction: Boolean = false,
|
|
||||||
block: suspend Database.() -> Query<T>,
|
|
||||||
): T?
|
|
||||||
|
|
||||||
suspend fun <T : Any> awaitOneOrNullExecutable(
|
|
||||||
inTransaction: Boolean = false,
|
|
||||||
block: suspend Database.() -> ExecutableQuery<T>,
|
|
||||||
): T?
|
|
||||||
|
|
||||||
fun <T : Any> subscribeToList(block: Database.() -> Query<T>): Flow<List<T>>
|
|
||||||
|
|
||||||
fun <T : Any> subscribeToOne(block: Database.() -> Query<T>): Flow<T>
|
|
||||||
|
|
||||||
fun <T : Any> subscribeToOneOrNull(block: Database.() -> Query<T>): Flow<T?>
|
|
||||||
|
|
||||||
fun <T : Any> subscribeToPagingSource(
|
|
||||||
countQuery: Database.() -> Query<Long>,
|
|
||||||
queryProvider: Database.(Long, Long) -> Query<T>,
|
|
||||||
): PagingSource<Long, T>
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package tachiyomi.data
|
||||||
|
|
||||||
|
import app.cash.sqldelight.Query
|
||||||
|
import app.cash.sqldelight.async.coroutines.awaitAsList
|
||||||
|
import app.cash.sqldelight.async.coroutines.awaitAsOne
|
||||||
|
import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull
|
||||||
|
import app.cash.sqldelight.coroutines.asFlow
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
|
||||||
|
fun <T : Any> Query<T>.subscribeToList(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
): Flow<List<T>> = asFlow().map {
|
||||||
|
withContext(context) {
|
||||||
|
it.awaitAsList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T : Any> Query<T>.subscribeToOne(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
): Flow<T> = asFlow().map {
|
||||||
|
withContext(context) {
|
||||||
|
it.awaitAsOne()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T : Any> Query<T>.subscribeToOneOrNull(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
): Flow<T?> = asFlow().map {
|
||||||
|
withContext(context) {
|
||||||
|
it.awaitAsOneOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,12 +3,14 @@ package tachiyomi.data
|
|||||||
import androidx.paging.PagingSource
|
import androidx.paging.PagingSource
|
||||||
import androidx.paging.PagingState
|
import androidx.paging.PagingState
|
||||||
import app.cash.sqldelight.Query
|
import app.cash.sqldelight.Query
|
||||||
|
import app.cash.sqldelight.async.coroutines.awaitAsList
|
||||||
|
import app.cash.sqldelight.async.coroutines.awaitAsOne
|
||||||
import kotlin.properties.Delegates
|
import kotlin.properties.Delegates
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
class QueryPagingSource<RowType : Any>(
|
class QueryPagingSource<RowType : Any>(
|
||||||
val handler: DatabaseHandler,
|
val countQuery: () -> Query<Long>,
|
||||||
val countQuery: Database.() -> Query<Long>,
|
val queryProvider: (Long, Long) -> Query<RowType>,
|
||||||
val queryProvider: Database.(Long, Long) -> Query<RowType>,
|
|
||||||
) : PagingSource<Long, RowType>(), Query.Listener {
|
) : PagingSource<Long, RowType>(), Query.Listener {
|
||||||
|
|
||||||
override val jumpingSupported: Boolean = true
|
override val jumpingSupported: Boolean = true
|
||||||
@@ -29,17 +31,16 @@ class QueryPagingSource<RowType : Any>(
|
|||||||
try {
|
try {
|
||||||
val key = params.key ?: 0L
|
val key = params.key ?: 0L
|
||||||
val loadSize = params.loadSize
|
val loadSize = params.loadSize
|
||||||
val count = handler.awaitOne { countQuery() }
|
val count = countQuery().awaitAsOne()
|
||||||
|
|
||||||
val (offset, limit) = when (params) {
|
val (offset, limit) = when (params) {
|
||||||
is LoadParams.Prepend -> key - loadSize to loadSize.toLong()
|
is LoadParams.Prepend -> key - loadSize to loadSize.toLong()
|
||||||
else -> key to loadSize.toLong()
|
else -> key to loadSize.toLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
val data = handler.awaitList {
|
val data = queryProvider(limit, offset)
|
||||||
queryProvider(limit, offset)
|
.also { currentQuery = it }
|
||||||
.also { currentQuery = it }
|
.awaitAsList()
|
||||||
}
|
|
||||||
|
|
||||||
val (prevKey, nextKey) = when (params) {
|
val (prevKey, nextKey) = when (params) {
|
||||||
is LoadParams.Append -> (offset - loadSize to offset + loadSize)
|
is LoadParams.Append -> (offset - loadSize to offset + loadSize)
|
||||||
|
|||||||
@@ -1,165 +0,0 @@
|
|||||||
package tachiyomi.data
|
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.asContextElement
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.util.concurrent.RejectedExecutionException
|
|
||||||
import kotlin.concurrent.atomics.AtomicInt
|
|
||||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
|
||||||
import kotlin.concurrent.atomics.decrementAndFetch
|
|
||||||
import kotlin.concurrent.atomics.incrementAndFetch
|
|
||||||
import kotlin.coroutines.ContinuationInterceptor
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
|
||||||
import kotlin.coroutines.coroutineContext
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the transaction dispatcher if we are on a transaction, or the database dispatchers.
|
|
||||||
*/
|
|
||||||
internal suspend fun AndroidDatabaseHandler.getCurrentDatabaseContext(): CoroutineContext {
|
|
||||||
return coroutineContext[TransactionElement]?.transactionDispatcher ?: queryDispatcher
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calls the specified suspending [block] in a database transaction. The transaction will be
|
|
||||||
* marked as successful unless an exception is thrown in the suspending [block] or the coroutine
|
|
||||||
* is cancelled.
|
|
||||||
*
|
|
||||||
* SQLDelight will only perform at most one transaction at a time, additional transactions are queued
|
|
||||||
* and executed on a first come, first serve order.
|
|
||||||
*
|
|
||||||
* Performing blocking database operations is not permitted in a coroutine scope other than the
|
|
||||||
* one received by the suspending block. It is recommended that all [Dao] function invoked within
|
|
||||||
* the [block] be suspending functions.
|
|
||||||
*
|
|
||||||
* The dispatcher used to execute the given [block] will utilize threads from SQLDelight's query executor.
|
|
||||||
*/
|
|
||||||
internal suspend fun <T> AndroidDatabaseHandler.withTransaction(block: suspend () -> T): T {
|
|
||||||
// Use inherited transaction context if available, this allows nested suspending transactions.
|
|
||||||
val transactionContext =
|
|
||||||
coroutineContext[TransactionElement]?.transactionDispatcher ?: createTransactionContext()
|
|
||||||
return withContext(transactionContext) {
|
|
||||||
val transactionElement = coroutineContext[TransactionElement]!!
|
|
||||||
transactionElement.acquire()
|
|
||||||
try {
|
|
||||||
db.transactionWithResult {
|
|
||||||
runBlocking(transactionContext) {
|
|
||||||
block()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
transactionElement.release()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a [CoroutineContext] for performing database operations within a coroutine transaction.
|
|
||||||
*
|
|
||||||
* The context is a combination of a dispatcher, a [TransactionElement] and a thread local element.
|
|
||||||
*
|
|
||||||
* * The dispatcher will dispatch coroutines to a single thread that is taken over from the SQLDelight
|
|
||||||
* query executor. If the coroutine context is switched, suspending DAO functions will be able to
|
|
||||||
* dispatch to the transaction thread.
|
|
||||||
*
|
|
||||||
* * The [TransactionElement] serves as an indicator for inherited context, meaning, if there is a
|
|
||||||
* switch of context, suspending DAO methods will be able to use the indicator to dispatch the
|
|
||||||
* database operation to the transaction thread.
|
|
||||||
*
|
|
||||||
* * The thread local element serves as a second indicator and marks threads that are used to
|
|
||||||
* execute coroutines within the coroutine transaction, more specifically it allows us to identify
|
|
||||||
* if a blocking DAO method is invoked within the transaction coroutine. Never assign meaning to
|
|
||||||
* this value, for now all we care is if its present or not.
|
|
||||||
*/
|
|
||||||
private suspend fun AndroidDatabaseHandler.createTransactionContext(): CoroutineContext {
|
|
||||||
val controlJob = Job()
|
|
||||||
// make sure to tie the control job to this context to avoid blocking the transaction if
|
|
||||||
// context get cancelled before we can even start using this job. Otherwise, the acquired
|
|
||||||
// transaction thread will forever wait for the controlJob to be cancelled.
|
|
||||||
// see b/148181325
|
|
||||||
coroutineContext[Job]?.invokeOnCompletion {
|
|
||||||
controlJob.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
val dispatcher = transactionDispatcher.acquireTransactionThread(controlJob)
|
|
||||||
val transactionElement = TransactionElement(controlJob, dispatcher)
|
|
||||||
val threadLocalElement =
|
|
||||||
suspendingTransactionId.asContextElement(System.identityHashCode(controlJob))
|
|
||||||
return dispatcher + transactionElement + threadLocalElement
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Acquires a thread from the executor and returns a [ContinuationInterceptor] to dispatch
|
|
||||||
* coroutines to the acquired thread. The [controlJob] is used to control the release of the
|
|
||||||
* thread by cancelling the job.
|
|
||||||
*/
|
|
||||||
private suspend fun CoroutineDispatcher.acquireTransactionThread(
|
|
||||||
controlJob: Job,
|
|
||||||
): ContinuationInterceptor {
|
|
||||||
return suspendCancellableCoroutine { continuation ->
|
|
||||||
continuation.invokeOnCancellation {
|
|
||||||
// We got cancelled while waiting to acquire a thread, we can't stop our attempt to
|
|
||||||
// acquire a thread, but we can cancel the controlling job so once it gets acquired it
|
|
||||||
// is quickly released.
|
|
||||||
controlJob.cancel()
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
dispatch(EmptyCoroutineContext) {
|
|
||||||
runBlocking {
|
|
||||||
// Thread acquired, resume coroutine
|
|
||||||
continuation.resume(coroutineContext[ContinuationInterceptor]!!)
|
|
||||||
controlJob.join()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (ex: RejectedExecutionException) {
|
|
||||||
// Couldn't acquire a thread, cancel coroutine
|
|
||||||
continuation.cancel(
|
|
||||||
IllegalStateException(
|
|
||||||
"Unable to acquire a thread to perform the database transaction",
|
|
||||||
ex,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A [CoroutineContext.Element] that indicates there is an on-going database transaction.
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalAtomicApi::class)
|
|
||||||
private class TransactionElement(
|
|
||||||
private val transactionThreadControlJob: Job,
|
|
||||||
val transactionDispatcher: ContinuationInterceptor,
|
|
||||||
) : CoroutineContext.Element {
|
|
||||||
|
|
||||||
companion object Key : CoroutineContext.Key<TransactionElement>
|
|
||||||
|
|
||||||
override val key: CoroutineContext.Key<TransactionElement>
|
|
||||||
get() = TransactionElement
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Number of transactions (including nested ones) started with this element.
|
|
||||||
* Call [acquire] to increase the count and [release] to decrease it. If the count reaches zero
|
|
||||||
* when [release] is invoked then the transaction job is cancelled and the transaction thread
|
|
||||||
* is released.
|
|
||||||
*/
|
|
||||||
private val referenceCount = AtomicInt(0)
|
|
||||||
|
|
||||||
fun acquire() {
|
|
||||||
referenceCount.incrementAndFetch()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun release() {
|
|
||||||
val count = referenceCount.decrementAndFetch()
|
|
||||||
if (count < 0) {
|
|
||||||
throw IllegalStateException("Transaction was never started or was already released")
|
|
||||||
} else if (count == 0) {
|
|
||||||
// Cancel the job that controls the transaction thread, causing it to be released.
|
|
||||||
transactionThreadControlJob.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +1,58 @@
|
|||||||
package tachiyomi.data.category
|
package tachiyomi.data.category
|
||||||
|
|
||||||
|
import app.cash.sqldelight.async.coroutines.awaitAsList
|
||||||
|
import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import tachiyomi.data.Database
|
import tachiyomi.data.Database
|
||||||
import tachiyomi.data.DatabaseHandler
|
import tachiyomi.data.subscribeToList
|
||||||
import tachiyomi.domain.category.model.Category
|
import tachiyomi.domain.category.model.Category
|
||||||
import tachiyomi.domain.category.model.CategoryUpdate
|
import tachiyomi.domain.category.model.CategoryUpdate
|
||||||
import tachiyomi.domain.category.repository.CategoryRepository
|
import tachiyomi.domain.category.repository.CategoryRepository
|
||||||
|
|
||||||
class CategoryRepositoryImpl(
|
class CategoryRepositoryImpl(
|
||||||
private val handler: DatabaseHandler,
|
private val database: Database,
|
||||||
) : CategoryRepository {
|
) : CategoryRepository {
|
||||||
|
|
||||||
override suspend fun get(id: Long): Category? {
|
override suspend fun get(id: Long): Category? {
|
||||||
return handler.awaitOneOrNull { categoriesQueries.getCategory(id, ::mapCategory) }
|
return database.categoriesQueries
|
||||||
|
.getCategory(id, ::mapCategory)
|
||||||
|
.awaitAsOneOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getAll(): List<Category> {
|
override suspend fun getAll(): List<Category> {
|
||||||
return handler.awaitList { categoriesQueries.getCategories(::mapCategory) }
|
return database.categoriesQueries
|
||||||
|
.getCategories(::mapCategory)
|
||||||
|
.awaitAsList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAllAsFlow(): Flow<List<Category>> {
|
override fun getAllAsFlow(): Flow<List<Category>> {
|
||||||
return handler.subscribeToList { categoriesQueries.getCategories(::mapCategory) }
|
return database.categoriesQueries
|
||||||
|
.getCategories(::mapCategory)
|
||||||
|
.subscribeToList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getCategoriesByMangaId(mangaId: Long): List<Category> {
|
override suspend fun getCategoriesByMangaId(mangaId: Long): List<Category> {
|
||||||
return handler.awaitList {
|
return database.categoriesQueries
|
||||||
categoriesQueries.getCategoriesByMangaId(mangaId, ::mapCategory)
|
.getCategoriesByMangaId(mangaId, ::mapCategory)
|
||||||
}
|
.awaitAsList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCategoriesByMangaIdAsFlow(mangaId: Long): Flow<List<Category>> {
|
override fun getCategoriesByMangaIdAsFlow(mangaId: Long): Flow<List<Category>> {
|
||||||
return handler.subscribeToList {
|
return database.categoriesQueries
|
||||||
categoriesQueries.getCategoriesByMangaId(mangaId, ::mapCategory)
|
.getCategoriesByMangaId(mangaId, ::mapCategory)
|
||||||
}
|
.subscribeToList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun insert(category: Category) {
|
override suspend fun insert(category: Category) {
|
||||||
handler.await {
|
database.categoriesQueries.insert(
|
||||||
categoriesQueries.insert(
|
name = category.name,
|
||||||
name = category.name,
|
order = category.order,
|
||||||
order = category.order,
|
flags = category.flags,
|
||||||
flags = category.flags,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun updatePartial(update: CategoryUpdate) {
|
override suspend fun updatePartial(update: CategoryUpdate) {
|
||||||
handler.await {
|
database.categoriesQueries.update(
|
||||||
updatePartialBlocking(update)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun updatePartial(updates: List<CategoryUpdate>) {
|
|
||||||
handler.await(inTransaction = true) {
|
|
||||||
for (update in updates) {
|
|
||||||
updatePartialBlocking(update)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Database.updatePartialBlocking(update: CategoryUpdate) {
|
|
||||||
categoriesQueries.update(
|
|
||||||
name = update.name,
|
name = update.name,
|
||||||
order = update.order,
|
order = update.order,
|
||||||
flags = update.flags,
|
flags = update.flags,
|
||||||
@@ -68,18 +60,18 @@ class CategoryRepositoryImpl(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun updateAllFlags(flags: Long?) {
|
override suspend fun updatePartial(updates: List<CategoryUpdate>) {
|
||||||
handler.await {
|
database.transaction {
|
||||||
categoriesQueries.updateAllFlags(flags)
|
updates.forEach { updatePartial(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun updateAllFlags(flags: Long?) {
|
||||||
|
database.categoriesQueries.updateAllFlags(flags)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun delete(categoryId: Long) {
|
override suspend fun delete(categoryId: Long) {
|
||||||
handler.await {
|
database.categoriesQueries.delete(categoryId = categoryId)
|
||||||
categoriesQueries.delete(
|
|
||||||
categoryId = categoryId,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun mapCategory(
|
private fun mapCategory(
|
||||||
|
|||||||
@@ -1,23 +1,27 @@
|
|||||||
package tachiyomi.data.chapter
|
package tachiyomi.data.chapter
|
||||||
|
|
||||||
|
import app.cash.sqldelight.async.coroutines.awaitAsList
|
||||||
|
import app.cash.sqldelight.async.coroutines.awaitAsOne
|
||||||
|
import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.common.util.lang.toLong
|
import tachiyomi.core.common.util.lang.toLong
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import tachiyomi.data.DatabaseHandler
|
import tachiyomi.data.Database
|
||||||
|
import tachiyomi.data.subscribeToList
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
import tachiyomi.domain.chapter.model.ChapterUpdate
|
import tachiyomi.domain.chapter.model.ChapterUpdate
|
||||||
import tachiyomi.domain.chapter.repository.ChapterRepository
|
import tachiyomi.domain.chapter.repository.ChapterRepository
|
||||||
|
|
||||||
class ChapterRepositoryImpl(
|
class ChapterRepositoryImpl(
|
||||||
private val handler: DatabaseHandler,
|
private val database: Database,
|
||||||
) : ChapterRepository {
|
) : ChapterRepository {
|
||||||
|
|
||||||
override suspend fun addAll(chapters: List<Chapter>): List<Chapter> {
|
override suspend fun addAll(chapters: List<Chapter>): List<Chapter> {
|
||||||
return try {
|
return try {
|
||||||
handler.await(inTransaction = true) {
|
database.transactionWithResult {
|
||||||
chapters.map { chapter ->
|
chapters.map { chapter ->
|
||||||
chaptersQueries.insert(
|
val lastInsertId = database.chaptersQueries.insert(
|
||||||
chapter.mangaId,
|
chapter.mangaId,
|
||||||
chapter.url,
|
chapter.url,
|
||||||
chapter.name,
|
chapter.name,
|
||||||
@@ -30,8 +34,7 @@ class ChapterRepositoryImpl(
|
|||||||
chapter.dateFetch,
|
chapter.dateFetch,
|
||||||
chapter.dateUpload,
|
chapter.dateUpload,
|
||||||
chapter.version,
|
chapter.version,
|
||||||
)
|
).awaitAsOne()
|
||||||
val lastInsertId = chaptersQueries.selectLastInsertedRowId().executeAsOne()
|
|
||||||
chapter.copy(id = lastInsertId)
|
chapter.copy(id = lastInsertId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,9 +53,9 @@ class ChapterRepositoryImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun partialUpdate(vararg chapterUpdates: ChapterUpdate) {
|
private suspend fun partialUpdate(vararg chapterUpdates: ChapterUpdate) {
|
||||||
handler.await(inTransaction = true) {
|
database.transaction {
|
||||||
chapterUpdates.forEach { chapterUpdate ->
|
chapterUpdates.forEach { chapterUpdate ->
|
||||||
chaptersQueries.update(
|
database.chaptersQueries.update(
|
||||||
mangaId = chapterUpdate.mangaId,
|
mangaId = chapterUpdate.mangaId,
|
||||||
url = chapterUpdate.url,
|
url = chapterUpdate.url,
|
||||||
name = chapterUpdate.name,
|
name = chapterUpdate.name,
|
||||||
@@ -74,59 +77,55 @@ class ChapterRepositoryImpl(
|
|||||||
|
|
||||||
override suspend fun removeChaptersWithIds(chapterIds: List<Long>) {
|
override suspend fun removeChaptersWithIds(chapterIds: List<Long>) {
|
||||||
try {
|
try {
|
||||||
handler.await { chaptersQueries.removeChaptersWithIds(chapterIds) }
|
database.chaptersQueries.removeChaptersWithIds(chapterIds)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, e)
|
logcat(LogPriority.ERROR, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getChapterByMangaId(mangaId: Long, applyScanlatorFilter: Boolean): List<Chapter> {
|
override suspend fun getChapterByMangaId(mangaId: Long, applyScanlatorFilter: Boolean): List<Chapter> {
|
||||||
return handler.awaitList {
|
return database.chaptersQueries
|
||||||
chaptersQueries.getChaptersByMangaId(mangaId, applyScanlatorFilter.toLong(), ::mapChapter)
|
.getChaptersByMangaId(mangaId, applyScanlatorFilter.toLong(), ::mapChapter)
|
||||||
}
|
.awaitAsList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getScanlatorsByMangaId(mangaId: Long): List<String> {
|
override suspend fun getScanlatorsByMangaId(mangaId: Long): List<String> {
|
||||||
return handler.awaitList {
|
return database.chaptersQueries
|
||||||
chaptersQueries.getScanlatorsByMangaId(mangaId) { it.orEmpty() }
|
.getScanlatorsByMangaId(mangaId) { it.orEmpty() }
|
||||||
}
|
.awaitAsList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getScanlatorsByMangaIdAsFlow(mangaId: Long): Flow<List<String>> {
|
override fun getScanlatorsByMangaIdAsFlow(mangaId: Long): Flow<List<String>> {
|
||||||
return handler.subscribeToList {
|
return database.chaptersQueries
|
||||||
chaptersQueries.getScanlatorsByMangaId(mangaId) { it.orEmpty() }
|
.getScanlatorsByMangaId(mangaId) { it.orEmpty() }
|
||||||
}
|
.subscribeToList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getBookmarkedChaptersByMangaId(mangaId: Long): List<Chapter> {
|
override suspend fun getBookmarkedChaptersByMangaId(mangaId: Long): List<Chapter> {
|
||||||
return handler.awaitList {
|
return database.chaptersQueries
|
||||||
chaptersQueries.getBookmarkedChaptersByMangaId(
|
.getBookmarkedChaptersByMangaId(mangaId, ::mapChapter)
|
||||||
mangaId,
|
.awaitAsList()
|
||||||
::mapChapter,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getChapterById(id: Long): Chapter? {
|
override suspend fun getChapterById(id: Long): Chapter? {
|
||||||
return handler.awaitOneOrNull { chaptersQueries.getChapterById(id, ::mapChapter) }
|
return database.chaptersQueries
|
||||||
|
.getChapterById(id, ::mapChapter)
|
||||||
|
.awaitAsOneOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getChapterByMangaIdAsFlow(mangaId: Long, applyScanlatorFilter: Boolean): Flow<List<Chapter>> {
|
override suspend fun getChapterByMangaIdAsFlow(mangaId: Long, applyScanlatorFilter: Boolean): Flow<List<Chapter>> {
|
||||||
return handler.subscribeToList {
|
return database.chaptersQueries
|
||||||
chaptersQueries.getChaptersByMangaId(mangaId, applyScanlatorFilter.toLong(), ::mapChapter)
|
.getChaptersByMangaId(mangaId, applyScanlatorFilter.toLong(), ::mapChapter)
|
||||||
}
|
.subscribeToList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getChapterByUrlAndMangaId(url: String, mangaId: Long): Chapter? {
|
override suspend fun getChapterByUrlAndMangaId(url: String, mangaId: Long): Chapter? {
|
||||||
return handler.awaitOneOrNull {
|
return database.chaptersQueries
|
||||||
chaptersQueries.getChapterByUrlAndMangaId(
|
.getChapterByUrlAndMangaId(url, mangaId, ::mapChapter)
|
||||||
url,
|
.awaitAsOneOrNull()
|
||||||
mangaId,
|
|
||||||
::mapChapter,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
private fun mapChapter(
|
private fun mapChapter(
|
||||||
id: Long,
|
id: Long,
|
||||||
mangaId: Long,
|
mangaId: Long,
|
||||||
@@ -142,7 +141,6 @@ class ChapterRepositoryImpl(
|
|||||||
dateUpload: Long,
|
dateUpload: Long,
|
||||||
lastModifiedAt: Long,
|
lastModifiedAt: Long,
|
||||||
version: Long,
|
version: Long,
|
||||||
@Suppress("UNUSED_PARAMETER")
|
|
||||||
isSyncing: Long,
|
isSyncing: Long,
|
||||||
): Chapter = Chapter(
|
): Chapter = Chapter(
|
||||||
id = id,
|
id = id,
|
||||||
|
|||||||
@@ -1,41 +1,49 @@
|
|||||||
package tachiyomi.data.history
|
package tachiyomi.data.history
|
||||||
|
|
||||||
|
import app.cash.sqldelight.async.coroutines.awaitAsList
|
||||||
|
import app.cash.sqldelight.async.coroutines.awaitAsOne
|
||||||
|
import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import tachiyomi.data.DatabaseHandler
|
import tachiyomi.data.Database
|
||||||
|
import tachiyomi.data.subscribeToList
|
||||||
import tachiyomi.domain.history.model.History
|
import tachiyomi.domain.history.model.History
|
||||||
import tachiyomi.domain.history.model.HistoryUpdate
|
import tachiyomi.domain.history.model.HistoryUpdate
|
||||||
import tachiyomi.domain.history.model.HistoryWithRelations
|
import tachiyomi.domain.history.model.HistoryWithRelations
|
||||||
import tachiyomi.domain.history.repository.HistoryRepository
|
import tachiyomi.domain.history.repository.HistoryRepository
|
||||||
|
|
||||||
class HistoryRepositoryImpl(
|
class HistoryRepositoryImpl(
|
||||||
private val handler: DatabaseHandler,
|
private val database: Database,
|
||||||
) : HistoryRepository {
|
) : HistoryRepository {
|
||||||
|
|
||||||
override fun getHistory(query: String): Flow<List<HistoryWithRelations>> {
|
override fun getHistory(query: String): Flow<List<HistoryWithRelations>> {
|
||||||
return handler.subscribeToList {
|
return database.historyViewQueries
|
||||||
historyViewQueries.history(query, HistoryMapper::mapHistoryWithRelations)
|
.history(query, HistoryMapper::mapHistoryWithRelations)
|
||||||
}
|
.subscribeToList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getLastHistory(): HistoryWithRelations? {
|
override suspend fun getLastHistory(): HistoryWithRelations? {
|
||||||
return handler.awaitOneOrNull {
|
return database.historyViewQueries
|
||||||
historyViewQueries.getLatestHistory(HistoryMapper::mapHistoryWithRelations)
|
.getLatestHistory(HistoryMapper::mapHistoryWithRelations)
|
||||||
}
|
.awaitAsOneOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getTotalReadDuration(): Long {
|
override suspend fun getTotalReadDuration(): Long {
|
||||||
return handler.awaitOne { historyQueries.getReadDuration() }
|
return database.historyQueries
|
||||||
|
.getReadDuration()
|
||||||
|
.awaitAsOne()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getHistoryByMangaId(mangaId: Long): List<History> {
|
override suspend fun getHistoryByMangaId(mangaId: Long): List<History> {
|
||||||
return handler.awaitList { historyQueries.getHistoryByMangaId(mangaId, HistoryMapper::mapHistory) }
|
return database.historyQueries
|
||||||
|
.getHistoryByMangaId(mangaId, HistoryMapper::mapHistory)
|
||||||
|
.awaitAsList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun resetHistory(historyId: Long) {
|
override suspend fun resetHistory(historyId: Long) {
|
||||||
try {
|
try {
|
||||||
handler.await { historyQueries.resetHistoryById(historyId) }
|
database.historyQueries.resetHistoryById(historyId)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, throwable = e)
|
logcat(LogPriority.ERROR, throwable = e)
|
||||||
}
|
}
|
||||||
@@ -43,7 +51,7 @@ class HistoryRepositoryImpl(
|
|||||||
|
|
||||||
override suspend fun resetHistoryByMangaId(mangaId: Long) {
|
override suspend fun resetHistoryByMangaId(mangaId: Long) {
|
||||||
try {
|
try {
|
||||||
handler.await { historyQueries.resetHistoryByMangaId(mangaId) }
|
database.historyQueries.resetHistoryByMangaId(mangaId)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, throwable = e)
|
logcat(LogPriority.ERROR, throwable = e)
|
||||||
}
|
}
|
||||||
@@ -51,7 +59,7 @@ class HistoryRepositoryImpl(
|
|||||||
|
|
||||||
override suspend fun deleteAllHistory(): Boolean {
|
override suspend fun deleteAllHistory(): Boolean {
|
||||||
return try {
|
return try {
|
||||||
handler.await { historyQueries.removeAllHistory() }
|
database.historyQueries.removeAllHistory()
|
||||||
true
|
true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, throwable = e)
|
logcat(LogPriority.ERROR, throwable = e)
|
||||||
@@ -61,13 +69,11 @@ class HistoryRepositoryImpl(
|
|||||||
|
|
||||||
override suspend fun upsertHistory(historyUpdate: HistoryUpdate) {
|
override suspend fun upsertHistory(historyUpdate: HistoryUpdate) {
|
||||||
try {
|
try {
|
||||||
handler.await {
|
database.historyQueries.upsert(
|
||||||
historyQueries.upsert(
|
historyUpdate.chapterId,
|
||||||
historyUpdate.chapterId,
|
historyUpdate.readAt,
|
||||||
historyUpdate.readAt,
|
historyUpdate.sessionReadDuration,
|
||||||
historyUpdate.sessionReadDuration,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, throwable = e)
|
logcat(LogPriority.ERROR, throwable = e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import tachiyomi.domain.manga.model.Manga
|
|||||||
import tachiyomi.domain.manga.model.MangaWithChapterCount
|
import tachiyomi.domain.manga.model.MangaWithChapterCount
|
||||||
|
|
||||||
object MangaMapper {
|
object MangaMapper {
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
fun mapManga(
|
fun mapManga(
|
||||||
id: Long,
|
id: Long,
|
||||||
source: Long,
|
source: Long,
|
||||||
@@ -30,7 +31,6 @@ object MangaMapper {
|
|||||||
lastModifiedAt: Long,
|
lastModifiedAt: Long,
|
||||||
favoriteModifiedAt: Long?,
|
favoriteModifiedAt: Long?,
|
||||||
version: Long,
|
version: Long,
|
||||||
@Suppress("UNUSED_PARAMETER")
|
|
||||||
isSyncing: Long,
|
isSyncing: Long,
|
||||||
notes: String,
|
notes: String,
|
||||||
): Manga = Manga(
|
): Manga = Manga(
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
package tachiyomi.data.manga
|
package tachiyomi.data.manga
|
||||||
|
|
||||||
|
import app.cash.sqldelight.async.coroutines.awaitAsList
|
||||||
|
import app.cash.sqldelight.async.coroutines.awaitAsOne
|
||||||
|
import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import tachiyomi.data.DatabaseHandler
|
import tachiyomi.data.Database
|
||||||
import tachiyomi.data.StringListColumnAdapter
|
import tachiyomi.data.StringListColumnAdapter
|
||||||
import tachiyomi.data.UpdateStrategyColumnAdapter
|
import tachiyomi.data.UpdateStrategyColumnAdapter
|
||||||
|
import tachiyomi.data.subscribeToList
|
||||||
|
import tachiyomi.data.subscribeToOne
|
||||||
|
import tachiyomi.data.subscribeToOneOrNull
|
||||||
import tachiyomi.domain.library.model.LibraryManga
|
import tachiyomi.domain.library.model.LibraryManga
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.manga.model.MangaUpdate
|
import tachiyomi.domain.manga.model.MangaUpdate
|
||||||
@@ -15,73 +21,79 @@ import java.time.LocalDate
|
|||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
|
|
||||||
class MangaRepositoryImpl(
|
class MangaRepositoryImpl(
|
||||||
private val handler: DatabaseHandler,
|
private val database: Database,
|
||||||
) : MangaRepository {
|
) : MangaRepository {
|
||||||
|
|
||||||
override suspend fun getMangaById(id: Long): Manga {
|
override suspend fun getMangaById(id: Long): Manga {
|
||||||
return handler.awaitOne { mangasQueries.getMangaById(id, MangaMapper::mapManga) }
|
return database.mangasQueries
|
||||||
|
.getMangaById(id, MangaMapper::mapManga)
|
||||||
|
.awaitAsOne()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getMangaByIdAsFlow(id: Long): Flow<Manga> {
|
override suspend fun getMangaByIdAsFlow(id: Long): Flow<Manga> {
|
||||||
return handler.subscribeToOne { mangasQueries.getMangaById(id, MangaMapper::mapManga) }
|
return database.mangasQueries
|
||||||
|
.getMangaById(id, MangaMapper::mapManga)
|
||||||
|
.subscribeToOne()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getMangaByUrlAndSourceId(url: String, sourceId: Long): Manga? {
|
override suspend fun getMangaByUrlAndSourceId(url: String, sourceId: Long): Manga? {
|
||||||
return handler.awaitOneOrNull {
|
return database.mangasQueries
|
||||||
mangasQueries.getMangaByUrlAndSource(
|
.getMangaByUrlAndSource(url, sourceId, MangaMapper::mapManga)
|
||||||
url,
|
.awaitAsOneOrNull()
|
||||||
sourceId,
|
|
||||||
MangaMapper::mapManga,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getMangaByUrlAndSourceIdAsFlow(url: String, sourceId: Long): Flow<Manga?> {
|
override fun getMangaByUrlAndSourceIdAsFlow(url: String, sourceId: Long): Flow<Manga?> {
|
||||||
return handler.subscribeToOneOrNull {
|
return database.mangasQueries
|
||||||
mangasQueries.getMangaByUrlAndSource(
|
.getMangaByUrlAndSource(url, sourceId, MangaMapper::mapManga)
|
||||||
url,
|
.subscribeToOneOrNull()
|
||||||
sourceId,
|
|
||||||
MangaMapper::mapManga,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getFavorites(): List<Manga> {
|
override suspend fun getFavorites(): List<Manga> {
|
||||||
return handler.awaitList { mangasQueries.getFavorites(MangaMapper::mapManga) }
|
return database.mangasQueries
|
||||||
|
.getFavorites(MangaMapper::mapManga)
|
||||||
|
.awaitAsList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getReadMangaNotInLibrary(): List<Manga> {
|
override suspend fun getReadMangaNotInLibrary(): List<Manga> {
|
||||||
return handler.awaitList { mangasQueries.getReadMangaNotInLibrary(MangaMapper::mapManga) }
|
return database.mangasQueries
|
||||||
|
.getReadMangaNotInLibrary(MangaMapper::mapManga)
|
||||||
|
.awaitAsList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getLibraryManga(): List<LibraryManga> {
|
override suspend fun getLibraryManga(): List<LibraryManga> {
|
||||||
return handler.awaitList { libraryViewQueries.library(MangaMapper::mapLibraryManga) }
|
return database.libraryViewQueries
|
||||||
|
.library(MangaMapper::mapLibraryManga)
|
||||||
|
.awaitAsList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLibraryMangaAsFlow(): Flow<List<LibraryManga>> {
|
override fun getLibraryMangaAsFlow(): Flow<List<LibraryManga>> {
|
||||||
return handler.subscribeToList { libraryViewQueries.library(MangaMapper::mapLibraryManga) }
|
return database.libraryViewQueries
|
||||||
|
.library(MangaMapper::mapLibraryManga)
|
||||||
|
.subscribeToList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getFavoritesBySourceId(sourceId: Long): Flow<List<Manga>> {
|
override fun getFavoritesBySourceId(sourceId: Long): Flow<List<Manga>> {
|
||||||
return handler.subscribeToList { mangasQueries.getFavoriteBySourceId(sourceId, MangaMapper::mapManga) }
|
return database.mangasQueries
|
||||||
|
.getFavoriteBySourceId(sourceId, MangaMapper::mapManga)
|
||||||
|
.subscribeToList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getDuplicateLibraryManga(id: Long, title: String): List<MangaWithChapterCount> {
|
override suspend fun getDuplicateLibraryManga(id: Long, title: String): List<MangaWithChapterCount> {
|
||||||
return handler.awaitList {
|
return database.mangasQueries
|
||||||
mangasQueries.getDuplicateLibraryManga(id, title, MangaMapper::mapMangaWithChapterCount)
|
.getDuplicateLibraryManga(id, title, MangaMapper::mapMangaWithChapterCount)
|
||||||
}
|
.awaitAsList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getUpcomingManga(statuses: Set<Long>): Flow<List<Manga>> {
|
override suspend fun getUpcomingManga(statuses: Set<Long>): Flow<List<Manga>> {
|
||||||
val epochMillis = LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toEpochSecond() * 1000
|
val epochMillis = LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toEpochSecond() * 1000
|
||||||
return handler.subscribeToList {
|
return database.mangasQueries
|
||||||
mangasQueries.getUpcomingManga(epochMillis, statuses, MangaMapper::mapManga)
|
.getUpcomingManga(epochMillis, statuses, MangaMapper::mapManga)
|
||||||
}
|
.subscribeToList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun resetViewerFlags(): Boolean {
|
override suspend fun resetViewerFlags(): Boolean {
|
||||||
return try {
|
return try {
|
||||||
handler.await { mangasQueries.resetViewerFlags() }
|
database.mangasQueries.resetViewerFlags()
|
||||||
true
|
true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, e)
|
logcat(LogPriority.ERROR, e)
|
||||||
@@ -90,10 +102,10 @@ class MangaRepositoryImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setMangaCategories(mangaId: Long, categoryIds: List<Long>) {
|
override suspend fun setMangaCategories(mangaId: Long, categoryIds: List<Long>) {
|
||||||
handler.await(inTransaction = true) {
|
database.transaction {
|
||||||
mangas_categoriesQueries.deleteMangaCategoryByMangaId(mangaId)
|
database.mangas_categoriesQueries.deleteMangaCategoryByMangaId(mangaId)
|
||||||
categoryIds.map { categoryId ->
|
categoryIds.forEach { categoryId ->
|
||||||
mangas_categoriesQueries.insert(mangaId, categoryId)
|
database.mangas_categoriesQueries.insert(mangaId, categoryId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,9 +131,9 @@ class MangaRepositoryImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun insertNetworkManga(manga: List<Manga>): List<Manga> {
|
override suspend fun insertNetworkManga(manga: List<Manga>): List<Manga> {
|
||||||
return handler.await(inTransaction = true) {
|
return database.transactionWithResult {
|
||||||
manga.map {
|
manga.map {
|
||||||
mangasQueries.insertNetworkManga(
|
database.mangasQueries.insertNetworkManga(
|
||||||
source = it.source,
|
source = it.source,
|
||||||
url = it.url,
|
url = it.url,
|
||||||
artist = it.artist,
|
artist = it.artist,
|
||||||
@@ -147,15 +159,15 @@ class MangaRepositoryImpl(
|
|||||||
updateDetails = it.initialized,
|
updateDetails = it.initialized,
|
||||||
mapper = MangaMapper::mapManga,
|
mapper = MangaMapper::mapManga,
|
||||||
)
|
)
|
||||||
.executeAsOne()
|
.awaitAsOne()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun partialUpdate(vararg mangaUpdates: MangaUpdate) {
|
private suspend fun partialUpdate(vararg mangaUpdates: MangaUpdate) {
|
||||||
handler.await(inTransaction = true) {
|
database.transaction {
|
||||||
mangaUpdates.forEach { value ->
|
mangaUpdates.forEach { value ->
|
||||||
mangasQueries.update(
|
database.mangasQueries.update(
|
||||||
source = value.source,
|
source = value.source,
|
||||||
url = value.url,
|
url = value.url,
|
||||||
artist = value.artist,
|
artist = value.artist,
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import eu.kanade.tachiyomi.source.online.HttpSource
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import tachiyomi.data.DatabaseHandler
|
import tachiyomi.data.Database
|
||||||
|
import tachiyomi.data.subscribeToList
|
||||||
import tachiyomi.domain.source.model.SourceWithCount
|
import tachiyomi.domain.source.model.SourceWithCount
|
||||||
import tachiyomi.domain.source.model.StubSource
|
import tachiyomi.domain.source.model.StubSource
|
||||||
import tachiyomi.domain.source.repository.SourcePagingSource
|
import tachiyomi.domain.source.repository.SourcePagingSource
|
||||||
@@ -17,7 +18,7 @@ import tachiyomi.domain.source.model.Source as DomainSource
|
|||||||
|
|
||||||
class SourceRepositoryImpl(
|
class SourceRepositoryImpl(
|
||||||
private val sourceManager: SourceManager,
|
private val sourceManager: SourceManager,
|
||||||
private val handler: DatabaseHandler,
|
private val database: Database,
|
||||||
) : SourceRepository {
|
) : SourceRepository {
|
||||||
|
|
||||||
override fun getSources(): Flow<List<DomainSource>> {
|
override fun getSources(): Flow<List<DomainSource>> {
|
||||||
@@ -39,10 +40,12 @@ class SourceRepositoryImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getSourcesWithFavoriteCount(): Flow<List<Pair<DomainSource, Long>>> {
|
override fun getSourcesWithFavoriteCount(): Flow<List<Pair<DomainSource, Long>>> {
|
||||||
return combine(
|
val sourceIdWithFavoriteCountFlow = database.mangasQueries
|
||||||
handler.subscribeToList { mangasQueries.getSourceIdWithFavoriteCount() },
|
.getSourceIdWithFavoriteCount()
|
||||||
sourceManager.catalogueSources,
|
.subscribeToList()
|
||||||
) { sourceIdWithFavoriteCount, _ -> sourceIdWithFavoriteCount }
|
return combine(sourceIdWithFavoriteCountFlow, sourceManager.catalogueSources) { sourceIdWithFavoriteCount, _ ->
|
||||||
|
sourceIdWithFavoriteCount
|
||||||
|
}
|
||||||
.map {
|
.map {
|
||||||
it.map { (sourceId, count) ->
|
it.map { (sourceId, count) ->
|
||||||
val source = sourceManager.getOrStub(sourceId)
|
val source = sourceManager.getOrStub(sourceId)
|
||||||
@@ -55,17 +58,18 @@ class SourceRepositoryImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getSourcesWithNonLibraryManga(): Flow<List<SourceWithCount>> {
|
override fun getSourcesWithNonLibraryManga(): Flow<List<SourceWithCount>> {
|
||||||
val sourceIdWithNonLibraryManga =
|
return database.mangasQueries
|
||||||
handler.subscribeToList { mangasQueries.getSourceIdsWithNonLibraryManga() }
|
.getSourceIdsWithNonLibraryManga()
|
||||||
return sourceIdWithNonLibraryManga.map { sourceId ->
|
.subscribeToList()
|
||||||
sourceId.map { (sourceId, count) ->
|
.map { sourceId ->
|
||||||
val source = sourceManager.getOrStub(sourceId)
|
sourceId.map { (sourceId, count) ->
|
||||||
val domainSource = mapSourceToDomainSource(source).copy(
|
val source = sourceManager.getOrStub(sourceId)
|
||||||
isStub = source is StubSource,
|
val domainSource = mapSourceToDomainSource(source).copy(
|
||||||
)
|
isStub = source is StubSource,
|
||||||
SourceWithCount(domainSource, count)
|
)
|
||||||
|
SourceWithCount(domainSource, count)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun search(
|
override fun search(
|
||||||
|
|||||||
@@ -1,24 +1,30 @@
|
|||||||
package tachiyomi.data.source
|
package tachiyomi.data.source
|
||||||
|
|
||||||
|
import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import tachiyomi.data.DatabaseHandler
|
import tachiyomi.data.Database
|
||||||
|
import tachiyomi.data.subscribeToList
|
||||||
import tachiyomi.domain.source.model.StubSource
|
import tachiyomi.domain.source.model.StubSource
|
||||||
import tachiyomi.domain.source.repository.StubSourceRepository
|
import tachiyomi.domain.source.repository.StubSourceRepository
|
||||||
|
|
||||||
class StubSourceRepositoryImpl(
|
class StubSourceRepositoryImpl(
|
||||||
private val handler: DatabaseHandler,
|
private val database: Database,
|
||||||
) : StubSourceRepository {
|
) : StubSourceRepository {
|
||||||
|
|
||||||
override fun subscribeAll(): Flow<List<StubSource>> {
|
override fun subscribeAll(): Flow<List<StubSource>> {
|
||||||
return handler.subscribeToList { sourcesQueries.findAll(::mapStubSource) }
|
return database.sourcesQueries
|
||||||
|
.findAll(::mapStubSource)
|
||||||
|
.subscribeToList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getStubSource(id: Long): StubSource? {
|
override suspend fun getStubSource(id: Long): StubSource? {
|
||||||
return handler.awaitOneOrNull { sourcesQueries.findOne(id, ::mapStubSource) }
|
return database.sourcesQueries
|
||||||
|
.findOne(id, ::mapStubSource)
|
||||||
|
.awaitAsOneOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun upsertStubSource(id: Long, lang: String, name: String) {
|
override suspend fun upsertStubSource(id: Long, lang: String, name: String) {
|
||||||
handler.await { sourcesQueries.upsert(id, lang, name) }
|
database.sourcesQueries.upsert(id, lang, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun mapStubSource(
|
private fun mapStubSource(
|
||||||
|
|||||||
@@ -1,43 +1,46 @@
|
|||||||
package tachiyomi.data.track
|
package tachiyomi.data.track
|
||||||
|
|
||||||
|
import app.cash.sqldelight.async.coroutines.awaitAsList
|
||||||
|
import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import tachiyomi.data.DatabaseHandler
|
import tachiyomi.data.Database
|
||||||
|
import tachiyomi.data.subscribeToList
|
||||||
import tachiyomi.domain.track.model.Track
|
import tachiyomi.domain.track.model.Track
|
||||||
import tachiyomi.domain.track.repository.TrackRepository
|
import tachiyomi.domain.track.repository.TrackRepository
|
||||||
|
|
||||||
class TrackRepositoryImpl(
|
class TrackRepositoryImpl(
|
||||||
private val handler: DatabaseHandler,
|
private val database: Database,
|
||||||
) : TrackRepository {
|
) : TrackRepository {
|
||||||
|
|
||||||
override suspend fun getTrackById(id: Long): Track? {
|
override suspend fun getTrackById(id: Long): Track? {
|
||||||
return handler.awaitOneOrNull { manga_syncQueries.getTrackById(id, TrackMapper::mapTrack) }
|
return database.manga_syncQueries
|
||||||
|
.getTrackById(id, TrackMapper::mapTrack)
|
||||||
|
.awaitAsOneOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getTracksByMangaId(mangaId: Long): List<Track> {
|
override suspend fun getTracksByMangaId(mangaId: Long): List<Track> {
|
||||||
return handler.awaitList {
|
return database.manga_syncQueries
|
||||||
manga_syncQueries.getTracksByMangaId(mangaId, TrackMapper::mapTrack)
|
.getTracksByMangaId(mangaId, TrackMapper::mapTrack)
|
||||||
}
|
.awaitAsList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getTracksAsFlow(): Flow<List<Track>> {
|
override fun getTracksAsFlow(): Flow<List<Track>> {
|
||||||
return handler.subscribeToList {
|
return database.manga_syncQueries
|
||||||
manga_syncQueries.getTracks(TrackMapper::mapTrack)
|
.getTracks(TrackMapper::mapTrack)
|
||||||
}
|
.subscribeToList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getTracksByMangaIdAsFlow(mangaId: Long): Flow<List<Track>> {
|
override fun getTracksByMangaIdAsFlow(mangaId: Long): Flow<List<Track>> {
|
||||||
return handler.subscribeToList {
|
return database.manga_syncQueries
|
||||||
manga_syncQueries.getTracksByMangaId(mangaId, TrackMapper::mapTrack)
|
.getTracksByMangaId(mangaId, TrackMapper::mapTrack)
|
||||||
}
|
.subscribeToList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun delete(mangaId: Long, trackerId: Long) {
|
override suspend fun delete(mangaId: Long, trackerId: Long) {
|
||||||
handler.await {
|
database.manga_syncQueries.delete(
|
||||||
manga_syncQueries.delete(
|
mangaId = mangaId,
|
||||||
mangaId = mangaId,
|
syncId = trackerId,
|
||||||
syncId = trackerId,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun insert(track: Track) {
|
override suspend fun insert(track: Track) {
|
||||||
@@ -49,9 +52,9 @@ class TrackRepositoryImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun insertValues(vararg tracks: Track) {
|
private suspend fun insertValues(vararg tracks: Track) {
|
||||||
handler.await(inTransaction = true) {
|
database.transaction {
|
||||||
tracks.forEach { mangaTrack ->
|
tracks.forEach { mangaTrack ->
|
||||||
manga_syncQueries.insert(
|
database.manga_syncQueries.insert(
|
||||||
mangaId = mangaTrack.mangaId,
|
mangaId = mangaTrack.mangaId,
|
||||||
syncId = mangaTrack.trackerId,
|
syncId = mangaTrack.trackerId,
|
||||||
remoteId = mangaTrack.remoteId,
|
remoteId = mangaTrack.remoteId,
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
package tachiyomi.data.updates
|
package tachiyomi.data.updates
|
||||||
|
|
||||||
|
import app.cash.sqldelight.async.coroutines.awaitAsList
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import tachiyomi.core.common.util.lang.toLong
|
import tachiyomi.core.common.util.lang.toLong
|
||||||
import tachiyomi.data.DatabaseHandler
|
import tachiyomi.data.Database
|
||||||
|
import tachiyomi.data.subscribeToList
|
||||||
import tachiyomi.domain.manga.model.MangaCover
|
import tachiyomi.domain.manga.model.MangaCover
|
||||||
import tachiyomi.domain.updates.model.UpdatesWithRelations
|
import tachiyomi.domain.updates.model.UpdatesWithRelations
|
||||||
import tachiyomi.domain.updates.repository.UpdatesRepository
|
import tachiyomi.domain.updates.repository.UpdatesRepository
|
||||||
|
|
||||||
class UpdatesRepositoryImpl(
|
class UpdatesRepositoryImpl(
|
||||||
private val databaseHandler: DatabaseHandler,
|
private val database: Database,
|
||||||
) : UpdatesRepository {
|
) : UpdatesRepository {
|
||||||
|
|
||||||
override suspend fun awaitWithRead(
|
override suspend fun awaitWithRead(
|
||||||
@@ -16,14 +18,14 @@ class UpdatesRepositoryImpl(
|
|||||||
after: Long,
|
after: Long,
|
||||||
limit: Long,
|
limit: Long,
|
||||||
): List<UpdatesWithRelations> {
|
): List<UpdatesWithRelations> {
|
||||||
return databaseHandler.awaitList {
|
return database.updatesViewQueries
|
||||||
updatesViewQueries.getUpdatesByReadStatus(
|
.getUpdatesByReadStatus(
|
||||||
read = read,
|
read = read,
|
||||||
after = after,
|
after = after,
|
||||||
limit = limit,
|
limit = limit,
|
||||||
mapper = ::mapUpdatesWithRelations,
|
mapper = ::mapUpdatesWithRelations,
|
||||||
)
|
)
|
||||||
}
|
.awaitAsList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun subscribeAll(
|
override fun subscribeAll(
|
||||||
@@ -34,18 +36,17 @@ class UpdatesRepositoryImpl(
|
|||||||
bookmarked: Boolean?,
|
bookmarked: Boolean?,
|
||||||
hideExcludedScanlators: Boolean,
|
hideExcludedScanlators: Boolean,
|
||||||
): Flow<List<UpdatesWithRelations>> {
|
): Flow<List<UpdatesWithRelations>> {
|
||||||
return databaseHandler.subscribeToList {
|
return database.updatesViewQueries
|
||||||
updatesViewQueries.getRecentUpdatesWithFilters(
|
.getRecentUpdatesWithFilters(
|
||||||
after = after,
|
after = after,
|
||||||
limit = limit,
|
limit = limit,
|
||||||
// invert because unread in Kotlin -> read column in SQL
|
|
||||||
read = unread?.let { !it },
|
read = unread?.let { !it },
|
||||||
started = started?.toLong(),
|
started = started?.toLong(),
|
||||||
bookmarked = bookmarked,
|
bookmarked = bookmarked,
|
||||||
hideExcludedScanlators = hideExcludedScanlators.toLong(),
|
hideExcludedScanlators = hideExcludedScanlators.toLong(),
|
||||||
mapper = ::mapUpdatesWithRelations,
|
mapper = ::mapUpdatesWithRelations,
|
||||||
)
|
)
|
||||||
}
|
.subscribeToList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun subscribeWithRead(
|
override fun subscribeWithRead(
|
||||||
@@ -53,16 +54,17 @@ class UpdatesRepositoryImpl(
|
|||||||
after: Long,
|
after: Long,
|
||||||
limit: Long,
|
limit: Long,
|
||||||
): Flow<List<UpdatesWithRelations>> {
|
): Flow<List<UpdatesWithRelations>> {
|
||||||
return databaseHandler.subscribeToList {
|
return database.updatesViewQueries
|
||||||
updatesViewQueries.getUpdatesByReadStatus(
|
.getUpdatesByReadStatus(
|
||||||
read = read,
|
read = read,
|
||||||
after = after,
|
after = after,
|
||||||
limit = limit,
|
limit = limit,
|
||||||
mapper = ::mapUpdatesWithRelations,
|
mapper = ::mapUpdatesWithRelations,
|
||||||
)
|
)
|
||||||
}
|
.subscribeToList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
private fun mapUpdatesWithRelations(
|
private fun mapUpdatesWithRelations(
|
||||||
mangaId: Long,
|
mangaId: Long,
|
||||||
mangaTitle: String,
|
mangaTitle: String,
|
||||||
|
|||||||
@@ -42,9 +42,12 @@ JOIN mangas_categories MC
|
|||||||
ON C._id = MC.category_id
|
ON C._id = MC.category_id
|
||||||
WHERE MC.manga_id = :mangaId;
|
WHERE MC.manga_id = :mangaId;
|
||||||
|
|
||||||
insert:
|
insert {
|
||||||
INSERT INTO categories(name, sort, flags)
|
INSERT INTO categories(name, sort, flags)
|
||||||
VALUES (:name, :order, :flags);
|
VALUES (:name, :order, :flags);
|
||||||
|
|
||||||
|
SELECT last_insert_rowid();
|
||||||
|
}
|
||||||
|
|
||||||
delete:
|
delete:
|
||||||
DELETE FROM categories
|
DELETE FROM categories
|
||||||
@@ -60,6 +63,3 @@ WHERE _id = :categoryId;
|
|||||||
updateAllFlags:
|
updateAllFlags:
|
||||||
UPDATE categories SET
|
UPDATE categories SET
|
||||||
flags = coalesce(?, flags);
|
flags = coalesce(?, flags);
|
||||||
|
|
||||||
selectLastInsertedRowId:
|
|
||||||
SELECT last_insert_rowid();
|
|
||||||
@@ -97,10 +97,12 @@ UPDATE chapters
|
|||||||
SET is_syncing = 0
|
SET is_syncing = 0
|
||||||
WHERE is_syncing = 1;
|
WHERE is_syncing = 1;
|
||||||
|
|
||||||
insert:
|
insert {
|
||||||
INSERT INTO chapters(manga_id, url, name, scanlator, read, bookmark, last_page_read, chapter_number, source_order, date_fetch, date_upload, last_modified_at, version, is_syncing)
|
INSERT INTO chapters(manga_id, url, name, scanlator, read, bookmark, last_page_read, chapter_number, source_order, date_fetch, date_upload, last_modified_at, version, is_syncing)
|
||||||
VALUES (:mangaId, :url, :name, :scanlator, :read, :bookmark, :lastPageRead, :chapterNumber, :sourceOrder, :dateFetch, :dateUpload, 0, :version, 0);
|
VALUES (:mangaId, :url, :name, :scanlator, :read, :bookmark, :lastPageRead, :chapterNumber, :sourceOrder, :dateFetch, :dateUpload, 0, :version, 0);
|
||||||
|
|
||||||
|
SELECT last_insert_rowid();
|
||||||
|
}
|
||||||
update:
|
update:
|
||||||
UPDATE chapters
|
UPDATE chapters
|
||||||
SET manga_id = coalesce(:mangaId, manga_id),
|
SET manga_id = coalesce(:mangaId, manga_id),
|
||||||
@@ -117,6 +119,3 @@ SET manga_id = coalesce(:mangaId, manga_id),
|
|||||||
version = coalesce(:version, version),
|
version = coalesce(:version, version),
|
||||||
is_syncing = coalesce(:isSyncing, is_syncing)
|
is_syncing = coalesce(:isSyncing, is_syncing)
|
||||||
WHERE _id = :chapterId;
|
WHERE _id = :chapterId;
|
||||||
|
|
||||||
selectLastInsertedRowId:
|
|
||||||
SELECT last_insert_rowid();
|
|
||||||
@@ -193,9 +193,12 @@ AND (
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
insert:
|
insert {
|
||||||
INSERT INTO mangas(source, url, artist, author, description, genre, title, status, thumbnail_url, favorite, last_update, next_update, initialized, viewer, chapter_flags, cover_last_modified, date_added, update_strategy, calculate_interval, last_modified_at, version, notes)
|
INSERT INTO mangas(source, url, artist, author, description, genre, title, status, thumbnail_url, favorite, last_update, next_update, initialized, viewer, chapter_flags, cover_last_modified, date_added, update_strategy, calculate_interval, last_modified_at, version, notes)
|
||||||
VALUES (:source, :url, :artist, :author, :description, :genre, :title, :status, :thumbnailUrl, :favorite, :lastUpdate, :nextUpdate, :initialized, :viewerFlags, :chapterFlags, :coverLastModified, :dateAdded, :updateStrategy, :calculateInterval, 0, :version, :notes);
|
VALUES (:source, :url, :artist, :author, :description, :genre, :title, :status, :thumbnailUrl, :favorite, :lastUpdate, :nextUpdate, :initialized, :viewerFlags, :chapterFlags, :coverLastModified, :dateAdded, :updateStrategy, :calculateInterval, 0, :version, :notes);
|
||||||
|
|
||||||
|
SELECT last_insert_rowid();
|
||||||
|
}
|
||||||
|
|
||||||
update:
|
update:
|
||||||
UPDATE mangas SET
|
UPDATE mangas SET
|
||||||
@@ -223,9 +226,6 @@ UPDATE mangas SET
|
|||||||
notes = coalesce(:notes, notes)
|
notes = coalesce(:notes, notes)
|
||||||
WHERE _id = :mangaId;
|
WHERE _id = :mangaId;
|
||||||
|
|
||||||
selectLastInsertedRowId:
|
|
||||||
SELECT last_insert_rowid();
|
|
||||||
|
|
||||||
insertNetworkManga {
|
insertNetworkManga {
|
||||||
-- Insert the manga if it doesn't exist already
|
-- Insert the manga if it doesn't exist already
|
||||||
INSERT INTO mangas(
|
INSERT INTO mangas(
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ rxJava = "1.3.8"
|
|||||||
shizuku = "13.1.5"
|
shizuku = "13.1.5"
|
||||||
spotless = "8.4.0"
|
spotless = "8.4.0"
|
||||||
sqldelight = "2.3.2"
|
sqldelight = "2.3.2"
|
||||||
sqldelight-androidx-driver = "0.0.17"
|
sqldelight-androidx-driver = "0.1.1"
|
||||||
stringSimilarity = "0.1.0"
|
stringSimilarity = "0.1.0"
|
||||||
subsamplingScaleImageView = "66e0db195d"
|
subsamplingScaleImageView = "66e0db195d"
|
||||||
swipe = "1.3.0"
|
swipe = "1.3.0"
|
||||||
@@ -169,7 +169,8 @@ shizuku-provider = { module = "dev.rikka.shizuku:provider", version.ref = "shizu
|
|||||||
spotless-gradle = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless" }
|
spotless-gradle = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless" }
|
||||||
sqldelight-androidxDriver = { module = "com.eygraber:sqldelight-androidx-driver", version.ref = "sqldelight-androidx-driver" }
|
sqldelight-androidxDriver = { module = "com.eygraber:sqldelight-androidx-driver", version.ref = "sqldelight-androidx-driver" }
|
||||||
sqldelight-androidxPaging = { module = "app.cash.sqldelight:androidx-paging3-extensions", version.ref = "sqldelight" }
|
sqldelight-androidxPaging = { module = "app.cash.sqldelight:androidx-paging3-extensions", version.ref = "sqldelight" }
|
||||||
sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions-jvm", version.ref = "sqldelight" }
|
sqldelight-async = { module = "app.cash.sqldelight:async-extensions", version.ref = "sqldelight" }
|
||||||
|
sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
|
||||||
sqldelight-sqliteDialect338 = { module = "app.cash.sqldelight:sqlite-3-38-dialect", version.ref = "sqldelight" }
|
sqldelight-sqliteDialect338 = { module = "app.cash.sqldelight:sqlite-3-38-dialect", version.ref = "sqldelight" }
|
||||||
stringSimilarity = { module = "com.aallam.similarity:string-similarity-kotlin", version.ref = "stringSimilarity" }
|
stringSimilarity = { module = "com.aallam.similarity:string-similarity-kotlin", version.ref = "stringSimilarity" }
|
||||||
subsamplingScaleImageView = { module = "com.github.tachiyomiorg:subsampling-scale-image-view", version.ref = "subsamplingScaleImageView" }
|
subsamplingScaleImageView = { module = "com.github.tachiyomiorg:subsampling-scale-image-view", version.ref = "subsamplingScaleImageView" }
|
||||||
@@ -208,6 +209,6 @@ markdown = ["markdown-core", "markdown-coil"]
|
|||||||
okhttp = ["okhttp-core", "okhttp-logging", "okhttp-brotli", "okhttp-dnsOverHttps"]
|
okhttp = ["okhttp-core", "okhttp-logging", "okhttp-brotli", "okhttp-dnsOverHttps"]
|
||||||
serialization = ["kotlinx-serialization-json", "kotlinx-serialization-jsonOkio", "kotlinx-serialization-protobuf", "xmlutil-core", "xmlutil-serialization"]
|
serialization = ["kotlinx-serialization-json", "kotlinx-serialization-jsonOkio", "kotlinx-serialization-protobuf", "xmlutil-core", "xmlutil-serialization"]
|
||||||
shizuku = ["shizuku-api", "shizuku-provider"]
|
shizuku = ["shizuku-api", "shizuku-provider"]
|
||||||
sqldelight = ["sqldelight-androidxDriver", "sqldelight-coroutines", "sqldelight-androidxPaging"]
|
sqldelight = ["sqldelight-androidxDriver", "sqldelight-async", "sqldelight-coroutines", "sqldelight-androidxPaging"]
|
||||||
test = ["junit-jupiter", "kotest-assertions", "mockk"]
|
test = ["junit-jupiter", "kotest-assertions", "mockk"]
|
||||||
voyager = ["voyager-navigator", "voyager-screenModel", "voyager-tabNavigator", "voyager-transitions"]
|
voyager = ["voyager-navigator", "voyager-screenModel", "voyager-tabNavigator", "voyager-transitions"]
|
||||||
|
|||||||
Reference in New Issue
Block a user