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,
|
title = state.manga.title,
|
||||||
author = state.manga.author,
|
author = state.manga.author,
|
||||||
artist = state.manga.artist,
|
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 },
|
isStubSource = remember { state.source is SourceManager.StubSource },
|
||||||
coverDataProvider = { state.manga },
|
coverDataProvider = { state.manga },
|
||||||
status = state.manga.status,
|
status = state.manga.status,
|
||||||
@ -662,7 +662,7 @@ fun MangaScreenLargeImpl(
|
|||||||
title = state.manga.title,
|
title = state.manga.title,
|
||||||
author = state.manga.author,
|
author = state.manga.author,
|
||||||
artist = state.manga.artist,
|
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 },
|
isStubSource = remember { state.source is SourceManager.StubSource },
|
||||||
coverDataProvider = { state.manga },
|
coverDataProvider = { state.manga },
|
||||||
status = state.manga.status,
|
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.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.ui.manga.MergedMangaData
|
|
||||||
import eu.kanade.tachiyomi.util.lang.awaitSingle
|
import eu.kanade.tachiyomi.util.lang.awaitSingle
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.Injekt
|
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.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 preferences = Injekt.get<PreferencesHelper>()
|
||||||
val enabledLanguages = preferences.enabledLanguages().get()
|
val enabledLanguages = preferences.enabledLanguages().get()
|
||||||
.filterNot { it in listOf("all", "other") }
|
.filterNot { it in listOf("all", "other") }
|
||||||
@ -104,8 +103,8 @@ fun Source.getNameForMangaInfo(mergeData: MergedMangaData?): String {
|
|||||||
val isInEnabledLanguages = lang in enabledLanguages
|
val isInEnabledLanguages = lang in enabledLanguages
|
||||||
return when {
|
return when {
|
||||||
// SY -->
|
// SY -->
|
||||||
mergeData != null -> getMergedSourcesString(
|
!mergeSources.isNullOrEmpty() -> getMergedSourcesString(
|
||||||
mergeData,
|
mergeSources,
|
||||||
enabledLanguages,
|
enabledLanguages,
|
||||||
hasOneActiveLanguages,
|
hasOneActiveLanguages,
|
||||||
)
|
)
|
||||||
@ -120,12 +119,12 @@ fun Source.getNameForMangaInfo(mergeData: MergedMangaData?): String {
|
|||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
private fun getMergedSourcesString(
|
private fun getMergedSourcesString(
|
||||||
mergeData: MergedMangaData,
|
mergeSources: List<Source>,
|
||||||
enabledLangs: List<String>,
|
enabledLangs: List<String>,
|
||||||
onlyName: Boolean,
|
onlyName: Boolean,
|
||||||
): String {
|
): String {
|
||||||
return if (onlyName) {
|
return if (onlyName) {
|
||||||
mergeData.sources.joinToString { source ->
|
mergeSources.joinToString { source ->
|
||||||
if (source.lang !in enabledLangs) {
|
if (source.lang !in enabledLangs) {
|
||||||
source.toString()
|
source.toString()
|
||||||
} else {
|
} else {
|
||||||
@ -133,7 +132,7 @@ private fun getMergedSourcesString(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
mergeData.sources.joinToString()
|
mergeSources.joinToString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
@ -31,6 +31,13 @@ class PreMigrationController(bundle: Bundle? = null) :
|
|||||||
FlexibleAdapter.OnItemClickListener,
|
FlexibleAdapter.OnItemClickListener,
|
||||||
FabController,
|
FabController,
|
||||||
StartMigrationListener {
|
StartMigrationListener {
|
||||||
|
|
||||||
|
constructor(mangaIds: List<Long>) : this(
|
||||||
|
bundleOf(
|
||||||
|
MANGA_IDS_EXTRA to mangaIds.toLongArray(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
private val sourceManager: SourceManager by injectLazy()
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
private val prefs: PreferencesHelper by injectLazy()
|
private val prefs: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
@ -95,7 +102,7 @@ class PreMigrationController(bundle: Bundle? = null) :
|
|||||||
prefs.migrationSources().set(listOfSources)
|
prefs.migrationSources().set(listOfSources)
|
||||||
|
|
||||||
router.replaceTopController(
|
router.replaceTopController(
|
||||||
MigrationListController.create(
|
MigrationListController(
|
||||||
MigrationProcedureConfig(
|
MigrationProcedureConfig(
|
||||||
config.toList(),
|
config.toList(),
|
||||||
extraSearchParams = extraParam,
|
extraSearchParams = extraParam,
|
||||||
@ -186,21 +193,13 @@ class PreMigrationController(bundle: Bundle? = null) :
|
|||||||
fun navigateToMigration(skipPre: Boolean, router: Router, mangaIds: List<Long>) {
|
fun navigateToMigration(skipPre: Boolean, router: Router, mangaIds: List<Long>) {
|
||||||
router.pushController(
|
router.pushController(
|
||||||
if (skipPre) {
|
if (skipPre) {
|
||||||
MigrationListController.create(
|
MigrationListController(
|
||||||
MigrationProcedureConfig(mangaIds, null),
|
MigrationProcedureConfig(mangaIds, null),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
create(mangaIds)
|
PreMigrationController(mangaIds)
|
||||||
}.withFadeTransaction().tag(if (skipPre) MigrationListController.TAG else null),
|
}.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
|
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.domain.manga.model.Manga
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
|
||||||
import exh.util.DeferredField
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
class MigratingManga(
|
class MigratingManga(
|
||||||
private val getManga: GetManga,
|
val manga: Manga,
|
||||||
private val sourceManager: SourceManager,
|
val chapterInfo: ChapterInfo,
|
||||||
val mangaId: Long,
|
val sourcesString: String,
|
||||||
parentContext: CoroutineContext,
|
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>
|
// <MAX, PROGRESS>
|
||||||
val progress = MutableStateFlow(1 to 0)
|
val progress = MutableStateFlow(1 to 0)
|
||||||
|
|
||||||
val migrationJob = parentContext + SupervisorJob() + Dispatchers.Default
|
sealed class SearchResult {
|
||||||
|
object Searching : SearchResult()
|
||||||
var migrationStatus = MigrationStatus.RUNNING
|
object NotFound : SearchResult()
|
||||||
|
data class Result(val id: Long) : SearchResult()
|
||||||
@Volatile
|
|
||||||
private var manga: Manga? = null
|
|
||||||
suspend fun manga(): Manga? {
|
|
||||||
if (manga == null) manga = getManga.await(mangaId)
|
|
||||||
return manga
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun mangaSource(): Source {
|
data class ChapterInfo(
|
||||||
return sourceManager.getOrStub(manga()?.source ?: -1)
|
val latestChapter: Float?,
|
||||||
}
|
val chapterCount: Int,
|
||||||
|
)
|
||||||
|
|
||||||
fun toModal(): MigrationProcessItem {
|
fun toModal(): MigrationProcessItem {
|
||||||
// Create the model object.
|
// Create the model object.
|
||||||
|
@ -8,57 +8,39 @@ import android.view.Menu
|
|||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.core.graphics.ColorUtils
|
import androidx.core.graphics.ColorUtils
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
|
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import dev.chrisbanes.insetter.applyInsetter
|
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.Manga
|
||||||
import eu.kanade.domain.manga.model.toDbManga
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import eu.kanade.tachiyomi.databinding.MigrationListControllerBinding
|
import eu.kanade.tachiyomi.databinding.MigrationListControllerBinding
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
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.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.pushController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.MigrationMangaDialog
|
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.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.browse.migration.search.SearchController
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
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.getParcelableCompat
|
||||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import exh.eh.EHentaiThrottleManager
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import exh.smartsearch.SmartSearchEngine
|
import kotlinx.coroutines.flow.onEach
|
||||||
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
|
|
||||||
|
|
||||||
class MigrationListController(bundle: Bundle? = null) :
|
class MigrationListController(bundle: Bundle? = null) :
|
||||||
BaseController<MigrationListControllerBinding>(bundle),
|
NucleusController<MigrationListControllerBinding, MigrationListPresenter>(bundle) {
|
||||||
MigrationProcessAdapter.MigrationProcessInterface {
|
|
||||||
|
constructor(config: MigrationProcedureConfig) : this(
|
||||||
|
bundleOf(
|
||||||
|
CONFIG_EXTRA to config,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setHasOptionsMenu(true)
|
setHasOptionsMenu(true)
|
||||||
@ -68,27 +50,19 @@ class MigrationListController(bundle: Bundle? = null) :
|
|||||||
|
|
||||||
val config = args.getParcelableCompat<MigrationProcedureConfig>(CONFIG_EXTRA)
|
val config = args.getParcelableCompat<MigrationProcedureConfig>(CONFIG_EXTRA)
|
||||||
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private var selectedMangaId: Long? = null
|
||||||
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 manualMigrations = 0
|
private var manualMigrations = 0
|
||||||
|
|
||||||
private val throttleManager = EHentaiThrottleManager()
|
|
||||||
|
|
||||||
override fun getTitle(): String {
|
override fun getTitle(): String {
|
||||||
return resources?.getString(R.string.migration) + " (${adapter?.items?.count {
|
val notFinished = presenter.migratingItems.value.count {
|
||||||
it.manga.migrationStatus != MigrationStatus.RUNNING
|
it.searchResult.value != SearchResult.Searching
|
||||||
}}/${adapter?.itemCount ?: 0})"
|
}
|
||||||
|
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)
|
override fun createBinding(inflater: LayoutInflater) = MigrationListControllerBinding.inflate(inflater)
|
||||||
@ -103,15 +77,6 @@ class MigrationListController(bundle: Bundle? = null) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
setTitle()
|
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)
|
adapter = MigrationProcessAdapter(this)
|
||||||
|
|
||||||
@ -119,308 +84,112 @@ class MigrationListController(bundle: Bundle? = null) :
|
|||||||
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
||||||
binding.recycler.setHasFixedSize(true)
|
binding.recycler.setHasFixedSize(true)
|
||||||
|
|
||||||
adapter?.updateDataSet(newMigratingManga.map { it.toModal() })
|
presenter.migratingItems
|
||||||
|
.onEach {
|
||||||
if (migrationsJob == null) {
|
adapter?.updateDataSet(it.map { it.toModal() })
|
||||||
migrationsJob = migrationScope.launch {
|
|
||||||
runMigrations(newMigratingManga)
|
|
||||||
}
|
}
|
||||||
}
|
.launchIn(viewScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun runMigrations(mangas: List<MigratingManga>) {
|
fun updateCount() {
|
||||||
throttleManager.resetThrottle()
|
if (router.backstack.lastOrNull()?.controller == this@MigrationListController) {
|
||||||
if (config == null) return
|
setTitle()
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
|
||||||
migrationScope.cancel()
|
|
||||||
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun enableButtons() {
|
private fun enableButtons() {
|
||||||
activity?.invalidateOptionsMenu()
|
activity?.invalidateOptionsMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun removeManga(item: MigrationProcessItem) {
|
private fun noMigration() {
|
||||||
val ids = config?.mangaIds?.toMutableList() ?: return
|
val res = resources
|
||||||
val index = ids.indexOf(item.manga.mangaId)
|
if (res != null) {
|
||||||
if (index > -1) {
|
activity?.toast(
|
||||||
ids.removeAt(index)
|
res.getQuantityString(
|
||||||
config.mangaIds = ids
|
R.plurals.manga_migrated,
|
||||||
val index2 = migratingManga?.indexOf(item.manga) ?: return
|
manualMigrations,
|
||||||
if (index2 > -1) migratingManga?.removeAt(index2)
|
manualMigrations,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!presenter.hideNotFound) {
|
||||||
|
router.popCurrentController()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun noMigration() {
|
fun onMenuItemClick(mangaId: Long, item: MenuItem) {
|
||||||
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) {
|
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.action_search_manually -> {
|
R.id.action_search_manually -> {
|
||||||
launchUI {
|
val manga = presenter.migratingItems.value
|
||||||
val manga = adapter?.getItem(position)?.manga?.manga() ?: return@launchUI
|
.find { it.manga.id == mangaId }
|
||||||
selectedPosition = position
|
?.manga
|
||||||
val sources = preferences.migrationSources().get().split("/").mapNotNull {
|
?: return
|
||||||
val value = it.toLongOrNull() ?: return@mapNotNull null
|
selectedMangaId = mangaId
|
||||||
sourceManager.get(value) as? CatalogueSource
|
val sources = presenter.getMigrationSources()
|
||||||
}
|
val validSources = if (sources.size == 1) {
|
||||||
val validSources = if (sources.size == 1) {
|
sources
|
||||||
sources
|
} else {
|
||||||
} else {
|
sources.filter { it.id != manga.source }
|
||||||
sources.filter { it.id != manga.source }
|
|
||||||
}
|
|
||||||
val searchController = SearchController(manga, validSources)
|
|
||||||
searchController.targetController = this@MigrationListController
|
|
||||||
router.pushController(searchController)
|
|
||||||
}
|
}
|
||||||
|
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 -> {
|
R.id.action_migrate_now -> {
|
||||||
adapter?.migrateManga(position, false)
|
migrateManga(mangaId, false)
|
||||||
manualMigrations++
|
manualMigrations++
|
||||||
}
|
}
|
||||||
R.id.action_copy_now -> {
|
R.id.action_copy_now -> {
|
||||||
adapter?.migrateManga(position, true)
|
migrateManga(mangaId, true)
|
||||||
manualMigrations++
|
manualMigrations++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun useMangaForMigration(manga: Manga, source: Source) {
|
fun useMangaForMigration(manga: Manga, source: Source) {
|
||||||
val firstIndex = selectedPosition ?: return
|
presenter.useMangaForMigration(manga, source, selectedMangaId ?: 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun migrateMangas() {
|
fun migrateMangas() {
|
||||||
launchUI {
|
presenter.migrateMangas()
|
||||||
adapter?.performMigrations(false)
|
|
||||||
navigateOut()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun copyMangas() {
|
fun copyMangas() {
|
||||||
launchUI {
|
presenter.copyMangas()
|
||||||
adapter?.performMigrations(true)
|
|
||||||
navigateOut()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun navigateOut() {
|
fun migrateManga(mangaId: Long, copy: Boolean) {
|
||||||
if (migratingManga?.size == 1) {
|
presenter.migrateManga(mangaId, copy)
|
||||||
launchUI {
|
}
|
||||||
val hasDetails = router.backstack.any { it.controller is MangaController }
|
|
||||||
if (hasDetails) {
|
fun removeManga(mangaId: Long) {
|
||||||
val manga = migratingManga?.firstOrNull()?.searchResult?.get()?.let {
|
presenter.removeManga(mangaId)
|
||||||
getManga.await(it)
|
}
|
||||||
}
|
|
||||||
if (manga != null) {
|
fun sourceFinished() {
|
||||||
val newStack = router.backstack.filter {
|
updateCount()
|
||||||
it.controller !is MangaController &&
|
if (presenter.migratingItems.value.isEmpty()) noMigration()
|
||||||
it.controller !is MigrationListController &&
|
if (presenter.allMangasDone()) enableButtons()
|
||||||
it.controller !is PreMigrationController
|
}
|
||||||
} + MangaController(manga.id).withFadeTransaction()
|
|
||||||
router.setBackstack(newStack, OneWayFadeChangeHandler())
|
fun navigateOut(manga: Manga?) {
|
||||||
return@launchUI
|
if (manga != null) {
|
||||||
}
|
val newStack = router.backstack.filter {
|
||||||
}
|
it.controller !is MangaController &&
|
||||||
router.popCurrentController()
|
it.controller !is MigrationListController &&
|
||||||
}
|
it.controller !is PreMigrationController
|
||||||
} else router.popCurrentController()
|
} + MangaController(manga.id).withFadeTransaction()
|
||||||
|
router.setBackstack(newStack, OneWayFadeChangeHandler())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
router.popCurrentController()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleBack(): Boolean {
|
override fun handleBack(): Boolean {
|
||||||
@ -429,7 +198,6 @@ class MigrationListController(bundle: Bundle? = null) :
|
|||||||
.setTitle(R.string.stop_migrating)
|
.setTitle(R.string.stop_migrating)
|
||||||
.setPositiveButton(R.string.action_stop) { _, _ ->
|
.setPositiveButton(R.string.action_stop) { _, _ ->
|
||||||
router.popCurrentController()
|
router.popCurrentController()
|
||||||
migrationsJob?.cancel()
|
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
.show()
|
.show()
|
||||||
@ -444,12 +212,12 @@ class MigrationListController(bundle: Bundle? = null) :
|
|||||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||||
// Initialize menu items.
|
// Initialize menu items.
|
||||||
|
|
||||||
val allMangasDone = adapter?.allMangasDone() ?: return
|
val allMangasDone = presenter.allMangasDone()
|
||||||
|
|
||||||
val menuCopy = menu.findItem(R.id.action_copy_manga)
|
val menuCopy = menu.findItem(R.id.action_copy_manga)
|
||||||
val menuMigrate = menu.findItem(R.id.action_migrate_manga)
|
val menuMigrate = menu.findItem(R.id.action_migrate_manga)
|
||||||
|
|
||||||
if (adapter?.itemCount == 1) {
|
if (presenter.migratingItems.value.size == 1) {
|
||||||
menuMigrate.icon = VectorDrawableCompat.create(
|
menuMigrate.icon = VectorDrawableCompat.create(
|
||||||
resources!!,
|
resources!!,
|
||||||
R.drawable.ic_done_24dp,
|
R.drawable.ic_done_24dp,
|
||||||
@ -474,8 +242,8 @@ class MigrationListController(bundle: Bundle? = null) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
val totalManga = adapter?.itemCount ?: 0
|
val totalManga = presenter.migratingItems.value.size
|
||||||
val mangaSkipped = adapter?.mangasSkipped() ?: 0
|
val mangaSkipped = presenter.mangasSkipped()
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.action_copy_manga -> MigrationMangaDialog(
|
R.id.action_copy_manga -> MigrationMangaDialog(
|
||||||
this,
|
this,
|
||||||
@ -493,34 +261,9 @@ class MigrationListController(bundle: Bundle? = null) :
|
|||||||
}
|
}
|
||||||
return true
|
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 {
|
companion object {
|
||||||
const val CONFIG_EXTRA = "config_extra"
|
const val CONFIG_EXTRA = "config_extra"
|
||||||
const val TAG = "migration_list"
|
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
|
package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
|
||||||
|
|
||||||
import android.view.MenuItem
|
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
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(
|
class MigrationProcessAdapter(
|
||||||
val controller: MigrationListController,
|
val controller: MigrationListController,
|
||||||
) : FlexibleAdapter<MigrationProcessItem>(null, controller, true) {
|
) : 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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -6,52 +6,51 @@ import androidx.core.view.isInvisible
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import coil.dispose
|
import coil.dispose
|
||||||
import eu.davidea.viewholders.FlexibleViewHolder
|
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.domain.manga.model.Manga
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.databinding.MigrationMangaCardBinding
|
import eu.kanade.tachiyomi.databinding.MigrationMangaCardBinding
|
||||||
import eu.kanade.tachiyomi.databinding.MigrationProcessItemBinding
|
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.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.ui.manga.MangaController
|
||||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
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.loadAutoPause
|
||||||
import eu.kanade.tachiyomi.util.view.setVectorCompat
|
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.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import reactivecircus.flowbinding.android.view.clicks
|
import reactivecircus.flowbinding.android.view.clicks
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
|
|
||||||
class MigrationProcessHolder(
|
class MigrationProcessHolder(
|
||||||
private val view: View,
|
view: View,
|
||||||
private val adapter: MigrationProcessAdapter,
|
private val adapter: MigrationProcessAdapter,
|
||||||
) : FlexibleViewHolder(view, adapter) {
|
) : 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 var item: MigrationProcessItem? = null
|
||||||
private val binding = MigrationProcessItemBinding.bind(view)
|
private val binding = MigrationProcessItemBinding.bind(view)
|
||||||
|
|
||||||
|
private val jobs = CopyOnWriteArrayList<Job>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// We need to post a Runnable to show the popup to make sure that the PopupMenu is
|
// 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
|
// correctly positioned. The reason being that the view may change position before the
|
||||||
// PopupMenu is shown.
|
// PopupMenu is shown.
|
||||||
binding.migrationMenu.setOnClickListener { it.post { showPopupMenu(it) } }
|
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) {
|
fun bind(item: MigrationProcessItem) {
|
||||||
this.item = item
|
this.item = item
|
||||||
launchUI {
|
jobs.removeAll { it.cancel(); true }
|
||||||
val manga = item.manga.manga()
|
jobs += adapter.controller.viewScope.launchUI {
|
||||||
val source = item.manga.mangaSource()
|
val migrateManga = item.manga
|
||||||
|
val manga = migrateManga.manga
|
||||||
|
|
||||||
binding.migrationMenu.setVectorCompat(
|
binding.migrationMenu.setVectorCompat(
|
||||||
R.drawable.ic_more_24dp,
|
R.drawable.ic_more_24dp,
|
||||||
@ -64,67 +63,74 @@ class MigrationProcessHolder(
|
|||||||
binding.migrationMenu.isInvisible = true
|
binding.migrationMenu.isInvisible = true
|
||||||
binding.skipManga.isVisible = true
|
binding.skipManga.isVisible = true
|
||||||
binding.migrationMangaCardTo.resetManga()
|
binding.migrationMangaCardTo.resetManga()
|
||||||
if (manga != null) {
|
binding.migrationMangaCardFrom.attachManga(manga, item.manga.sourcesString, item.manga.chapterInfo)
|
||||||
binding.migrationMangaCardFrom.attachManga(manga, source)
|
jobs += binding.migrationMangaCardFrom.root.clicks()
|
||||||
binding.migrationMangaCardFrom.root.clicks()
|
.onEach {
|
||||||
.onEach {
|
adapter.controller.router.pushController(
|
||||||
adapter.controller.router.pushController(
|
MangaController(
|
||||||
MangaController(
|
manga.id,
|
||||||
manga.id,
|
true,
|
||||||
true,
|
),
|
||||||
),
|
)
|
||||||
)
|
}
|
||||||
}
|
.launchIn(adapter.controller.viewScope)
|
||||||
.launchIn(adapter.controller.viewScope)
|
|
||||||
|
|
||||||
/*launchUI {
|
/*launchUI {
|
||||||
item.manga.progress.asFlow().collect { (max, progress) ->
|
item.manga.progress.asFlow().collect { (max, progress) ->
|
||||||
withUIContext {
|
withUIContext {
|
||||||
migration_manga_card_to.search_progress.let { progressBar ->
|
migration_manga_card_to.search_progress.let { progressBar ->
|
||||||
progressBar.max = max
|
progressBar.max = max
|
||||||
progressBar.progress = progress
|
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,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
.launchIn(adapter.controller.viewScope)
|
||||||
}
|
|
||||||
}*/
|
|
||||||
|
|
||||||
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)
|
|
||||||
} else {
|
} else {
|
||||||
binding.migrationMangaCardTo.progress.isVisible = false
|
binding.migrationMangaCardTo.progress.isVisible = false
|
||||||
binding.migrationMangaCardTo.title.text = view.context.applicationContext
|
binding.migrationMangaCardTo.title.text = itemView.context
|
||||||
.getString(R.string.no_alternatives_found)
|
.getString(R.string.no_alternatives_found)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.migrationMenu.isVisible = true
|
||||||
|
binding.skipManga.isVisible = false
|
||||||
|
adapter.controller.sourceFinished()
|
||||||
}
|
}
|
||||||
binding.migrationMenu.isVisible = true
|
.catch {
|
||||||
binding.skipManga.isVisible = false
|
this@MigrationProcessHolder.logcat(throwable = it) { "Error updating result info" }
|
||||||
adapter.sourceFinished()
|
}
|
||||||
}
|
.launchIn(adapter.controller.viewScope)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,39 +145,35 @@ class MigrationProcessHolder(
|
|||||||
mangaLastChapterLabel.text = ""
|
mangaLastChapterLabel.text = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun MigrationMangaCardBinding.attachManga(manga: Manga, source: Source) {
|
private fun MigrationMangaCardBinding.attachManga(
|
||||||
|
manga: Manga,
|
||||||
|
sourceString: String,
|
||||||
|
chapterInfo: ChapterInfo,
|
||||||
|
) {
|
||||||
progress.isVisible = false
|
progress.isVisible = false
|
||||||
thumbnail.loadAutoPause(manga)
|
thumbnail.loadAutoPause(manga)
|
||||||
|
|
||||||
title.text = if (manga.title.isBlank()) {
|
title.text = if (manga.title.isBlank()) {
|
||||||
view.context.getString(R.string.unknown)
|
itemView.context.getString(R.string.unknown)
|
||||||
} else {
|
} else {
|
||||||
manga.ogTitle
|
manga.ogTitle
|
||||||
}
|
}
|
||||||
|
|
||||||
mangaSourceLabel.text = if (source.id == MERGED_SOURCE_ID) {
|
mangaSourceLabel.text = sourceString
|
||||||
getMergedReferencesById.await(manga.id).map {
|
|
||||||
sourceManager.getOrStub(it.mangaSourceId).toString()
|
|
||||||
}.distinct().joinToString()
|
|
||||||
} else {
|
|
||||||
source.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
val chapters = getChapterByMangaId.await(manga.id)
|
|
||||||
// For rounded corners
|
// For rounded corners
|
||||||
badges.leftBadges.clipToOutline = true
|
badges.leftBadges.clipToOutline = true
|
||||||
badges.rightBadges.clipToOutline = true
|
badges.rightBadges.clipToOutline = true
|
||||||
badges.unreadText.isVisible = true
|
badges.unreadText.isVisible = true
|
||||||
badges.unreadText.text = chapters.size.toString()
|
badges.unreadText.text = chapterInfo.chapterCount.toString()
|
||||||
val latestChapter = chapters.maxOfOrNull { it.chapterNumber } ?: -1f
|
|
||||||
|
|
||||||
if (latestChapter > 0f) {
|
if (chapterInfo.latestChapter != null && chapterInfo.latestChapter > 0f) {
|
||||||
mangaLastChapterLabel.text = root.context.getString(
|
mangaLastChapterLabel.text = itemView.context.getString(
|
||||||
R.string.latest_,
|
R.string.latest_,
|
||||||
DecimalFormat("#.#").format(latestChapter),
|
DecimalFormat("#.#").format(chapterInfo.latestChapter),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
mangaLastChapterLabel.text = root.context.getString(
|
mangaLastChapterLabel.text = itemView.context.getString(
|
||||||
R.string.latest_,
|
R.string.latest_,
|
||||||
root.context.getString(R.string.unknown),
|
root.context.getString(R.string.unknown),
|
||||||
)
|
)
|
||||||
@ -191,14 +193,14 @@ class MigrationProcessHolder(
|
|||||||
|
|
||||||
popup.menu.findItem(R.id.action_search_manually).isVisible = true
|
popup.menu.findItem(R.id.action_search_manually).isVisible = true
|
||||||
// Hide download and show delete if the chapter is downloaded
|
// 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_migrate_now).isVisible = true
|
||||||
popup.menu.findItem(R.id.action_copy_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
|
// Set a listener so we are notified if a menu item is clicked
|
||||||
popup.setOnMenuItemClickListener { menuItem ->
|
popup.setOnMenuItemClickListener { menuItem ->
|
||||||
adapter.menuItemListener.onMenuItemClick(bindingAdapterPosition, menuItem)
|
adapter.controller.onMenuItemClick(item.manga.manga.id, menuItem)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,12 +30,12 @@ class MigrationProcessItem(val manga: MigratingManga) :
|
|||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (other is MigrationProcessItem) {
|
if (other is MigrationProcessItem) {
|
||||||
return manga.mangaId == other.manga.mangaId
|
return manga.manga.id == other.manga.manga.id
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
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 sourceManager = Injekt.get<SourceManager>()
|
||||||
val source = sourceManager.get(manga.source) ?: return@SourceSearchScreen
|
val source = sourceManager.get(manga.source) ?: return@SourceSearchScreen
|
||||||
migrationListController.useMangaForMigration(manga, source)
|
migrationListController.useMangaForMigration(manga, source)
|
||||||
router.popCurrentController()
|
router.popToTag(MigrationListController.TAG)
|
||||||
router.popCurrentController()
|
|
||||||
},
|
},
|
||||||
// SY <--
|
// SY <--
|
||||||
onWebViewClick = f@{
|
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