Browse Source

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

AntsyLich 2 years ago
parent
commit
04f0ca7846

+ 36 - 1
app/src/main/java/eu/kanade/data/track/TrackRepositoryImpl.kt

@@ -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,

+ 4 - 0
app/src/main/java/eu/kanade/domain/DomainModule.kt

@@ -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()) }

+ 41 - 0
app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithTrackServiceTwoWay.kt

@@ -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)
+        }
+    }
+}

+ 3 - 1
app/src/main/java/eu/kanade/domain/manga/interactor/SetMangaChapterFlags.kt

@@ -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(

+ 10 - 1
app/src/main/java/eu/kanade/domain/manga/model/Manga.kt

@@ -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(

+ 18 - 0
app/src/main/java/eu/kanade/domain/track/interactor/DeleteTrack.kt

@@ -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)
+        }
+    }
+}

+ 5 - 0
app/src/main/java/eu/kanade/domain/track/interactor/GetTracks.kt

@@ -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)
+    }
 }

+ 8 - 0
app/src/main/java/eu/kanade/domain/track/interactor/InsertTrack.kt

@@ -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)

+ 36 - 0
app/src/main/java/eu/kanade/domain/track/model/Track.kt

@@ -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,
+    )
+}

+ 7 - 0
app/src/main/java/eu/kanade/domain/track/repository/TrackRepository.kt

@@ -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>)
 }

+ 102 - 55
app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt

@@ -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()
-            .map { tracks ->
-                loggedServices.map { service ->
-                    TrackItem(tracks.find { it.sync_id == service.id }, service)
+
+        presenterScope.launchIO {
+            getTracks.subscribe(manga.id)
+                .catch { logcat(LogPriority.ERROR, it) }
+                .map { tracks ->
+                    val dbTracks = tracks.map { it.toDbTrack() }
+                    loggedServices.map { service ->
+                        TrackItem(dbTracks.find { it.sync_id == service.id }, service)
+                    }
                 }
-            }
-            .observeOn(AndroidSchedulers.mainThread())
-            .doOnNext { _trackList = it }
-            .subscribeLatestCache(MangaController::onNextTrackers)
+                .collectLatest { trackItems ->
+                    _trackList = trackItems
+                    withContext(Dispatchers.Main) {
+                        view?.onNextTrackers(trackItems)
+                    }
+                }
+        }
     }
 
     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
+
+                                val updatedTrack = it.service.refresh(track)
 
-                                if (it.service is EnhancedTrackService) {
+                                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()
             }
         }
     }

+ 4 - 0
app/src/main/sqldelight/data/manga_sync.sq

@@ -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