mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-30 22:07:57 +01:00 
			
		
		
		
	Make migration manga-centric rather than source-centric (#2786)
This commit is contained in:
		| @@ -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 New Issue
	
	Block a user