Make migration manga-centric rather than source-centric (#2786)

This commit is contained in:
FlaminSarge 2020-03-31 19:36:23 -07:00 committed by GitHub
parent f53cc10338
commit cce3b3a559
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 128 additions and 140 deletions

View File

@ -1,19 +1,13 @@
package eu.kanade.tachiyomi.ui.migration
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import com.afollestad.materialdialogs.MaterialDialog
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.popControllerWithTag
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import kotlinx.android.synthetic.main.migration_controller.migration_recycler
@ -81,16 +75,6 @@ class MigrationController : NucleusController<MigrationPresenter>(),
}
}
fun renderIsReplacingManga(state: ViewState) {
if (state.isReplacingManga) {
if (router.getControllerWithTag(LOADING_DIALOG_TAG) == null) {
LoadingController().showDialog(router, LOADING_DIALOG_TAG)
}
} else {
router.popControllerWithTag(LOADING_DIALOG_TAG)
}
}
override fun onItemClick(view: View, position: Int): Boolean {
val item = adapter?.getItem(position) ?: return false
@ -108,27 +92,4 @@ class MigrationController : NucleusController<MigrationPresenter>(),
override fun onSelectClick(position: Int) {
onItemClick(view!!, position)
}
fun migrateManga(prevManga: Manga, manga: Manga) {
presenter.migrateManga(prevManga, manga, replace = true)
}
fun copyManga(prevManga: Manga, manga: Manga) {
presenter.migrateManga(prevManga, manga, replace = false)
}
class LoadingController : DialogController() {
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
return MaterialDialog.Builder(activity!!)
.progress(true, 0)
.content(R.string.migrating)
.cancelable(false)
.build()
}
}
companion object {
const val LOADING_DIALOG_TAG = "LoadingDialog"
}
}

View File

