Make migration manga-centric rather than source-centric (#2786)
This commit is contained in:
parent
f53cc10338
commit
cce3b3a559
5 changed files with 128 additions and 140 deletions
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
|
|
Reference in a new issue