Rewrite migration, shove all logic into a presenter instead of the UI
This commit is contained in:
parent
6e1e42fefd
commit
96f24e0600
@ -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,
|
||||
|
@ -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<Source>?): String {
|
||||
val preferences = Injekt.get<PreferencesHelper>()
|
||||
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<Source>,
|
||||
enabledLangs: List<String>,
|
||||
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 <--
|
||||
|
@ -31,6 +31,13 @@ class PreMigrationController(bundle: Bundle? = null) :
|
||||
FlexibleAdapter.OnItemClickListener,
|
||||
FabController,
|
||||
StartMigrationListener {
|
||||
|
||||
constructor(mangaIds: List<Long>) : 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<Long>) {
|
||||
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<Long>): PreMigrationController {
|
||||
return PreMigrationController(
|
||||
bundleOf(
|
||||
MANGA_IDS_EXTRA to mangaIds.toLongArray(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<Long?>()
|
||||
val migrationScope = CoroutineScope(parentContext + SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
val searchResult = MutableStateFlow<SearchResult>(SearchResult.Searching)
|
||||
|
||||
// <MAX, PROGRESS>
|
||||
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.
|
||||
|
@ -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<MigrationListControllerBinding>(bundle),
|
||||
MigrationProcessAdapter.MigrationProcessInterface {
|
||||
NucleusController<MigrationListControllerBinding, MigrationListPresenter>(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<MigrationProcedureConfig>(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<MigratingManga>? = 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<MigratingManga>) {
|
||||
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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<MigrationListController>() {
|
||||
|
||||
private val smartSearchEngine = SmartSearchEngine(config.extraSearchParams)
|
||||
private val throttleManager = EHentaiThrottleManager()
|
||||
|
||||
var migrationsJob: Job? = null
|
||||
private set
|
||||
|
||||
val migratingItems = MutableStateFlow<List<MigratingManga>>(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<MigratingManga>) {
|
||||
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<ChapterUpdate>()
|
||||
val historyUpdates = mutableListOf<HistoryUpdate>()
|
||||
|
||||
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!!
|
||||
}
|
||||
}
|
@ -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<MigrationProcessItem>(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<MigrationProcessItem> = emptyList()
|
||||
|
||||
val menuItemListener: MigrationProcessInterface = controller
|
||||
|
||||
val hideNotFound = preferences.hideNotFoundMigration().get()
|
||||
|
||||
override fun updateDataSet(items: List<MigrationProcessItem>?) {
|
||||
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<ChapterUpdate>()
|
||||
val historyUpdates = mutableListOf<HistoryUpdate>()
|
||||
|
||||
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<MigrationProcessItem>(null, controller, true)
|
||||
|
@ -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<Job>()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
|
||||
|
||||
enum class MigrationStatus {
|
||||
RUNNING,
|
||||
MANGA_FOUND,
|
||||
MANGA_NOT_FOUND
|
||||
}
|
@ -45,8 +45,7 @@ class SourceSearchController(
|
||||
val sourceManager = Injekt.get<SourceManager>()
|
||||
val source = sourceManager.get(manga.source) ?: return@SourceSearchScreen
|
||||
migrationListController.useMangaForMigration(manga, source)
|
||||
router.popCurrentController()
|
||||
router.popCurrentController()
|
||||
router.popToTag(MigrationListController.TAG)
|
||||
},
|
||||
// SY <--
|
||||
onWebViewClick = f@{
|
||||
|
@ -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<T> {
|
||||
|
||||
@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
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user