@ -4,17 +4,11 @@ import android.os.Bundle
import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import eu.kanade.tachiyomi.util.lang.combineLatest
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import uy.kohesive.injekt.Injekt
@ -22,8 +16,7 @@ import uy.kohesive.injekt.api.get
class MigrationPresenter(
private val sourceManager: SourceManager = Injekt.get(),
private val db: DatabaseHelper = Injekt.get(),
private val preferences: PreferencesHelper = Injekt.get()
private val db: DatabaseHelper = Injekt.get()
) : BasePresenter<MigrationController>() {
var state = ViewState()
@ -51,13 +44,8 @@ class MigrationPresenter(
.doOnNext { state = state.copy(mangaForSource = it) }
.subscribe()
stateRelay
// Render the view when any field other than isReplacingManga changes
.distinctUntilChanged { t1, t2 -> t1.isReplacingManga != t2.isReplacingManga }
.subscribeLatestCache(MigrationController::render)
stateRelay.distinctUntilChanged { state -> state.isReplacingManga }
.subscribeLatestCache(MigrationController::renderIsReplacingManga)
// Render the view when any field changes
stateRelay.subscribeLatestCache(MigrationController::render)
}
fun setSelectedSource(source: Source) {
@ -78,82 +66,4 @@ class MigrationPresenter(
private fun libraryToMigrationItem(library: List<Manga>, sourceId: Long): List<MangaItem> {
return library.filter { it.source == sourceId }.map(::MangaItem)
}
fun migrateManga(prevManga: Manga, manga: Manga, replace: Boolean) {
val source = sourceManager.get(manga.source) ?: return
state = state.copy(isReplacingManga = true)
Observable.defer { source.fetchChapterList(manga) }
.onErrorReturn { emptyList() }
.doOnNext { migrateMangaInternal(source, it, prevManga, manga, replace) }
.onErrorReturn { emptyList() }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnUnsubscribe { state = state.copy(isReplacingManga = false) }
.subscribe()
}
private fun migrateMangaInternal(
source: Source,
sourceChapters: List<SChapter>,
prevManga: Manga,
manga: Manga,
replace: Boolean
) {
val flags = preferences.migrateFlags().getOrDefault()
val migrateChapters = MigrationFlags.hasChapters(flags)
val migrateCategories = MigrationFlags.hasCategories(flags)
val migrateTracks = MigrationFlags.hasTracks(flags)
db.inTransaction {
// Update chapters read
if (migrateChapters) {
try {
syncChaptersWithSource(db, sourceChapters, manga, source)
} catch (e: Exception) {
// Worst case, chapters won't be synced
}
val prevMangaChapters = db.getChapters(prevManga).executeAsBlocking()
val maxChapterRead = prevMangaChapters.filter { it.read }
.maxBy { it.chapter_number }?.chapter_number
if (maxChapterRead != null) {
val dbChapters = db.getChapters(manga).executeAsBlocking()
for (chapter in dbChapters) {
if (chapter.isRecognizedNumber && chapter.chapter_number <= maxChapterRead) {
chapter.read = true
}
}
db.insertChapters(dbChapters).executeAsBlocking()
}
}
// Update categories
if (migrateCategories) {
val categories = db.getCategoriesForManga(prevManga).executeAsBlocking()
val mangaCategories = categories.map { MangaCategory.create(manga, it) }
db.setMangaCategories(mangaCategories, listOf(manga))
}
// Update track
if (migrateTracks) {
val tracks = db.getTracks(prevManga).executeAsBlocking()
for (track in tracks) {
track.id = null
track.manga_id = manga.id!!
}
db.insertTracks(tracks).executeAsBlocking()
}
// Update favorite status
if (replace) {
prevManga.favorite = false
db.updateMangaFavorite(prevManga).executeAsBlocking()
}
manga.favorite = true
db.updateMangaFavorite(manga).executeAsBlocking()
// SearchPresenter#networkToLocalManga may have updated the manga title, so ensure db gets updated title
db.updateMangaTitle(manga).executeAsBlocking()
}
}
}

View File

@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.popControllerWithTag
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchPresenter
import uy.kohesive.injekt.injectLazy
@ -35,21 +36,17 @@ class SearchController(
}
fun migrateManga() {
val target = targetController as? MigrationController ?: return
val manga = manga ?: return
val newManga = newManga ?: return
router.popController(this)
target.migrateManga(manga, newManga)
(presenter as? SearchPresenter)?.migrateManga(manga, newManga, true)
}
fun copyManga() {
val target = targetController as? MigrationController ?: return
val manga = manga ?: return
val newManga = newManga ?: return
router.popController(this)
target.copyManga(manga, newManga)
(presenter as? SearchPresenter)?.migrateManga(manga, newManga, false)
}
override fun onMangaClick(manga: Manga) {
@ -64,6 +61,17 @@ class SearchController(
super.onMangaClick(manga)
}
fun renderIsReplacingManga(isReplacingManga: Boolean) {
if (isReplacingManga) {
if (router.getControllerWithTag(LOADING_DIALOG_TAG) == null) {
LoadingController().showDialog(router, LOADING_DIALOG_TAG)
}
} else {
router.popControllerWithTag(LOADING_DIALOG_TAG)
router.popController(this)
}
}
class MigrationDialog : DialogController() {
private val preferences: PreferencesHelper by injectLazy()
@ -96,4 +104,19 @@ class SearchController(
.build()
}
}
class LoadingController : DialogController() {
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
return MaterialDialog.Builder(activity!!)
.progress(true, 0)
.content(R.string.migrating)
.cancelable(false)
.build()
}
}
companion object {
const val LOADING_DIALOG_TAG = "LoadingDialog"
}
}

View File

@ -1,17 +1,35 @@
package eu.kanade.tachiyomi.ui.migration
import android.os.Bundle
import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchCardItem
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchItem
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchPresenter
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
class SearchPresenter(
initialQuery: String? = "",
private val manga: Manga
) : CatalogueSearchPresenter(initialQuery) {
private val replacingMangaRelay = BehaviorRelay.create<Boolean>()
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
replacingMangaRelay.subscribeLatestCache({ controller, isReplacingManga -> (controller as? SearchController)?.renderIsReplacingManga(isReplacingManga) })
}
override fun getEnabledSources(): List<CatalogueSource> {
// Put the source of the selected manga at the top
return super.getEnabledSources()
@ -29,4 +47,81 @@ class SearchPresenter(
localManga.title = sManga.title
return localManga
}
fun migrateManga(prevManga: Manga, manga: Manga, replace: Boolean) {
val source = sourceManager.get(manga.source) ?: return
replacingMangaRelay.call(true)
Observable.defer { source.fetchChapterList(manga) }
.onErrorReturn { emptyList() }
.doOnNext { migrateMangaInternal(source, it, prevManga, manga, replace) }
.onErrorReturn { emptyList() }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnUnsubscribe { replacingMangaRelay.call(false) }
.subscribe()
}
private fun migrateMangaInternal(
source: Source,
sourceChapters: List<SChapter>,
prevManga: Manga,
manga: Manga,
replace: Boolean
) {
val flags = preferencesHelper.migrateFlags().getOrDefault()
val migrateChapters = MigrationFlags.hasChapters(flags)
val migrateCategories = MigrationFlags.hasCategories(flags)
val migrateTracks = MigrationFlags.hasTracks(flags)
db.inTransaction {
// Update chapters read
if (migrateChapters) {
try {
syncChaptersWithSource(db, sourceChapters, manga, source)
} catch (e: Exception) {
// Worst case, chapters won't be synced
}
val prevMangaChapters = db.getChapters(prevManga).executeAsBlocking()
val maxChapterRead = prevMangaChapters.filter { it.read }
.maxBy { it.chapter_number }?.chapter_number
if (maxChapterRead != null) {
val dbChapters = db.getChapters(manga).executeAsBlocking()
for (chapter in dbChapters) {
if (chapter.isRecognizedNumber && chapter.chapter_number <= maxChapterRead) {
chapter.read = true
}
}
db.insertChapters(dbChapters).executeAsBlocking()
}
}
// Update categories
if (migrateCategories) {
val categories = db.getCategoriesForManga(prevManga).executeAsBlocking()
val mangaCategories = categories.map { MangaCategory.create(manga, it) }
db.setMangaCategories(mangaCategories, listOf(manga))
}
// Update track
if (migrateTracks) {
val tracks = db.getTracks(prevManga).executeAsBlocking()
for (track in tracks) {
track.id = null
track.manga_id = manga.id!!
}
db.insertTracks(tracks).executeAsBlocking()
}
// Update favorite status
if (replace) {
prevManga.favorite = false
db.updateMangaFavorite(prevManga).executeAsBlocking()
}
manga.favorite = true
db.updateMangaFavorite(manga).executeAsBlocking()
// SearchPresenter#networkToLocalManga may have updated the manga title, so ensure db gets updated title
db.updateMangaTitle(manga).executeAsBlocking()
}
}
}

View File

@ -5,6 +5,5 @@ import eu.kanade.tachiyomi.source.Source
data class ViewState(
val selectedSource: Source? = null,
val mangaForSource: List<MangaItem> = emptyList(),
val sourcesWithManga: List<SourceItem> = emptyList(),
val isReplacingManga: Boolean = false
val sourcesWithManga: List<SourceItem> = emptyList()
)