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 package eu.kanade.tachiyomi.ui.migration
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.afollestad.materialdialogs.MaterialDialog
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R 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.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.popControllerWithTag
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import kotlinx.android.synthetic.main.migration_controller.migration_recycler 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 { override fun onItemClick(view: View, position: Int): Boolean {
val item = adapter?.getItem(position) ?: return false val item = adapter?.getItem(position) ?: return false
@ -108,27 +92,4 @@ class MigrationController : NucleusController<MigrationPresenter>(),
override fun onSelectClick(position: Int) { override fun onSelectClick(position: Int) {
onItemClick(view!!, position) 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 com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga 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.LocalSource
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager 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.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import eu.kanade.tachiyomi.util.lang.combineLatest import eu.kanade.tachiyomi.util.lang.combineLatest
import rx.Observable
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@ -22,8 +16,7 @@ import uy.kohesive.injekt.api.get
class MigrationPresenter( class MigrationPresenter(
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
private val db: DatabaseHelper = Injekt.get(), private val db: DatabaseHelper = Injekt.get()
private val preferences: PreferencesHelper = Injekt.get()
) : BasePresenter<MigrationController>() { ) : BasePresenter<MigrationController>() {
var state = ViewState() var state = ViewState()
@ -51,13 +44,8 @@ class MigrationPresenter(
.doOnNext { state = state.copy(mangaForSource = it) } .doOnNext { state = state.copy(mangaForSource = it) }
.subscribe() .subscribe()
stateRelay // Render the view when any field changes
// Render the view when any field other than isReplacingManga changes stateRelay.subscribeLatestCache(MigrationController::render)
.distinctUntilChanged { t1, t2 -> t1.isReplacingManga != t2.isReplacingManga }
.subscribeLatestCache(MigrationController::render)
stateRelay.distinctUntilChanged { state -> state.isReplacingManga }
.subscribeLatestCache(MigrationController::renderIsReplacingManga)
} }
fun setSelectedSource(source: Source) { fun setSelectedSource(source: Source) {
@ -78,82 +66,4 @@ class MigrationPresenter(
private fun libraryToMigrationItem(library: List<Manga>, sourceId: Long): List<MangaItem> { private fun libraryToMigrationItem(library: List<Manga>, sourceId: Long): List<MangaItem> {
return library.filter { it.source == sourceId }.map(::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.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.ui.base.controller.DialogController 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.CatalogueSearchController
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchPresenter import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchPresenter
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -35,21 +36,17 @@ class SearchController(
} }
fun migrateManga() { fun migrateManga() {
val target = targetController as? MigrationController ?: return
val manga = manga ?: return val manga = manga ?: return
val newManga = newManga ?: return val newManga = newManga ?: return
router.popController(this) (presenter as? SearchPresenter)?.migrateManga(manga, newManga, true)
target.migrateManga(manga, newManga)
} }
fun copyManga() { fun copyManga() {
val target = targetController as? MigrationController ?: return
val manga = manga ?: return val manga = manga ?: return
val newManga = newManga ?: return val newManga = newManga ?: return
router.popController(this) (presenter as? SearchPresenter)?.migrateManga(manga, newManga, false)
target.copyManga(manga, newManga)
} }
override fun onMangaClick(manga: Manga) { override fun onMangaClick(manga: Manga) {
@ -64,6 +61,17 @@ class SearchController(
super.onMangaClick(manga) 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() { class MigrationDialog : DialogController() {
private val preferences: PreferencesHelper by injectLazy() private val preferences: PreferencesHelper by injectLazy()
@ -96,4 +104,19 @@ class SearchController(
.build() .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 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.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.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.source.model.SManga
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchCardItem 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.CatalogueSearchItem
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchPresenter 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( class SearchPresenter(
initialQuery: String? = "", initialQuery: String? = "",
private val manga: Manga private val manga: Manga
) : CatalogueSearchPresenter(initialQuery) { ) : 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> { override fun getEnabledSources(): List<CatalogueSource> {
// Put the source of the selected manga at the top // Put the source of the selected manga at the top
return super.getEnabledSources() return super.getEnabledSources()
@ -29,4 +47,81 @@ class SearchPresenter(
localManga.title = sManga.title localManga.title = sManga.title
return localManga 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( data class ViewState(
val selectedSource: Source? = null, val selectedSource: Source? = null,
val mangaForSource: List<MangaItem> = emptyList(), val mangaForSource: List<MangaItem> = emptyList(),
val sourcesWithManga: List<SourceItem> = emptyList(), val sourcesWithManga: List<SourceItem> = emptyList()
val isReplacingManga: Boolean = false
) )