diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt index 5c81c9444..5e81777bf 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt @@ -381,7 +381,7 @@ private fun MangaScreenSmallImpl( title = state.manga.title, author = state.manga.author, artist = state.manga.artist, - sourceName = remember { state.source.getNameForMangaInfo(state.mergedData) }, + sourceName = remember { state.source.getNameForMangaInfo(state.mergedData?.sources) }, isStubSource = remember { state.source is SourceManager.StubSource }, coverDataProvider = { state.manga }, status = state.manga.status, @@ -662,7 +662,7 @@ fun MangaScreenLargeImpl( title = state.manga.title, author = state.manga.author, artist = state.manga.artist, - sourceName = remember { state.source.getNameForMangaInfo(state.mergedData) }, + sourceName = remember { state.source.getNameForMangaInfo(state.mergedData?.sources) }, isStubSource = remember { state.source is SourceManager.StubSource }, coverDataProvider = { state.manga }, status = state.manga.status, diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/Source.kt b/app/src/main/java/eu/kanade/tachiyomi/source/Source.kt index 8aec931a4..a7f51df6d 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/Source.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/Source.kt @@ -7,7 +7,6 @@ import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.ui.manga.MergedMangaData import eu.kanade.tachiyomi.util.lang.awaitSingle import rx.Observable import uy.kohesive.injekt.Injekt @@ -96,7 +95,7 @@ fun Source.getPreferenceKey(): String = "source_$id" fun Source.toSourceData(): SourceData = SourceData(id = id, lang = lang, name = name) -fun Source.getNameForMangaInfo(mergeData: MergedMangaData?): String { +fun Source.getNameForMangaInfo(mergeSources: List?): String { val preferences = Injekt.get() val enabledLanguages = preferences.enabledLanguages().get() .filterNot { it in listOf("all", "other") } @@ -104,8 +103,8 @@ fun Source.getNameForMangaInfo(mergeData: MergedMangaData?): String { val isInEnabledLanguages = lang in enabledLanguages return when { // SY --> - mergeData != null -> getMergedSourcesString( - mergeData, + !mergeSources.isNullOrEmpty() -> getMergedSourcesString( + mergeSources, enabledLanguages, hasOneActiveLanguages, ) @@ -120,12 +119,12 @@ fun Source.getNameForMangaInfo(mergeData: MergedMangaData?): String { // SY --> private fun getMergedSourcesString( - mergeData: MergedMangaData, + mergeSources: List, enabledLangs: List, onlyName: Boolean, ): String { return if (onlyName) { - mergeData.sources.joinToString { source -> + mergeSources.joinToString { source -> if (source.lang !in enabledLangs) { source.toString() } else { @@ -133,7 +132,7 @@ private fun getMergedSourcesString( } } } else { - mergeData.sources.joinToString() + mergeSources.joinToString() } } // SY <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/design/PreMigrationController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/design/PreMigrationController.kt index 8fb7e0c91..be113713b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/design/PreMigrationController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/design/PreMigrationController.kt @@ -31,6 +31,13 @@ class PreMigrationController(bundle: Bundle? = null) : FlexibleAdapter.OnItemClickListener, FabController, StartMigrationListener { + + constructor(mangaIds: List) : this( + bundleOf( + MANGA_IDS_EXTRA to mangaIds.toLongArray(), + ), + ) + private val sourceManager: SourceManager by injectLazy() private val prefs: PreferencesHelper by injectLazy() @@ -95,7 +102,7 @@ class PreMigrationController(bundle: Bundle? = null) : prefs.migrationSources().set(listOfSources) router.replaceTopController( - MigrationListController.create( + MigrationListController( MigrationProcedureConfig( config.toList(), extraSearchParams = extraParam, @@ -186,21 +193,13 @@ class PreMigrationController(bundle: Bundle? = null) : fun navigateToMigration(skipPre: Boolean, router: Router, mangaIds: List) { router.pushController( if (skipPre) { - MigrationListController.create( + MigrationListController( MigrationProcedureConfig(mangaIds, null), ) } else { - create(mangaIds) + PreMigrationController(mangaIds) }.withFadeTransaction().tag(if (skipPre) MigrationListController.TAG else null), ) } - - fun create(mangaIds: List): PreMigrationController { - return PreMigrationController( - bundleOf( - MANGA_IDS_EXTRA to mangaIds.toLongArray(), - ), - ) - } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigratingManga.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigratingManga.kt index 43b81c107..2d937148e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigratingManga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigratingManga.kt @@ -1,40 +1,38 @@ package eu.kanade.tachiyomi.ui.browse.migration.advanced.process -import eu.kanade.domain.manga.interactor.GetManga import eu.kanade.domain.manga.model.Manga -import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.source.SourceManager -import exh.util.DeferredField +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.MutableStateFlow import kotlin.coroutines.CoroutineContext class MigratingManga( - private val getManga: GetManga, - private val sourceManager: SourceManager, - val mangaId: Long, + val manga: Manga, + val chapterInfo: ChapterInfo, + val sourcesString: String, parentContext: CoroutineContext, + val getManga: suspend (SearchResult.Result) -> Manga?, + val getChapterInfo: suspend (SearchResult.Result) -> ChapterInfo, + val getSourceName: (Manga) -> String?, ) { - val searchResult = DeferredField() + val migrationScope = CoroutineScope(parentContext + SupervisorJob() + Dispatchers.Default) + + val searchResult = MutableStateFlow(SearchResult.Searching) // val progress = MutableStateFlow(1 to 0) - val migrationJob = parentContext + SupervisorJob() + Dispatchers.Default - - var migrationStatus = MigrationStatus.RUNNING - - @Volatile - private var manga: Manga? = null - suspend fun manga(): Manga? { - if (manga == null) manga = getManga.await(mangaId) - return manga + sealed class SearchResult { + object Searching : SearchResult() + object NotFound : SearchResult() + data class Result(val id: Long) : SearchResult() } - suspend fun mangaSource(): Source { - return sourceManager.getOrStub(manga()?.source ?: -1) - } + data class ChapterInfo( + val latestChapter: Float?, + val chapterCount: Int, + ) fun toModal(): MigrationProcessItem { // Create the model object. diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListController.kt index aa40cd37c..4532fc033 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListController.kt @@ -8,57 +8,39 @@ import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.view.View -import android.widget.Toast import androidx.core.graphics.ColorUtils import androidx.core.os.bundleOf import androidx.recyclerview.widget.LinearLayoutManager import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat import com.google.android.material.dialog.MaterialAlertDialogBuilder import dev.chrisbanes.insetter.applyInsetter -import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource -import eu.kanade.domain.manga.interactor.GetManga -import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.model.Manga -import eu.kanade.domain.manga.model.toDbManga import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.databinding.MigrationListControllerBinding -import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.source.SourceManager -import eu.kanade.tachiyomi.source.online.all.EHentai import eu.kanade.tachiyomi.ui.base.changehandler.OneWayFadeChangeHandler -import eu.kanade.tachiyomi.ui.base.controller.BaseController +import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.pushController import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.browse.migration.MigrationMangaDialog import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController +import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigratingManga.SearchResult import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController import eu.kanade.tachiyomi.ui.manga.MangaController -import eu.kanade.tachiyomi.util.lang.launchUI import eu.kanade.tachiyomi.util.system.getParcelableCompat import eu.kanade.tachiyomi.util.system.getResourceColor -import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.toast -import exh.eh.EHentaiThrottleManager -import exh.smartsearch.SmartSearchEngine -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.async -import kotlinx.coroutines.cancel -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Semaphore -import kotlinx.coroutines.sync.withPermit -import logcat.LogPriority -import uy.kohesive.injekt.injectLazy -import java.util.concurrent.atomic.AtomicInteger +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach class MigrationListController(bundle: Bundle? = null) : - BaseController(bundle), - MigrationProcessAdapter.MigrationProcessInterface { + NucleusController(bundle) { + + constructor(config: MigrationProcedureConfig) : this( + bundleOf( + CONFIG_EXTRA to config, + ), + ) init { setHasOptionsMenu(true) @@ -68,27 +50,19 @@ class MigrationListController(bundle: Bundle? = null) : val config = args.getParcelableCompat(CONFIG_EXTRA) - private val preferences: PreferencesHelper by injectLazy() - private val sourceManager: SourceManager by injectLazy() - - private val smartSearchEngine = SmartSearchEngine(config?.extraSearchParams) - private val syncChaptersWithSource: SyncChaptersWithSource by injectLazy() - private val getManga: GetManga by injectLazy() - private val updateManga: UpdateManga by injectLazy() - - private val migrationScope = CoroutineScope(Job() + Dispatchers.IO) - var migrationsJob: Job? = null - private set - private var migratingManga: MutableList? = null - private var selectedPosition: Int? = null + private var selectedMangaId: Long? = null private var manualMigrations = 0 - private val throttleManager = EHentaiThrottleManager() - override fun getTitle(): String { - return resources?.getString(R.string.migration) + " (${adapter?.items?.count { - it.manga.migrationStatus != MigrationStatus.RUNNING - }}/${adapter?.itemCount ?: 0})" + val notFinished = presenter.migratingItems.value.count { + it.searchResult.value != SearchResult.Searching + } + val total = presenter.migratingItems.value.size + return activity?.getString(R.string.migration) + " ($notFinished/$total)" + } + + override fun createPresenter(): MigrationListPresenter { + return MigrationListPresenter(config!!) } override fun createBinding(inflater: LayoutInflater) = MigrationListControllerBinding.inflate(inflater) @@ -103,15 +77,6 @@ class MigrationListController(bundle: Bundle? = null) : } setTitle() - val config = this.config ?: return - - val newMigratingManga = migratingManga ?: run { - val new = config.mangaIds.map { - MigratingManga(getManga, sourceManager, it, migrationScope.coroutineContext) - } - migratingManga = new.toMutableList() - new - } adapter = MigrationProcessAdapter(this) @@ -119,308 +84,112 @@ class MigrationListController(bundle: Bundle? = null) : binding.recycler.layoutManager = LinearLayoutManager(view.context) binding.recycler.setHasFixedSize(true) - adapter?.updateDataSet(newMigratingManga.map { it.toModal() }) - - if (migrationsJob == null) { - migrationsJob = migrationScope.launch { - runMigrations(newMigratingManga) + presenter.migratingItems + .onEach { + adapter?.updateDataSet(it.map { it.toModal() }) } - } + .launchIn(viewScope) } - private suspend fun runMigrations(mangas: List) { - throttleManager.resetThrottle() - if (config == null) return - val useSourceWithMost = preferences.useSourceWithMost().get() - val useSmartSearch = preferences.smartMigration().get() - - val sources = preferences.migrationSources().get().split("/").mapNotNull { - val value = it.toLongOrNull() ?: return@mapNotNull null - sourceManager.get(value) as? CatalogueSource - } - for (manga in mangas) { - if (migrationsJob?.isCancelled == true) { - break - } - // in case it was removed - if (manga.mangaId !in config.mangaIds) { - continue - } - if (!manga.searchResult.initialized && manga.migrationJob.isActive) { - val mangaObj = manga.manga() - - if (mangaObj == null) { - manga.searchResult.initialize(null) - continue - } - - val mangaSource = manga.mangaSource() - - val result = try { - CoroutineScope(manga.migrationJob).async { - val validSources = if (sources.size == 1) { - sources - } else { - sources.filter { it.id != mangaSource.id } - } - if (useSourceWithMost) { - val sourceSemaphore = Semaphore(3) - val processedSources = AtomicInteger() - - validSources.map { source -> - async async2@{ - sourceSemaphore.withPermit { - try { - val searchResult = if (useSmartSearch) { - smartSearchEngine.smartSearch(source, mangaObj.ogTitle) - } else { - smartSearchEngine.normalSearch(source, mangaObj.ogTitle) - } - - if (searchResult != null && !(searchResult.url == mangaObj.url && source.id == mangaObj.source)) { - val localManga = smartSearchEngine.networkToLocalManga( - searchResult, - source.id, - ) - - val chapters = if (source is EHentai) { - source.getChapterList(localManga.toSManga(), throttleManager::throttle) - } else { - source.getChapterList(localManga.toSManga()) - } - - try { - syncChaptersWithSource.await(chapters, localManga, source) - } catch (e: Exception) { - return@async2 null - } - manga.progress.value = validSources.size to processedSources.incrementAndGet() - localManga to chapters.size - } else { - null - } - } catch (e: CancellationException) { - // Ignore cancellations - throw e - } catch (e: Exception) { - null - } - } - } - }.mapNotNull { it.await() }.maxByOrNull { it.second }?.first - } else { - validSources.forEachIndexed { index, source -> - val searchResult = try { - val searchResult = if (useSmartSearch) { - smartSearchEngine.smartSearch(source, mangaObj.ogTitle) - } else { - smartSearchEngine.normalSearch(source, mangaObj.ogTitle) - } - - if (searchResult != null) { - val localManga = smartSearchEngine.networkToLocalManga(searchResult, source.id) - val chapters = try { - if (source is EHentai) { - source.getChapterList(localManga.toSManga(), throttleManager::throttle) - } else { - source.getChapterList(localManga.toSManga()) - } - } catch (e: Exception) { - this@MigrationListController.logcat(LogPriority.ERROR, e) - emptyList() - } - syncChaptersWithSource.await(chapters, localManga, source) - localManga - } else null - } catch (e: CancellationException) { - // Ignore cancellations - throw e - } catch (e: Exception) { - null - } - manga.progress.value = validSources.size to (index + 1) - if (searchResult != null) return@async searchResult - } - - null - } - }.await() - } catch (e: CancellationException) { - // Ignore canceled migrations - continue - } - - if (result != null && result.thumbnailUrl == null) { - try { - val newManga = sourceManager.getOrStub(result.source).getMangaDetails(result.toSManga()) - updateManga.awaitUpdateFromSource(result, newManga, true) - } catch (e: CancellationException) { - // Ignore cancellations - throw e - } catch (e: Exception) { - } - } - - manga.migrationStatus = if (result == null) MigrationStatus.MANGA_NOT_FOUND else MigrationStatus.MANGA_FOUND - adapter?.sourceFinished() - manga.searchResult.initialize(result?.id) - } - } - } - - override fun updateCount() { - launchUI { - if (router.backstack.lastOrNull()?.controller == this@MigrationListController) { - setTitle() - } + fun updateCount() { + if (router.backstack.lastOrNull()?.controller == this@MigrationListController) { + setTitle() } } override fun onDestroy() { super.onDestroy() - migrationScope.cancel() activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED } - override fun enableButtons() { + private fun enableButtons() { activity?.invalidateOptionsMenu() } - override fun removeManga(item: MigrationProcessItem) { - val ids = config?.mangaIds?.toMutableList() ?: return - val index = ids.indexOf(item.manga.mangaId) - if (index > -1) { - ids.removeAt(index) - config.mangaIds = ids - val index2 = migratingManga?.indexOf(item.manga) ?: return - if (index2 > -1) migratingManga?.removeAt(index2) + private fun noMigration() { + val res = resources + if (res != null) { + activity?.toast( + res.getQuantityString( + R.plurals.manga_migrated, + manualMigrations, + manualMigrations, + ), + ) + } + if (!presenter.hideNotFound) { + router.popCurrentController() } } - override fun noMigration() { - launchUI { - val res = resources - if (res != null) { - activity?.toast( - res.getQuantityString( - R.plurals.manga_migrated, - manualMigrations, - manualMigrations, - ), - ) - } - if (adapter?.hideNotFound == false) { - router.popCurrentController() - } - } - } - - override fun onMenuItemClick(position: Int, item: MenuItem) { + fun onMenuItemClick(mangaId: Long, item: MenuItem) { when (item.itemId) { R.id.action_search_manually -> { - launchUI { - val manga = adapter?.getItem(position)?.manga?.manga() ?: return@launchUI - selectedPosition = position - val sources = preferences.migrationSources().get().split("/").mapNotNull { - val value = it.toLongOrNull() ?: return@mapNotNull null - sourceManager.get(value) as? CatalogueSource - } - val validSources = if (sources.size == 1) { - sources - } else { - sources.filter { it.id != manga.source } - } - val searchController = SearchController(manga, validSources) - searchController.targetController = this@MigrationListController - router.pushController(searchController) + val manga = presenter.migratingItems.value + .find { it.manga.id == mangaId } + ?.manga + ?: return + selectedMangaId = mangaId + val sources = presenter.getMigrationSources() + val validSources = if (sources.size == 1) { + sources + } else { + sources.filter { it.id != manga.source } } + val searchController = SearchController(manga, validSources) + searchController.targetController = this@MigrationListController + router.pushController(searchController) } - R.id.action_skip -> adapter?.removeManga(position) + R.id.action_skip -> presenter.removeManga(mangaId) R.id.action_migrate_now -> { - adapter?.migrateManga(position, false) + migrateManga(mangaId, false) manualMigrations++ } R.id.action_copy_now -> { - adapter?.migrateManga(position, true) + migrateManga(mangaId, true) manualMigrations++ } } } fun useMangaForMigration(manga: Manga, source: Source) { - val firstIndex = selectedPosition ?: return - val migratingManga = adapter?.getItem(firstIndex) ?: return - migratingManga.manga.migrationStatus = MigrationStatus.RUNNING - adapter?.notifyItemChanged(firstIndex) - launchUI { - val result = CoroutineScope(migratingManga.manga.migrationJob).async { - val localManga = smartSearchEngine.networkToLocalManga(manga.toDbManga(), source.id) - try { - val chapters = source.getChapterList(localManga.toSManga()) - syncChaptersWithSource.await(chapters, localManga, source) - } catch (e: Exception) { - return@async null - } - localManga - }.await() - - if (result != null) { - try { - val newManga = sourceManager.getOrStub(result.source).getMangaDetails(result.toSManga()) - updateManga.awaitUpdateFromSource(result, newManga, true) - } catch (e: CancellationException) { - // Ignore cancellations - throw e - } catch (e: Exception) { - } - - migratingManga.manga.migrationStatus = MigrationStatus.MANGA_FOUND - migratingManga.manga.searchResult.set(result.id) - adapter?.notifyDataSetChanged() - } else { - migratingManga.manga.migrationStatus = MigrationStatus.MANGA_NOT_FOUND - activity?.toast(R.string.no_chapters_found_for_migration, Toast.LENGTH_LONG) - adapter?.notifyDataSetChanged() - } - } + presenter.useMangaForMigration(manga, source, selectedMangaId ?: return) } fun migrateMangas() { - launchUI { - adapter?.performMigrations(false) - navigateOut() - } + presenter.migrateMangas() } fun copyMangas() { - launchUI { - adapter?.performMigrations(true) - navigateOut() - } + presenter.copyMangas() } - private fun navigateOut() { - if (migratingManga?.size == 1) { - launchUI { - val hasDetails = router.backstack.any { it.controller is MangaController } - if (hasDetails) { - val manga = migratingManga?.firstOrNull()?.searchResult?.get()?.let { - getManga.await(it) - } - if (manga != null) { - val newStack = router.backstack.filter { - it.controller !is MangaController && - it.controller !is MigrationListController && - it.controller !is PreMigrationController - } + MangaController(manga.id).withFadeTransaction() - router.setBackstack(newStack, OneWayFadeChangeHandler()) - return@launchUI - } - } - router.popCurrentController() - } - } else router.popCurrentController() + fun migrateManga(mangaId: Long, copy: Boolean) { + presenter.migrateManga(mangaId, copy) + } + + fun removeManga(mangaId: Long) { + presenter.removeManga(mangaId) + } + + fun sourceFinished() { + updateCount() + if (presenter.migratingItems.value.isEmpty()) noMigration() + if (presenter.allMangasDone()) enableButtons() + } + + fun navigateOut(manga: Manga?) { + if (manga != null) { + val newStack = router.backstack.filter { + it.controller !is MangaController && + it.controller !is MigrationListController && + it.controller !is PreMigrationController + } + MangaController(manga.id).withFadeTransaction() + router.setBackstack(newStack, OneWayFadeChangeHandler()) + return + } + router.popCurrentController() } override fun handleBack(): Boolean { @@ -429,7 +198,6 @@ class MigrationListController(bundle: Bundle? = null) : .setTitle(R.string.stop_migrating) .setPositiveButton(R.string.action_stop) { _, _ -> router.popCurrentController() - migrationsJob?.cancel() } .setNegativeButton(android.R.string.cancel, null) .show() @@ -444,12 +212,12 @@ class MigrationListController(bundle: Bundle? = null) : override fun onPrepareOptionsMenu(menu: Menu) { // Initialize menu items. - val allMangasDone = adapter?.allMangasDone() ?: return + val allMangasDone = presenter.allMangasDone() val menuCopy = menu.findItem(R.id.action_copy_manga) val menuMigrate = menu.findItem(R.id.action_migrate_manga) - if (adapter?.itemCount == 1) { + if (presenter.migratingItems.value.size == 1) { menuMigrate.icon = VectorDrawableCompat.create( resources!!, R.drawable.ic_done_24dp, @@ -474,8 +242,8 @@ class MigrationListController(bundle: Bundle? = null) : } override fun onOptionsItemSelected(item: MenuItem): Boolean { - val totalManga = adapter?.itemCount ?: 0 - val mangaSkipped = adapter?.mangasSkipped() ?: 0 + val totalManga = presenter.migratingItems.value.size + val mangaSkipped = presenter.mangasSkipped() when (item.itemId) { R.id.action_copy_manga -> MigrationMangaDialog( this, @@ -493,34 +261,9 @@ class MigrationListController(bundle: Bundle? = null) : } return true } - /* - override fun canChangeTabs(block: () -> Unit): Boolean { - if (migrationsJob?.isCancelled == false || adapter?.allMangasDone() == true) { - activity?.let { - MaterialDialog(it).show { - title(R.string.stop_migrating) - positiveButton(R.string.action_stop) { - block() - migrationsJob?.cancel() - } - negativeButton(android.R.string.cancel) - } - } - return false - } - return true - }*/ companion object { const val CONFIG_EXTRA = "config_extra" const val TAG = "migration_list" - - fun create(config: MigrationProcedureConfig): MigrationListController { - return MigrationListController( - bundleOf( - CONFIG_EXTRA to config, - ), - ) - } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListPresenter.kt new file mode 100644 index 000000000..1117dffe6 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListPresenter.kt @@ -0,0 +1,546 @@ +package eu.kanade.tachiyomi.ui.browse.migration.advanced.process + +import android.os.Bundle +import android.widget.Toast +import eu.kanade.domain.category.interactor.GetCategories +import eu.kanade.domain.category.interactor.SetMangaCategories +import eu.kanade.domain.chapter.interactor.GetChapterByMangaId +import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource +import eu.kanade.domain.chapter.interactor.UpdateChapter +import eu.kanade.domain.chapter.model.Chapter +import eu.kanade.domain.chapter.model.ChapterUpdate +import eu.kanade.domain.history.interactor.GetHistoryByMangaId +import eu.kanade.domain.history.interactor.UpsertHistory +import eu.kanade.domain.history.model.HistoryUpdate +import eu.kanade.domain.manga.interactor.GetManga +import eu.kanade.domain.manga.interactor.GetMergedReferencesById +import eu.kanade.domain.manga.interactor.InsertManga +import eu.kanade.domain.manga.interactor.UpdateManga +import eu.kanade.domain.manga.model.Manga +import eu.kanade.domain.manga.model.MangaUpdate +import eu.kanade.domain.manga.model.hasCustomCover +import eu.kanade.domain.manga.model.toDbManga +import eu.kanade.domain.track.interactor.DeleteTrack +import eu.kanade.domain.track.interactor.GetTracks +import eu.kanade.domain.track.interactor.InsertTrack +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.cache.CoverCache +import eu.kanade.tachiyomi.data.database.models.toDomainManga +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.SourceManager +import eu.kanade.tachiyomi.source.getNameForMangaInfo +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.online.all.EHentai +import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter +import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags +import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigratingManga.SearchResult +import eu.kanade.tachiyomi.ui.manga.MangaController +import eu.kanade.tachiyomi.util.lang.launchIO +import eu.kanade.tachiyomi.util.lang.withUIContext +import eu.kanade.tachiyomi.util.system.logcat +import eu.kanade.tachiyomi.util.system.toast +import exh.eh.EHentaiThrottleManager +import exh.smartsearch.SmartSearchEngine +import exh.source.MERGED_SOURCE_ID +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.isActive +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit +import logcat.LogPriority +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.util.concurrent.atomic.AtomicInteger + +class MigrationListPresenter( + private val config: MigrationProcedureConfig, + private val preferences: PreferencesHelper = Injekt.get(), + private val sourceManager: SourceManager = Injekt.get(), + private val coverCache: CoverCache = Injekt.get(), + private val getManga: GetManga = Injekt.get(), + private val insertManga: InsertManga = Injekt.get(), + private val updateManga: UpdateManga = Injekt.get(), + private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(), + private val updateChapter: UpdateChapter = Injekt.get(), + private val getChapterByMangaId: GetChapterByMangaId = Injekt.get(), + private val getMergedReferencesById: GetMergedReferencesById = Injekt.get(), + private val getHistoryByMangaId: GetHistoryByMangaId = Injekt.get(), + private val upsertHistory: UpsertHistory = Injekt.get(), + private val getCategories: GetCategories = Injekt.get(), + private val setMangaCategories: SetMangaCategories = Injekt.get(), + private val getTracks: GetTracks = Injekt.get(), + private val insertTrack: InsertTrack = Injekt.get(), + private val deleteTrack: DeleteTrack = Injekt.get(), +) : BasePresenter() { + + private val smartSearchEngine = SmartSearchEngine(config.extraSearchParams) + private val throttleManager = EHentaiThrottleManager() + + var migrationsJob: Job? = null + private set + + val migratingItems = MutableStateFlow>(emptyList()) + + val hideNotFound = preferences.hideNotFoundMigration().get() + + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) + + if (migrationsJob?.isActive != true) { + migrationsJob = presenterScope.launchIO { + runMigrations( + config.mangaIds + .map { + async { + val manga = getManga.await(it) ?: return@async null + MigratingManga( + manga = manga, + chapterInfo = getChapterInfo(it), + sourcesString = sourceManager.getOrStub(manga.source).getNameForMangaInfo( + if (manga.source == MERGED_SOURCE_ID) { + getMergedReferencesById.await(manga.id) + .map { sourceManager.getOrStub(it.mangaSourceId) } + } else null, + ), + parentContext = presenterScope.coroutineContext, + getManga = ::getManga, + getChapterInfo = ::getChapterInfo, + getSourceName = ::getSourceName, + ) + } + } + .awaitAll() + .filterNotNull() + .also { + migratingItems.value = it + }, + ) + } + } + } + + private suspend fun getManga(result: SearchResult.Result) = getManga.await(result.id) + private suspend fun getChapterInfo(result: SearchResult.Result) = getChapterInfo(result.id) + private suspend fun getChapterInfo(id: Long) = getChapterByMangaId.await(id).let { chapters -> + MigratingManga.ChapterInfo( + latestChapter = chapters.maxOfOrNull { it.chapterNumber }, + chapterCount = chapters.size, + ) + } + private fun getSourceName(manga: Manga) = sourceManager.getOrStub(manga.source).getNameForMangaInfo(null) + + fun getMigrationSources() = preferences.migrationSources().get().split("/").mapNotNull { + val value = it.toLongOrNull() ?: return@mapNotNull null + sourceManager.get(value) as? CatalogueSource + } + + private suspend fun runMigrations(mangas: List) { + throttleManager.resetThrottle() + val useSourceWithMost = preferences.useSourceWithMost().get() + val useSmartSearch = preferences.smartMigration().get() + + val sources = getMigrationSources() + for (manga in mangas) { + if (migrationsJob?.isCancelled == true) { + break + } + // in case it was removed + if (manga.manga.id !in config.mangaIds) { + continue + } + if (manga.searchResult.value == SearchResult.Searching && manga.migrationScope.isActive) { + val mangaObj = manga.manga + val mangaSource = sourceManager.getOrStub(mangaObj.source) + + val result = try { + manga.migrationScope.async { + val validSources = if (sources.size == 1) { + sources + } else { + sources.filter { it.id != mangaSource.id } + } + if (useSourceWithMost) { + val sourceSemaphore = Semaphore(3) + val processedSources = AtomicInteger() + + validSources.map { source -> + async async2@{ + sourceSemaphore.withPermit { + try { + val searchResult = if (useSmartSearch) { + smartSearchEngine.smartSearch(source, mangaObj.ogTitle) + } else { + smartSearchEngine.normalSearch(source, mangaObj.ogTitle) + } + + if (searchResult != null && !(searchResult.url == mangaObj.url && source.id == mangaObj.source)) { + val localManga = networkToLocalManga( + searchResult, + source.id, + ) + + val chapters = if (source is EHentai) { + source.getChapterList(localManga.toSManga(), throttleManager::throttle) + } else { + source.getChapterList(localManga.toSManga()) + } + + try { + syncChaptersWithSource.await(chapters, localManga, source) + } catch (e: Exception) { + return@async2 null + } + manga.progress.value = validSources.size to processedSources.incrementAndGet() + localManga to chapters.size + } else { + null + } + } catch (e: CancellationException) { + // Ignore cancellations + throw e + } catch (e: Exception) { + null + } + } + } + }.mapNotNull { it.await() }.maxByOrNull { it.second }?.first + } else { + validSources.forEachIndexed { index, source -> + val searchResult = try { + val searchResult = if (useSmartSearch) { + smartSearchEngine.smartSearch(source, mangaObj.ogTitle) + } else { + smartSearchEngine.normalSearch(source, mangaObj.ogTitle) + } + + if (searchResult != null) { + val localManga = networkToLocalManga(searchResult, source.id) + val chapters = try { + if (source is EHentai) { + source.getChapterList(localManga.toSManga(), throttleManager::throttle) + } else { + source.getChapterList(localManga.toSManga()) + } + } catch (e: Exception) { + this@MigrationListPresenter.logcat(LogPriority.ERROR, e) + emptyList() + } + syncChaptersWithSource.await(chapters, localManga, source) + localManga + } else null + } catch (e: CancellationException) { + // Ignore cancellations + throw e + } catch (e: Exception) { + null + } + manga.progress.value = validSources.size to (index + 1) + if (searchResult != null) return@async searchResult + } + + null + } + }.await() + } catch (e: CancellationException) { + // Ignore canceled migrations + continue + } + + if (result != null && result.thumbnailUrl == null) { + try { + val newManga = sourceManager.getOrStub(result.source).getMangaDetails(result.toSManga()) + updateManga.awaitUpdateFromSource(result, newManga, true) + } catch (e: CancellationException) { + // Ignore cancellations + throw e + } catch (e: Exception) { + } + } + + manga.searchResult.value = if (result == null) { + SearchResult.NotFound + } else { + SearchResult.Result(result.id) + } + if (result == null && hideNotFound) { + removeManga(manga) + } + withUIContext { + view?.sourceFinished() + } + } + } + } + + fun allMangasDone() = migratingItems.value.all { it.searchResult.value != SearchResult.Searching } && + migratingItems.value.any { it.searchResult.value is SearchResult.Result } + + fun mangasSkipped() = migratingItems.value.count { it.searchResult.value == SearchResult.NotFound } + + private suspend fun migrateMangaInternal( + prevManga: Manga, + manga: Manga, + replace: Boolean, + ) { + val flags = preferences.migrateFlags().get() + // Update chapters read + if (MigrationFlags.hasChapters(flags)) { + val prevMangaChapters = getChapterByMangaId.await(prevManga.id) + val maxChapterRead = + prevMangaChapters.filter(Chapter::read).maxOfOrNull(Chapter::chapterNumber) + val dbChapters = getChapterByMangaId.await(manga.id) + val prevHistoryList = getHistoryByMangaId.await(prevManga.id) + + val chapterUpdates = mutableListOf() + val historyUpdates = mutableListOf() + + dbChapters.forEach { chapter -> + if (chapter.isRecognizedNumber) { + val prevChapter = prevMangaChapters.find { it.isRecognizedNumber && it.chapterNumber == chapter.chapterNumber } + if (prevChapter != null) { + chapterUpdates += ChapterUpdate( + id = chapter.id, + bookmark = prevChapter.bookmark, + read = prevChapter.read, + dateFetch = prevChapter.dateFetch, + ) + prevHistoryList.find { it.chapterId == prevChapter.id }?.let { prevHistory -> + historyUpdates += HistoryUpdate( + chapter.id, + prevHistory.readAt ?: return@let, + prevHistory.readDuration, + ) + } + } else if (maxChapterRead != null && chapter.chapterNumber <= maxChapterRead) { + chapterUpdates += ChapterUpdate( + id = chapter.id, + read = true, + ) + } + } + } + + updateChapter.awaitAll(chapterUpdates) + historyUpdates.forEach { + upsertHistory.await(it) + } + } + // Update categories + if (MigrationFlags.hasCategories(flags)) { + val categories = getCategories.await(prevManga.id) + setMangaCategories.await(manga.id, categories.map { it.id }) + } + // Update track + if (MigrationFlags.hasTracks(flags)) { + val tracks = getTracks.await(prevManga.id) + if (tracks.isNotEmpty()) { + getTracks.await(manga.id).forEach { + deleteTrack.await(manga.id, it.syncId) + } + insertTrack.awaitAll(tracks.map { it.copy(mangaId = manga.id) }) + } + } + // Update custom cover + if (MigrationFlags.hasCustomCover(flags) && prevManga.hasCustomCover(coverCache)) { + coverCache.setCustomCoverToCache(manga.toDbManga(), coverCache.getCustomCoverFile(prevManga.id).inputStream()) + } + + var mangaUpdate = MangaUpdate(manga.id, favorite = true, dateAdded = System.currentTimeMillis()) + var prevMangaUpdate: MangaUpdate? = null + // Update extras + if (MigrationFlags.hasExtra(flags)) { + mangaUpdate = mangaUpdate.copy( + chapterFlags = prevManga.chapterFlags, + viewerFlags = prevManga.viewerFlags, + ) + } + // Update favorite status + if (replace) { + prevMangaUpdate = MangaUpdate( + id = prevManga.id, + favorite = false, + dateAdded = 0, + ) + mangaUpdate = mangaUpdate.copy( + dateAdded = prevManga.dateAdded, + ) + } + + updateManga.awaitAll(listOfNotNull(mangaUpdate, prevMangaUpdate)) + } + + fun useMangaForMigration(manga: Manga, source: Source, selectedMangaId: Long) { + val migratingManga = migratingItems.value.find { it.manga.id == selectedMangaId } + ?: return + migratingManga.searchResult.value = SearchResult.Searching + presenterScope.launchIO { + val result = migratingManga.migrationScope.async { + val localManga = networkToLocalManga(manga.toDbManga(), source.id) + try { + val chapters = source.getChapterList(localManga.toSManga()) + syncChaptersWithSource.await(chapters, localManga, source) + } catch (e: Exception) { + return@async null + } + localManga + }.await() + + if (result != null) { + try { + val newManga = sourceManager.getOrStub(result.source).getMangaDetails(result.toSManga()) + updateManga.awaitUpdateFromSource(result, newManga, true) + } catch (e: CancellationException) { + // Ignore cancellations + throw e + } catch (e: Exception) { + } + + migratingManga.searchResult.value = SearchResult.Result(result.id) + } else { + migratingManga.searchResult.value = SearchResult.NotFound + withUIContext { + view?.activity?.toast(R.string.no_chapters_found_for_migration, Toast.LENGTH_LONG) + } + } + } + } + + fun migrateMangas() { + presenterScope.launchIO { + migratingItems.value.forEach { manga -> + val searchResult = manga.searchResult.value + if (searchResult is SearchResult.Result) { + val toMangaObj = getManga.await(searchResult.id) ?: return@forEach + migrateMangaInternal( + manga.manga, + toMangaObj, + true, + ) + } + } + + navigateOut() + } + } + + fun copyMangas() { + presenterScope.launchIO { + migratingItems.value.forEach { manga -> + val searchResult = manga.searchResult.value + if (searchResult is SearchResult.Result) { + val toMangaObj = getManga.await(searchResult.id) ?: return@forEach + migrateMangaInternal( + manga.manga, + toMangaObj, + false, + ) + } + } + navigateOut() + } + } + + private suspend fun navigateOut() { + val view = view ?: return + if (migratingItems.value.size == 1) { + val hasDetails = view.router.backstack.any { it.controller is MangaController } + if (hasDetails) { + val manga = (migratingItems.value.firstOrNull()?.searchResult?.value as? SearchResult.Result)?.let { + getManga.await(it.id) + } + withUIContext { + view.navigateOut(manga) + } + return + } + } + withUIContext { + view.navigateOut(null) + } + } + + fun migrateManga(mangaId: Long, copy: Boolean) { + presenterScope.launchIO { + val manga = migratingItems.value.find { it.manga.id == mangaId } + ?: return@launchIO + + val toMangaObj = getManga.await((manga.searchResult.value as? SearchResult.Result)?.id ?: return@launchIO) + ?: return@launchIO + migrateMangaInternal( + manga.manga, + toMangaObj, + !copy, + ) + + removeManga(mangaId) + } + } + + fun removeManga(mangaId: Long) { + presenterScope.launchIO { + val item = migratingItems.value.find { it.manga.id == mangaId } + ?: return@launchIO + if (migratingItems.value.size == 1) { + item.searchResult.value = SearchResult.NotFound + item.migrationScope.cancel() + withUIContext { + view?.sourceFinished() + } + return@launchIO + } + removeManga(item) + item.migrationScope.cancel() + withUIContext { + view?.sourceFinished() + } + } + } + + fun removeManga(item: MigratingManga) { + val ids = config.mangaIds.toMutableList() + val index = ids.indexOf(item.manga.id) + if (index > -1) { + ids.removeAt(index) + config.mangaIds = ids + val index2 = migratingItems.value.indexOf(item) + if (index2 > -1) migratingItems.value = migratingItems.value - item + } + } + + override fun onDestroy() { + super.onDestroy() + migrationsJob?.cancel() + } + + /** + * Returns a manga from the database for the given manga from network. It creates a new entry + * if the manga is not yet in the database. + * + * @param sManga the manga from the source. + * @return a manga from the database. + */ + private suspend fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga { + var localManga = getManga.await(sManga.url, sourceId) + if (localManga == null) { + val newManga = eu.kanade.tachiyomi.data.database.models.Manga.create(sManga.url, sManga.title, sourceId) + newManga.copyFrom(sManga) + newManga.id = -1 + val result = run { + val id = insertManga.await(newManga.toDomainManga()!!) + getManga.await(id!!) + } + localManga = result + } else if (!localManga.favorite) { + // if the manga isn't a favorite, set its display title from source + // if it later becomes a favorite, updated title will go to db + localManga = localManga.copy(ogTitle = sManga.title) + } + return localManga!! + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcessAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcessAdapter.kt index d7d25cb3a..1a6a967bf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcessAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcessAdapter.kt @@ -1,220 +1,7 @@ package eu.kanade.tachiyomi.ui.browse.migration.advanced.process -import android.view.MenuItem import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.kanade.domain.category.interactor.GetCategories -import eu.kanade.domain.category.interactor.SetMangaCategories -import eu.kanade.domain.chapter.interactor.GetChapterByMangaId -import eu.kanade.domain.chapter.interactor.UpdateChapter -import eu.kanade.domain.chapter.model.Chapter -import eu.kanade.domain.chapter.model.ChapterUpdate -import eu.kanade.domain.history.interactor.GetHistoryByMangaId -import eu.kanade.domain.history.interactor.UpsertHistory -import eu.kanade.domain.history.model.HistoryUpdate -import eu.kanade.domain.manga.interactor.GetManga -import eu.kanade.domain.manga.interactor.UpdateManga -import eu.kanade.domain.manga.model.Manga -import eu.kanade.domain.manga.model.MangaUpdate -import eu.kanade.domain.manga.model.hasCustomCover -import eu.kanade.domain.manga.model.toDbManga -import eu.kanade.domain.track.interactor.DeleteTrack -import eu.kanade.domain.track.interactor.GetTracks -import eu.kanade.domain.track.interactor.InsertTrack -import eu.kanade.tachiyomi.data.cache.CoverCache -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags -import eu.kanade.tachiyomi.util.lang.launchUI -import eu.kanade.tachiyomi.util.lang.withIOContext -import kotlinx.coroutines.cancel -import uy.kohesive.injekt.injectLazy class MigrationProcessAdapter( val controller: MigrationListController, -) : FlexibleAdapter(null, controller, true) { - private val preferences: PreferencesHelper by injectLazy() - private val coverCache: CoverCache by injectLazy() - private val getManga: GetManga by injectLazy() - private val updateManga: UpdateManga by injectLazy() - private val updateChapter: UpdateChapter by injectLazy() - private val getChapterByMangaId: GetChapterByMangaId by injectLazy() - private val getHistoryByMangaId: GetHistoryByMangaId by injectLazy() - private val upsertHistory: UpsertHistory by injectLazy() - private val getCategories: GetCategories by injectLazy() - private val setMangaCategories: SetMangaCategories by injectLazy() - private val getTracks: GetTracks by injectLazy() - private val insertTrack: InsertTrack by injectLazy() - private val deleteTrack: DeleteTrack by injectLazy() - - var items: List = emptyList() - - val menuItemListener: MigrationProcessInterface = controller - - val hideNotFound = preferences.hideNotFoundMigration().get() - - override fun updateDataSet(items: List?) { - this.items = items.orEmpty() - super.updateDataSet(items) - } - - interface MigrationProcessInterface { - fun onMenuItemClick(position: Int, item: MenuItem) - fun enableButtons() - fun removeManga(item: MigrationProcessItem) - fun noMigration() - fun updateCount() - } - - fun sourceFinished() { - menuItemListener.updateCount() - if (itemCount == 0) menuItemListener.noMigration() - if (allMangasDone()) menuItemListener.enableButtons() - } - - fun allMangasDone() = items.all { it.manga.migrationStatus != MigrationStatus.RUNNING } && - items.any { it.manga.migrationStatus == MigrationStatus.MANGA_FOUND } - - fun mangasSkipped() = items.count { it.manga.migrationStatus == MigrationStatus.MANGA_NOT_FOUND } - - suspend fun performMigrations(copy: Boolean) { - withIOContext { - currentItems.forEach { migratingManga -> - val manga = migratingManga.manga - if (manga.searchResult.initialized) { - val toMangaObj = getManga.await(manga.searchResult.get() ?: return@forEach) - ?: return@forEach - migrateMangaInternal( - manga.manga() ?: return@forEach, - toMangaObj, - !copy, - ) - } - } - } - } - - fun migrateManga(position: Int, copy: Boolean) { - launchUI { - val manga = getItem(position)?.manga ?: return@launchUI - - val toMangaObj = getManga.await(manga.searchResult.get() ?: return@launchUI) - ?: return@launchUI - migrateMangaInternal( - manga.manga() ?: return@launchUI, - toMangaObj, - !copy, - ) - - removeManga(position) - } - } - - fun removeManga(position: Int) { - val item = getItem(position) ?: return - if (items.size == 1) { - item.manga.migrationStatus = MigrationStatus.MANGA_NOT_FOUND - item.manga.migrationJob.cancel() - item.manga.searchResult.set(null) - sourceFinished() - notifyItemChanged(position) - return - } - menuItemListener.removeManga(item) - item.manga.migrationJob.cancel() - removeItem(position) - items = currentItems - sourceFinished() - } - - private suspend fun migrateMangaInternal( - prevManga: Manga, - manga: Manga, - replace: Boolean, - ) { - controller.config ?: return - val flags = preferences.migrateFlags().get() - // Update chapters read - if (MigrationFlags.hasChapters(flags)) { - val prevMangaChapters = getChapterByMangaId.await(prevManga.id) - val maxChapterRead = - prevMangaChapters.filter(Chapter::read).maxOfOrNull(Chapter::chapterNumber) - val dbChapters = getChapterByMangaId.await(manga.id) - val prevHistoryList = getHistoryByMangaId.await(prevManga.id) - - val chapterUpdates = mutableListOf() - val historyUpdates = mutableListOf() - - dbChapters.forEach { chapter -> - if (chapter.isRecognizedNumber) { - val prevChapter = prevMangaChapters.find { it.isRecognizedNumber && it.chapterNumber == chapter.chapterNumber } - if (prevChapter != null) { - chapterUpdates += ChapterUpdate( - id = chapter.id, - bookmark = prevChapter.bookmark, - read = prevChapter.read, - dateFetch = prevChapter.dateFetch, - ) - prevHistoryList.find { it.chapterId == prevChapter.id }?.let { prevHistory -> - historyUpdates += HistoryUpdate( - chapter.id, - prevHistory.readAt ?: return@let, - prevHistory.readDuration, - ) - } - } else if (maxChapterRead != null && chapter.chapterNumber <= maxChapterRead) { - chapterUpdates += ChapterUpdate( - id = chapter.id, - read = true, - ) - } - } - } - - updateChapter.awaitAll(chapterUpdates) - historyUpdates.forEach { - upsertHistory.await(it) - } - } - // Update categories - if (MigrationFlags.hasCategories(flags)) { - val categories = getCategories.await(prevManga.id) - setMangaCategories.await(manga.id, categories.map { it.id }) - } - // Update track - if (MigrationFlags.hasTracks(flags)) { - val tracks = getTracks.await(prevManga.id) - if (tracks.isNotEmpty()) { - getTracks.await(manga.id).forEach { - deleteTrack.await(manga.id, it.syncId) - } - insertTrack.awaitAll(tracks.map { it.copy(mangaId = manga.id) }) - } - } - // Update custom cover - if (MigrationFlags.hasCustomCover(flags) && prevManga.hasCustomCover(coverCache)) { - coverCache.setCustomCoverToCache(manga.toDbManga(), coverCache.getCustomCoverFile(prevManga.id).inputStream()) - } - - var mangaUpdate = MangaUpdate(manga.id, favorite = true, dateAdded = System.currentTimeMillis()) - var prevMangaUpdate: MangaUpdate? = null - // Update extras - if (MigrationFlags.hasExtra(flags)) { - mangaUpdate = mangaUpdate.copy( - chapterFlags = prevManga.chapterFlags, - viewerFlags = prevManga.viewerFlags, - ) - } - // Update favorite status - if (replace) { - prevMangaUpdate = MangaUpdate( - id = prevManga.id, - favorite = false, - dateAdded = 0, - ) - mangaUpdate = mangaUpdate.copy( - dateAdded = prevManga.dateAdded, - ) - } - - updateManga.awaitAll(listOfNotNull(mangaUpdate, prevMangaUpdate)) - } -} +) : FlexibleAdapter(null, controller, true) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcessHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcessHolder.kt index d6499f4ff..fd7ada2a3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcessHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcessHolder.kt @@ -6,52 +6,51 @@ import androidx.core.view.isInvisible import androidx.core.view.isVisible import coil.dispose import eu.davidea.viewholders.FlexibleViewHolder -import eu.kanade.domain.chapter.interactor.GetChapterByMangaId -import eu.kanade.domain.manga.interactor.GetManga -import eu.kanade.domain.manga.interactor.GetMergedReferencesById import eu.kanade.domain.manga.model.Manga import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.databinding.MigrationMangaCardBinding import eu.kanade.tachiyomi.databinding.MigrationProcessItemBinding -import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.ui.base.controller.pushController +import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigratingManga.ChapterInfo +import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigratingManga.SearchResult import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.util.lang.launchUI +import eu.kanade.tachiyomi.util.lang.withIOContext +import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.view.loadAutoPause import eu.kanade.tachiyomi.util.view.setVectorCompat -import exh.source.MERGED_SOURCE_ID +import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import reactivecircus.flowbinding.android.view.clicks -import uy.kohesive.injekt.injectLazy import java.text.DecimalFormat +import java.util.concurrent.CopyOnWriteArrayList class MigrationProcessHolder( - private val view: View, + view: View, private val adapter: MigrationProcessAdapter, ) : FlexibleViewHolder(view, adapter) { - private val sourceManager: SourceManager by injectLazy() - private val getManga: GetManga by injectLazy() - private val getChapterByMangaId: GetChapterByMangaId by injectLazy() - private val getMergedReferencesById: GetMergedReferencesById by injectLazy() - private var item: MigrationProcessItem? = null private val binding = MigrationProcessItemBinding.bind(view) + private val jobs = CopyOnWriteArrayList() + init { // We need to post a Runnable to show the popup to make sure that the PopupMenu is // correctly positioned. The reason being that the view may change position before the // PopupMenu is shown. binding.migrationMenu.setOnClickListener { it.post { showPopupMenu(it) } } - binding.skipManga.setOnClickListener { it.post { adapter.removeManga(bindingAdapterPosition) } } + binding.skipManga.setOnClickListener { it.post { adapter.controller.removeManga(item?.manga?.manga?.id ?: return@post) } } } fun bind(item: MigrationProcessItem) { this.item = item - launchUI { - val manga = item.manga.manga() - val source = item.manga.mangaSource() + jobs.removeAll { it.cancel(); true } + jobs += adapter.controller.viewScope.launchUI { + val migrateManga = item.manga + val manga = migrateManga.manga binding.migrationMenu.setVectorCompat( R.drawable.ic_more_24dp, @@ -64,67 +63,74 @@ class MigrationProcessHolder( binding.migrationMenu.isInvisible = true binding.skipManga.isVisible = true binding.migrationMangaCardTo.resetManga() - if (manga != null) { - binding.migrationMangaCardFrom.attachManga(manga, source) - binding.migrationMangaCardFrom.root.clicks() - .onEach { - adapter.controller.router.pushController( - MangaController( - manga.id, - true, - ), - ) - } - .launchIn(adapter.controller.viewScope) + binding.migrationMangaCardFrom.attachManga(manga, item.manga.sourcesString, item.manga.chapterInfo) + jobs += binding.migrationMangaCardFrom.root.clicks() + .onEach { + adapter.controller.router.pushController( + MangaController( + manga.id, + true, + ), + ) + } + .launchIn(adapter.controller.viewScope) - /*launchUI { - item.manga.progress.asFlow().collect { (max, progress) -> - withUIContext { - migration_manga_card_to.search_progress.let { progressBar -> - progressBar.max = max - progressBar.progress = progress + /*launchUI { + item.manga.progress.asFlow().collect { (max, progress) -> + withUIContext { + migration_manga_card_to.search_progress.let { progressBar -> + progressBar.max = max + progressBar.progress = progress + } + } + } + }*/ + + jobs += migrateManga.searchResult + .onEach { searchResult -> + this@MigrationProcessHolder.logcat { (searchResult to (migrateManga.manga.id to this@MigrationProcessHolder.item?.manga?.manga?.id)).toString() } + if (migrateManga.manga.id != this@MigrationProcessHolder.item?.manga?.manga?.id || + searchResult == SearchResult.Searching + ) { + return@onEach + } + + val resultManga = withIOContext { + (searchResult as? SearchResult.Result) + ?.let { migrateManga.getManga(it) } + } + if (resultManga != null) { + val (sourceName, latestChapter) = withIOContext { + val sourceNameAsync = async { migrateManga.getSourceName(resultManga).orEmpty() } + val latestChapterAsync = async { migrateManga.getChapterInfo(searchResult as SearchResult.Result) } + sourceNameAsync.await() to latestChapterAsync.await() + } + + binding.migrationMangaCardTo.attachManga(resultManga, sourceName, latestChapter) + jobs += binding.migrationMangaCardTo.root.clicks() + .onEach { + adapter.controller.router.pushController( + MangaController( + resultManga.id, + true, + ), + ) } - } - } - }*/ - - val searchResult = item.manga.searchResult.get()?.let { - getManga.await(it) - } - val resultSource = searchResult?.source?.let { - sourceManager.get(it) - } - - if (item.manga.mangaId != this@MigrationProcessHolder.item?.manga?.mangaId || - item.manga.migrationStatus == MigrationStatus.RUNNING - ) { - return@launchUI - } - if (searchResult != null && resultSource != null) { - binding.migrationMangaCardTo.attachManga(searchResult, resultSource) - binding.migrationMangaCardTo.root.clicks() - .onEach { - adapter.controller.router.pushController( - MangaController( - searchResult.id, - true, - ), - ) - } - .launchIn(adapter.controller.viewScope) - } else { - if (adapter.hideNotFound) { - adapter.removeManga(bindingAdapterPosition) + .launchIn(adapter.controller.viewScope) } else { binding.migrationMangaCardTo.progress.isVisible = false - binding.migrationMangaCardTo.title.text = view.context.applicationContext + binding.migrationMangaCardTo.title.text = itemView.context .getString(R.string.no_alternatives_found) } + + binding.migrationMenu.isVisible = true + binding.skipManga.isVisible = false + adapter.controller.sourceFinished() } - binding.migrationMenu.isVisible = true - binding.skipManga.isVisible = false - adapter.sourceFinished() - } + .catch { + this@MigrationProcessHolder.logcat(throwable = it) { "Error updating result info" } + } + .launchIn(adapter.controller.viewScope) } } @@ -139,39 +145,35 @@ class MigrationProcessHolder( mangaLastChapterLabel.text = "" } - private suspend fun MigrationMangaCardBinding.attachManga(manga: Manga, source: Source) { + private fun MigrationMangaCardBinding.attachManga( + manga: Manga, + sourceString: String, + chapterInfo: ChapterInfo, + ) { progress.isVisible = false thumbnail.loadAutoPause(manga) title.text = if (manga.title.isBlank()) { - view.context.getString(R.string.unknown) + itemView.context.getString(R.string.unknown) } else { manga.ogTitle } - mangaSourceLabel.text = if (source.id == MERGED_SOURCE_ID) { - getMergedReferencesById.await(manga.id).map { - sourceManager.getOrStub(it.mangaSourceId).toString() - }.distinct().joinToString() - } else { - source.toString() - } + mangaSourceLabel.text = sourceString - val chapters = getChapterByMangaId.await(manga.id) // For rounded corners badges.leftBadges.clipToOutline = true badges.rightBadges.clipToOutline = true badges.unreadText.isVisible = true - badges.unreadText.text = chapters.size.toString() - val latestChapter = chapters.maxOfOrNull { it.chapterNumber } ?: -1f + badges.unreadText.text = chapterInfo.chapterCount.toString() - if (latestChapter > 0f) { - mangaLastChapterLabel.text = root.context.getString( + if (chapterInfo.latestChapter != null && chapterInfo.latestChapter > 0f) { + mangaLastChapterLabel.text = itemView.context.getString( R.string.latest_, - DecimalFormat("#.#").format(latestChapter), + DecimalFormat("#.#").format(chapterInfo.latestChapter), ) } else { - mangaLastChapterLabel.text = root.context.getString( + mangaLastChapterLabel.text = itemView.context.getString( R.string.latest_, root.context.getString(R.string.unknown), ) @@ -191,14 +193,14 @@ class MigrationProcessHolder( popup.menu.findItem(R.id.action_search_manually).isVisible = true // Hide download and show delete if the chapter is downloaded - if (mangas.searchResult.content != null) { + if (mangas.searchResult.value != SearchResult.Searching) { popup.menu.findItem(R.id.action_migrate_now).isVisible = true popup.menu.findItem(R.id.action_copy_now).isVisible = true } // Set a listener so we are notified if a menu item is clicked popup.setOnMenuItemClickListener { menuItem -> - adapter.menuItemListener.onMenuItemClick(bindingAdapterPosition, menuItem) + adapter.controller.onMenuItemClick(item.manga.manga.id, menuItem) true } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcessItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcessItem.kt index ec304f130..52a27633c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcessItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcessItem.kt @@ -30,12 +30,12 @@ class MigrationProcessItem(val manga: MigratingManga) : override fun equals(other: Any?): Boolean { if (this === other) return true if (other is MigrationProcessItem) { - return manga.mangaId == other.manga.mangaId + return manga.manga.id == other.manga.manga.id } return false } override fun hashCode(): Int { - return manga.mangaId.toInt() + return manga.manga.id.hashCode() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationStatus.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationStatus.kt deleted file mode 100644 index c27e94556..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationStatus.kt +++ /dev/null @@ -1,7 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.migration.advanced.process - -enum class MigrationStatus { - RUNNING, - MANGA_FOUND, - MANGA_NOT_FOUND -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchController.kt index 122cbb0ff..c841dc88e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchController.kt @@ -45,8 +45,7 @@ class SourceSearchController( val sourceManager = Injekt.get() val source = sourceManager.get(manga.source) ?: return@SourceSearchScreen migrationListController.useMangaForMigration(manga, source) - router.popCurrentController() - router.popCurrentController() + router.popToTag(MigrationListController.TAG) }, // SY <-- onWebViewClick = f@{ diff --git a/app/src/main/java/exh/util/DeferredField.kt b/app/src/main/java/exh/util/DeferredField.kt deleted file mode 100644 index ec3c73d57..000000000 --- a/app/src/main/java/exh/util/DeferredField.kt +++ /dev/null @@ -1,57 +0,0 @@ -package exh.util - -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock - -/** - * Field that can be initialized later. Users can suspend while waiting for the field to initialize. - * - * @author nulldev - */ -class DeferredField { - - @Volatile - var content: T? = null - - @Volatile - var initialized = false - private set - - private val mutex = Mutex(true) - - /** - * Initialize the field - */ - fun initialize(content: T) { - // Fast-path new listeners - this.content = content - initialized = true - - // Notify current listeners - mutex.unlock() - } - - fun set(content: T) { - mutex.tryLock() - this.content = content - initialized = true - // Notify current listeners - mutex.unlock() - } - - /** - * Will only suspend if !initialized. - */ - suspend fun get(): T { - // Check if field is initialized and return immediately if it is - @Suppress("UNCHECKED_CAST") - if (initialized) return content as T - - // Wait for field to initialize - mutex.withLock {} - - // Field is initialized, return value - @Suppress("UNCHECKED_CAST") - return content as T - } -}