Use sqldelight for direct db calls in MangaPresenter (#7366)

This commit is contained in:
AntsyLich 2022-06-27 01:54:34 +06:00 committed by GitHub
parent 61a44101a2
commit 04f0ca7846
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 274 additions and 58 deletions

View File

@ -3,6 +3,7 @@ package eu.kanade.data.track
import eu.kanade.data.DatabaseHandler
import eu.kanade.domain.track.model.Track
import eu.kanade.domain.track.repository.TrackRepository
import kotlinx.coroutines.flow.Flow
class TrackRepositoryImpl(
private val handler: DatabaseHandler,
@ -14,11 +15,45 @@ class TrackRepositoryImpl(
}
}
override suspend fun subscribeTracksByMangaId(mangaId: Long): Flow<List<Track>> {
return handler.subscribeToList {
manga_syncQueries.getTracksByMangaId(mangaId, trackMapper)
}
}
override suspend fun delete(mangaId: Long, syncId: Long) {
handler.await {
manga_syncQueries.delete(
mangaId = mangaId,
syncId = syncId,
)
}
}
override suspend fun insert(track: Track) {
handler.await {
manga_syncQueries.insert(
mangaId = track.mangaId,
syncId = track.syncId,
remoteId = track.remoteId,
libraryId = track.libraryId,
title = track.title,
lastChapterRead = track.lastChapterRead,
totalChapters = track.totalChapters,
status = track.status,
score = track.score,
remoteUrl = track.remoteUrl,
startDate = track.startDate,
finishDate = track.finishDate,
)
}
}
override suspend fun insertAll(tracks: List<Track>) {
handler.await(inTransaction = true) {
tracks.forEach { mangaTrack ->
manga_syncQueries.insert(
mangaId = mangaTrack.id,
mangaId = mangaTrack.mangaId,
syncId = mangaTrack.syncId,
remoteId = mangaTrack.remoteId,
libraryId = mangaTrack.libraryId,

View File

@ -15,6 +15,7 @@ import eu.kanade.domain.category.repository.CategoryRepository
import eu.kanade.domain.chapter.interactor.GetChapterByMangaId
import eu.kanade.domain.chapter.interactor.ShouldUpdateDbChapter
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
import eu.kanade.domain.chapter.interactor.SyncChaptersWithTrackServiceTwoWay
import eu.kanade.domain.chapter.interactor.UpdateChapter
import eu.kanade.domain.chapter.repository.ChapterRepository
import eu.kanade.domain.extension.interactor.GetExtensionLanguages
@ -47,6 +48,7 @@ import eu.kanade.domain.source.interactor.ToggleSource
import eu.kanade.domain.source.interactor.ToggleSourcePin
import eu.kanade.domain.source.interactor.UpsertSourceData
import eu.kanade.domain.source.repository.SourceRepository
import eu.kanade.domain.track.interactor.DeleteTrack
import eu.kanade.domain.track.interactor.GetTracks
import eu.kanade.domain.track.interactor.InsertTrack
import eu.kanade.domain.track.repository.TrackRepository
@ -77,6 +79,7 @@ class DomainModule : InjektModule {
addFactory { MoveMangaToCategories(get()) }
addSingletonFactory<TrackRepository> { TrackRepositoryImpl(get()) }
addFactory { DeleteTrack(get()) }
addFactory { GetTracks(get()) }
addFactory { InsertTrack(get()) }
@ -85,6 +88,7 @@ class DomainModule : InjektModule {
addFactory { UpdateChapter(get()) }
addFactory { ShouldUpdateDbChapter() }
addFactory { SyncChaptersWithSource(get(), get(), get(), get()) }
addFactory { SyncChaptersWithTrackServiceTwoWay(get(), get()) }
addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) }
addFactory { DeleteHistoryTable(get()) }

View File

@ -0,0 +1,41 @@
package eu.kanade.domain.chapter.interactor
import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.domain.chapter.model.toChapterUpdate
import eu.kanade.domain.track.interactor.InsertTrack
import eu.kanade.domain.track.model.Track
import eu.kanade.domain.track.model.toDbTrack
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.util.system.logcat
import logcat.LogPriority
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class SyncChaptersWithTrackServiceTwoWay(
private val updateChapter: UpdateChapter = Injekt.get(),
private val insertTrack: InsertTrack = Injekt.get(),
) {
suspend fun await(
chapters: List<Chapter>,
remoteTrack: Track,
service: TrackService,
) {
val sortedChapters = chapters.sortedBy { it.chapterNumber }
val chapterUpdates = sortedChapters
.filter { chapter -> chapter.chapterNumber <= remoteTrack.lastChapterRead && !chapter.read }
.map { it.copy(read = true).toChapterUpdate() }
// only take into account continuous reading
val localLastRead = sortedChapters.takeWhile { it.read }.lastOrNull()?.chapterNumber ?: 0F
val updatedTrack = remoteTrack.copy(lastChapterRead = localLastRead.toDouble())
try {
service.update(updatedTrack.toDbTrack())
updateChapter.awaitAll(chapterUpdates)
insertTrack.await(updatedTrack)
} catch (e: Throwable) {
logcat(LogPriority.WARN, e)
}
}
}

View File

@ -4,7 +4,9 @@ import eu.kanade.domain.manga.model.Manga
import eu.kanade.domain.manga.model.MangaUpdate
import eu.kanade.domain.manga.repository.MangaRepository
class SetMangaChapterFlags(private val mangaRepository: MangaRepository) {
class SetMangaChapterFlags(
private val mangaRepository: MangaRepository,
) {
suspend fun awaitSetDownloadedFilter(manga: Manga, flag: Long): Boolean {
return mangaRepository.update(

View File

@ -1,5 +1,6 @@
package eu.kanade.domain.manga.model
import eu.kanade.data.listOfStringsAdapter
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.LocalSource
@ -143,7 +144,7 @@ fun TriStateFilter.toTriStateGroupState(): ExtendedNavigationView.Item.TriStateG
}
// TODO: Remove when all deps are migrated
fun Manga.toDbManga(): DbManga = DbManga.create(url, title, source).also {
fun Manga.toDbManga(): DbManga = DbManga.create(source).also {
it.id = id
it.favorite = favorite
it.last_update = lastUpdate
@ -151,7 +152,15 @@ fun Manga.toDbManga(): DbManga = DbManga.create(url, title, source).also {
it.viewer_flags = viewerFlags.toInt()
it.chapter_flags = chapterFlags.toInt()
it.cover_last_modified = coverLastModified
it.url = url
it.title = title
it.artist = artist
it.author = author
it.description = description
it.genre = genre?.let(listOfStringsAdapter::encode)
it.status = status.toInt()
it.thumbnail_url = thumbnailUrl
it.initialized = initialized
}
fun Manga.toMangaInfo(): MangaInfo = MangaInfo(

View File

@ -0,0 +1,18 @@
package eu.kanade.domain.track.interactor
import eu.kanade.domain.track.repository.TrackRepository
import eu.kanade.tachiyomi.util.system.logcat
import logcat.LogPriority
class DeleteTrack(
private val trackRepository: TrackRepository,
) {
suspend fun await(mangaId: Long, syncId: Long) {
try {
trackRepository.delete(mangaId, syncId)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
}
}
}

View File

@ -3,6 +3,7 @@ package eu.kanade.domain.track.interactor
import eu.kanade.domain.track.model.Track
import eu.kanade.domain.track.repository.TrackRepository
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.flow.Flow
import logcat.LogPriority
class GetTracks(
@ -17,4 +18,8 @@ class GetTracks(
emptyList()
}
}
suspend fun subscribe(mangaId: Long): Flow<List<Track>> {
return trackRepository.subscribeTracksByMangaId(mangaId)
}
}

View File

@ -9,6 +9,14 @@ class InsertTrack(
private val trackRepository: TrackRepository,
) {
suspend fun await(track: Track) {
try {
trackRepository.insert(track)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
}
}
suspend fun awaitAll(tracks: List<Track>) {
try {
trackRepository.insertAll(tracks)

View File

@ -1,5 +1,7 @@
package eu.kanade.domain.track.model
import eu.kanade.tachiyomi.data.database.models.Track as DbTrack
data class Track(
val id: Long,
val mangaId: Long,
@ -25,3 +27,37 @@ data class Track(
)
}
}
fun Track.toDbTrack(): DbTrack = DbTrack.create(syncId.toInt()).also {
it.id = id
it.manga_id = mangaId
it.media_id = remoteId
it.library_id = libraryId
it.title = title
it.last_chapter_read = lastChapterRead.toFloat()
it.total_chapters = totalChapters.toInt()
it.status = status.toInt()
it.score = score
it.tracking_url = remoteUrl
it.started_reading_date = startDate
it.finished_reading_date = finishDate
}
fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? {
val trackId = id ?: if (idRequired.not()) -1 else return null
return Track(
id = trackId,
mangaId = manga_id,
syncId = sync_id.toLong(),
remoteId = media_id,
libraryId = library_id,
title = title,
lastChapterRead = last_chapter_read.toDouble(),
totalChapters = total_chapters.toLong(),
status = status.toLong(),
score = score,
remoteUrl = tracking_url,
startDate = started_reading_date,
finishDate = finished_reading_date,
)
}

View File

@ -1,10 +1,17 @@
package eu.kanade.domain.track.repository
import eu.kanade.domain.track.model.Track
import kotlinx.coroutines.flow.Flow
interface TrackRepository {
suspend fun getTracksByMangaId(mangaId: Long): List<Track>
suspend fun subscribeTracksByMangaId(mangaId: Long): Flow<List<Track>>
suspend fun delete(mangaId: Long, syncId: Long)
suspend fun insert(track: Track)
suspend fun insertAll(tracks: List<Track>)
}

View File

@ -2,7 +2,10 @@ package eu.kanade.tachiyomi.ui.manga
import android.os.Bundle
import androidx.compose.runtime.Immutable
import eu.kanade.domain.category.interactor.GetCategories
import eu.kanade.domain.category.interactor.MoveMangaToCategories
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
import eu.kanade.domain.chapter.interactor.SyncChaptersWithTrackServiceTwoWay
import eu.kanade.domain.chapter.interactor.UpdateChapter
import eu.kanade.domain.chapter.model.ChapterUpdate
import eu.kanade.domain.chapter.model.toDbChapter
@ -14,11 +17,15 @@ import eu.kanade.domain.manga.model.TriStateFilter
import eu.kanade.domain.manga.model.isLocal
import eu.kanade.domain.manga.model.toDbManga
import eu.kanade.domain.manga.model.toMangaInfo
import eu.kanade.domain.track.interactor.DeleteTrack
import eu.kanade.domain.track.interactor.GetTracks
import eu.kanade.domain.track.interactor.InsertTrack
import eu.kanade.domain.track.model.toDbTrack
import eu.kanade.domain.track.model.toDomainTrack
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.models.toDomainChapter
import eu.kanade.tachiyomi.data.download.DownloadManager
@ -34,7 +41,6 @@ import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.manga.track.TrackItem
import eu.kanade.tachiyomi.util.chapter.ChapterSettingsHelper
import eu.kanade.tachiyomi.util.chapter.getChapterSort
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithTrackServiceTwoWay
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withUIContext
@ -44,17 +50,21 @@ import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.withContext
import logcat.LogPriority
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
@ -77,6 +87,12 @@ class MangaPresenter(
private val updateChapter: UpdateChapter = Injekt.get(),
private val updateManga: UpdateManga = Injekt.get(),
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(),
private val deleteTrack: DeleteTrack = Injekt.get(),
private val getTracks: GetTracks = Injekt.get(),
private val moveMangaToCategories: MoveMangaToCategories = Injekt.get(),
private val insertTrack: InsertTrack = Injekt.get(),
private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay = Injekt.get(),
) : BasePresenter<MangaController>() {
private val _state: MutableStateFlow<MangaScreenState> = MutableStateFlow(MangaScreenState.Loading)
@ -107,7 +123,6 @@ class MangaPresenter(
private val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
private var trackSubscription: Subscription? = null
private var searchTrackerJob: Job? = null
private var refreshTrackersJob: Job? = null
@ -154,20 +169,15 @@ class MangaPresenter(
isFromSource = isFromSource,
trackingAvailable = trackManager.hasLoggedServices(),
chapters = chapterItems,
).also {
getTrackingObservable(manga)
.subscribeLatestCache(
{ _, count -> updateSuccessState { it.copy(trackingCount = count) } },
{ _, error -> logcat(LogPriority.ERROR, error) },
)
}
// Update state
is MangaScreenState.Success -> currentState.copy(manga = manga, chapters = chapterItems)
}
}
fetchTrackers()
observeTrackers()
observeTrackingCount()
observeDownloads()
if (!manga.initialized) {
@ -195,20 +205,6 @@ class MangaPresenter(
}
// Manga info - start
private fun getTrackingObservable(manga: DomainManga): Observable<Int> {
if (!trackManager.hasLoggedServices()) {
return Observable.just(0)
}
return db.getTracks(manga.id).asRxObservable()
.map { tracks ->
val loggedServices = trackManager.services.filter { it.isLogged }.map { it.id }
tracks.filter { it.sync_id in loggedServices }
}
.map { it.size }
}
/**
* Fetch manga information from source.
*/
@ -341,8 +337,8 @@ class MangaPresenter(
* @return Array of category ids the manga is in, if none returns default id
*/
fun getMangaCategoryIds(manga: DomainManga): Array<Int> {
val categories = db.getCategoriesForManga(manga.toDbManga()).executeAsBlocking()
return categories.mapNotNull { it.id }.toTypedArray()
val categories = runBlocking { getCategories.await(manga.id) }
return categories.map { it.id.toInt() }.toTypedArray()
}
fun moveMangaToCategoriesAndAddToLibrary(manga: Manga, categories: List<Category>) {
@ -359,8 +355,11 @@ class MangaPresenter(
* @param categories the selected categories.
*/
private fun moveMangaToCategories(manga: Manga, categories: List<Category>) {
val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) }
db.setMangaCategories(mc, listOf(manga))
val mangaId = manga.id ?: return
val categoryIds = categories.mapNotNull { it.id?.toLong() }
presenterScope.launchIO {
moveMangaToCategories.await(mangaId, categoryIds)
}
}
/**
@ -373,6 +372,22 @@ class MangaPresenter(
moveMangaToCategories(manga, listOfNotNull(category))
}
private fun observeTrackingCount() {
val manga = successState?.manga ?: return
presenterScope.launchIO {
getTracks.subscribe(manga.id)
.catch { logcat(LogPriority.ERROR, it) }
.map { tracks ->
val loggedServicesId = loggedServices.map { it.id.toLong() }
tracks.filter { it.syncId in loggedServicesId }.size
}
.collectLatest { trackingCount ->
updateSuccessState { it.copy(trackingCount = trackingCount) }
}
}
}
// Manga info - end
// Chapters list - start
@ -520,7 +535,7 @@ class MangaPresenter(
val modified = chapters.filterNot { it.read == read }
modified
.map { ChapterUpdate(id = it.id, read = read) }
.forEach { updateChapter.await(it) }
.let { updateChapter.awaitAll(it) }
if (read && preferences.removeAfterMarkedAsRead()) {
deleteChapters(modified)
}
@ -545,7 +560,7 @@ class MangaPresenter(
chapters
.filterNot { it.bookmark == bookmarked }
.map { ChapterUpdate(id = it.id, bookmark = bookmarked) }
.forEach { updateChapter.await(it) }
.let { updateChapter.awaitAll(it) }
}
}
@ -593,6 +608,7 @@ class MangaPresenter(
*/
fun setUnreadFilter(state: State) {
val manga = successState?.manga ?: return
val flag = when (state) {
State.IGNORE -> DomainManga.SHOW_ALL
State.INCLUDE -> DomainManga.CHAPTER_SHOW_UNREAD
@ -609,11 +625,13 @@ class MangaPresenter(
*/
fun setDownloadedFilter(state: State) {
val manga = successState?.manga ?: return
val flag = when (state) {
State.IGNORE -> DomainManga.SHOW_ALL
State.INCLUDE -> DomainManga.CHAPTER_SHOW_DOWNLOADED
State.EXCLUDE -> DomainManga.CHAPTER_SHOW_NOT_DOWNLOADED
}
presenterScope.launchIO {
setMangaChapterFlags.awaitSetDownloadedFilter(manga, flag)
}
@ -625,11 +643,13 @@ class MangaPresenter(
*/
fun setBookmarkedFilter(state: State) {
val manga = successState?.manga ?: return
val flag = when (state) {
State.IGNORE -> DomainManga.SHOW_ALL
State.INCLUDE -> DomainManga.CHAPTER_SHOW_BOOKMARKED
State.EXCLUDE -> DomainManga.CHAPTER_SHOW_NOT_BOOKMARKED
}
presenterScope.launchIO {
setMangaChapterFlags.awaitSetBookmarkFilter(manga, flag)
}
@ -641,6 +661,7 @@ class MangaPresenter(
*/
fun setDisplayMode(mode: Long) {
val manga = successState?.manga ?: return
presenterScope.launchIO {
setMangaChapterFlags.awaitSetDisplayMode(manga, mode)
}
@ -652,6 +673,7 @@ class MangaPresenter(
*/
fun setSorting(sort: Long) {
val manga = successState?.manga ?: return
presenterScope.launchIO {
setMangaChapterFlags.awaitSetSortingModeOrFlipOrder(manga, sort)
}
@ -661,19 +683,25 @@ class MangaPresenter(
// Track sheet - start
private fun fetchTrackers() {
private fun observeTrackers() {
val manga = successState?.manga ?: return
trackSubscription?.let { remove(it) }
trackSubscription = db.getTracks(manga.id)
.asRxObservable()
presenterScope.launchIO {
getTracks.subscribe(manga.id)
.catch { logcat(LogPriority.ERROR, it) }
.map { tracks ->
val dbTracks = tracks.map { it.toDbTrack() }
loggedServices.map { service ->
TrackItem(tracks.find { it.sync_id == service.id }, service)
TrackItem(dbTracks.find { it.sync_id == service.id }, service)
}
}
.collectLatest { trackItems ->
_trackList = trackItems
withContext(Dispatchers.Main) {
view?.onNextTrackers(trackItems)
}
}
}
.observeOn(AndroidSchedulers.mainThread())
.doOnNext { _trackList = it }
.subscribeLatestCache(MangaController::onNextTrackers)
}
fun refreshTrackers() {
@ -682,16 +710,21 @@ class MangaPresenter(
supervisorScope {
try {
trackList
.filter { it.track != null }
.map {
async {
val track = it.service.refresh(it.track!!)
db.insertTrack(track).executeAsBlocking()
val track = it.track ?: return@async null
if (it.service is EnhancedTrackService) {
val updatedTrack = it.service.refresh(track)
val domainTrack = updatedTrack.toDomainTrack() ?: return@async null
insertTrack.await(domainTrack)
(it.service as? EnhancedTrackService)?.let { _ ->
val allChapters = successState?.chapters
?.map { it.chapter.toDbChapter() } ?: emptyList()
syncChaptersWithTrackServiceTwoWay(db, allChapters, track, it.service)
?.map { it.chapter } ?: emptyList()
syncChaptersWithTrackServiceTwoWay
.await(allChapters, domainTrack, it.service)
}
}
}
@ -727,10 +760,17 @@ class MangaPresenter(
.map { it.chapter.toDbChapter() }
val hasReadChapters = allChapters.any { it.read }
service.bind(item, hasReadChapters)
db.insertTrack(item).executeAsBlocking()
if (service is EnhancedTrackService) {
syncChaptersWithTrackServiceTwoWay(db, allChapters, item, service)
item.toDomainTrack(idRequired = false)?.let { track ->
insertTrack.await(track)
(service as? EnhancedTrackService)?.let { _ ->
val chapters = successState.chapters
.map { it.chapter }
syncChaptersWithTrackServiceTwoWay
.await(chapters, track, service)
}
}
} catch (e: Throwable) {
withUIContext { view?.applicationContext?.toast(e.message) }
@ -743,20 +783,27 @@ class MangaPresenter(
fun unregisterTracking(service: TrackService) {
val manga = successState?.manga ?: return
db.deleteTrackForManga(manga.toDbManga(), service).executeAsBlocking()
presenterScope.launchIO {
deleteTrack.await(manga.id, service.id.toLong())
}
}
private fun updateRemote(track: Track, service: TrackService) {
launchIO {
try {
service.update(track)
db.insertTrack(track).executeAsBlocking()
track.toDomainTrack(idRequired = false)?.let {
insertTrack.await(it)
}
withUIContext { view?.onTrackingRefreshDone() }
} catch (e: Throwable) {
withUIContext { view?.onTrackingRefreshError(e) }
// Restart on error to set old values
fetchTrackers()
observeTrackers()
}
}
}

View File

@ -17,6 +17,10 @@ CREATE TABLE manga_sync(
ON DELETE CASCADE
);
delete:
DELETE FROM manga_sync
WHERE manga_id = :mangaId AND sync_id = :syncId;
getTracksByMangaId:
SELECT *
FROM manga_sync