diff --git a/app/src/debug/res/drawable-anydpi/ic_copy.xml b/app/src/debug/res/drawable-anydpi/ic_copy.xml
new file mode 100644
index 000000000..b11c7d4ef
--- /dev/null
+++ b/app/src/debug/res/drawable-anydpi/ic_copy.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/app/src/debug/res/drawable-anydpi/ic_done.xml b/app/src/debug/res/drawable-anydpi/ic_done.xml
new file mode 100644
index 000000000..28bf05369
--- /dev/null
+++ b/app/src/debug/res/drawable-anydpi/ic_done.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/app/src/debug/res/drawable-anydpi/ic_done_all.xml b/app/src/debug/res/drawable-anydpi/ic_done_all.xml
new file mode 100644
index 000000000..85d9ffd19
--- /dev/null
+++ b/app/src/debug/res/drawable-anydpi/ic_done_all.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/app/src/debug/res/drawable-hdpi/ic_copy.png b/app/src/debug/res/drawable-hdpi/ic_copy.png
new file mode 100644
index 000000000..7821875d3
Binary files /dev/null and b/app/src/debug/res/drawable-hdpi/ic_copy.png differ
diff --git a/app/src/debug/res/drawable-hdpi/ic_done.png b/app/src/debug/res/drawable-hdpi/ic_done.png
new file mode 100644
index 000000000..f1f39724e
Binary files /dev/null and b/app/src/debug/res/drawable-hdpi/ic_done.png differ
diff --git a/app/src/debug/res/drawable-hdpi/ic_done_all.png b/app/src/debug/res/drawable-hdpi/ic_done_all.png
new file mode 100644
index 000000000..3d0e27aa9
Binary files /dev/null and b/app/src/debug/res/drawable-hdpi/ic_done_all.png differ
diff --git a/app/src/debug/res/drawable-mdpi/ic_copy.png b/app/src/debug/res/drawable-mdpi/ic_copy.png
new file mode 100644
index 000000000..912309132
Binary files /dev/null and b/app/src/debug/res/drawable-mdpi/ic_copy.png differ
diff --git a/app/src/debug/res/drawable-mdpi/ic_done.png b/app/src/debug/res/drawable-mdpi/ic_done.png
new file mode 100644
index 000000000..4d1d9bdfd
Binary files /dev/null and b/app/src/debug/res/drawable-mdpi/ic_done.png differ
diff --git a/app/src/debug/res/drawable-mdpi/ic_done_all.png b/app/src/debug/res/drawable-mdpi/ic_done_all.png
new file mode 100644
index 000000000..c0e88e9fe
Binary files /dev/null and b/app/src/debug/res/drawable-mdpi/ic_done_all.png differ
diff --git a/app/src/debug/res/drawable-xhdpi/ic_copy.png b/app/src/debug/res/drawable-xhdpi/ic_copy.png
new file mode 100644
index 000000000..a15eb3da5
Binary files /dev/null and b/app/src/debug/res/drawable-xhdpi/ic_copy.png differ
diff --git a/app/src/debug/res/drawable-xhdpi/ic_done.png b/app/src/debug/res/drawable-xhdpi/ic_done.png
new file mode 100644
index 000000000..f193eba59
Binary files /dev/null and b/app/src/debug/res/drawable-xhdpi/ic_done.png differ
diff --git a/app/src/debug/res/drawable-xhdpi/ic_done_all.png b/app/src/debug/res/drawable-xhdpi/ic_done_all.png
new file mode 100644
index 000000000..d6036992c
Binary files /dev/null and b/app/src/debug/res/drawable-xhdpi/ic_done_all.png differ
diff --git a/app/src/debug/res/drawable-xxhdpi/ic_copy.png b/app/src/debug/res/drawable-xxhdpi/ic_copy.png
new file mode 100644
index 000000000..bfc0c2588
Binary files /dev/null and b/app/src/debug/res/drawable-xxhdpi/ic_copy.png differ
diff --git a/app/src/debug/res/drawable-xxhdpi/ic_done.png b/app/src/debug/res/drawable-xxhdpi/ic_done.png
new file mode 100644
index 000000000..612c0a3fd
Binary files /dev/null and b/app/src/debug/res/drawable-xxhdpi/ic_done.png differ
diff --git a/app/src/debug/res/drawable-xxhdpi/ic_done_all.png b/app/src/debug/res/drawable-xxhdpi/ic_done_all.png
new file mode 100644
index 000000000..da71795bd
Binary files /dev/null and b/app/src/debug/res/drawable-xxhdpi/ic_done_all.png differ
diff --git a/app/src/debug/res/drawable/ic_migrate_direction.xml b/app/src/debug/res/drawable/ic_migrate_direction.xml
new file mode 100644
index 000000000..90e15b6b9
--- /dev/null
+++ b/app/src/debug/res/drawable/ic_migrate_direction.xml
@@ -0,0 +1,6 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationMangaDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationMangaDialog.kt
new file mode 100644
index 000000000..c9c2057b4
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationMangaDialog.kt
@@ -0,0 +1,41 @@
+package eu.kanade.tachiyomi.ui.migration
+
+import android.app.Dialog
+import android.os.Bundle
+import com.afollestad.materialdialogs.MaterialDialog
+import com.bluelinelabs.conductor.Controller
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.ui.base.controller.DialogController
+import eu.kanade.tachiyomi.ui.migration.manga.process.MigrationListController
+
+class MigrationMangaDialog(bundle: Bundle? = null) : DialogController(bundle)
+ where T : Controller {
+
+ var copy = false
+ var mangaSet = 0
+ var mangaSkipped = 0
+ constructor(target: T, copy: Boolean, mangaSet: Int, mangaSkipped: Int) : this() {
+ targetController = target
+ this.copy = copy
+ this.mangaSet = mangaSet
+ this.mangaSkipped = mangaSkipped
+ }
+
+ override fun onCreateDialog(savedViewState: Bundle?): Dialog {
+ val confirmRes = if (copy) R.string.confirm_copy else R.string.confirm_migration
+ val confirmString = applicationContext?.getString(confirmRes, mangaSet, (
+ if (mangaSkipped > 0)
+ " " + applicationContext?.getString(R.string.skipping_x, mangaSkipped) ?: ""
+ else "")) ?: ""
+ return MaterialDialog.Builder(activity!!)
+ .content(confirmString)
+ .positiveText(android.R.string.yes)
+ .negativeText(android.R.string.no)
+ .onPositive { _, _ ->
+ if (copy)
+ (targetController as? MigrationListController)?.copyMangas()
+ else
+ (targetController as? MigrationListController)?.migrateMangas()
+ }.show()
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/design/MigrationDesignController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/design/MigrationDesignController.kt
index 52a0a4017..89be89b55 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/design/MigrationDesignController.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/design/MigrationDesignController.kt
@@ -14,13 +14,11 @@ import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.controller.BaseController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.migration.MigrationFlags
+import eu.kanade.tachiyomi.ui.migration.manga.process.MigrationListController
import eu.kanade.tachiyomi.ui.migration.manga.process.MigrationProcedureConfig
-import eu.kanade.tachiyomi.ui.migration.manga.process.MigrationProcedureController
import eu.kanade.tachiyomi.util.view.gone
import eu.kanade.tachiyomi.util.view.visible
import kotlinx.android.synthetic.main.migration_design_controller.begin_migration_btn
-import kotlinx.android.synthetic.main.migration_design_controller.copy_manga
-import kotlinx.android.synthetic.main.migration_design_controller.copy_manga_desc
import kotlinx.android.synthetic.main.migration_design_controller.extra_search_param
import kotlinx.android.synthetic.main.migration_design_controller.extra_search_param_desc
import kotlinx.android.synthetic.main.migration_design_controller.extra_search_param_text
@@ -73,10 +71,6 @@ class MigrationDesignController(bundle: Bundle? = null) : BaseController(bundle)
use_smart_search.toggle()
}
- copy_manga_desc.setOnClickListener {
- copy_manga.toggle()
- }
-
extra_search_param_desc.setOnClickListener {
extra_search_param.toggle()
}
@@ -106,7 +100,7 @@ class MigrationDesignController(bundle: Bundle? = null) : BaseController(bundle)
if (mig_categories.isChecked) flags = flags or MigrationFlags.TRACK
router.replaceTopController(
- MigrationProcedureController.create(
+ MigrationListController.create(
MigrationProcedureConfig(
config.toList(),
ourAdapter.items.filter {
@@ -115,7 +109,6 @@ class MigrationDesignController(bundle: Bundle? = null) : BaseController(bundle)
useSourceWithMostChapters = prioritize_chapter_count.isChecked,
enableLenientSearch = use_smart_search.isChecked,
migrationFlags = flags,
- copy = copy_manga.isChecked,
extraSearchParams = if (extra_search_param.isChecked && extra_search_param_text.text.isNotBlank()) {
extra_search_param_text.text.toString()
} else null
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigratingManga.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigratingManga.kt
index 5303ccc09..37e0c432b 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigratingManga.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigratingManga.kt
@@ -33,4 +33,9 @@ class MigratingManga(
suspend fun mangaSource(): Source {
return sourceManager.getOrStub(manga()?.source ?: -1)
}
+
+ fun toModal(): MigrationProcessItem {
+ // Create the model object.
+ return MigrationProcessItem(this)
+ }
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationListController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationListController.kt
new file mode 100644
index 000000000..796874d51
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationListController.kt
@@ -0,0 +1,386 @@
+package eu.kanade.tachiyomi.ui.migration.manga.process
+
+import android.content.pm.ActivityInfo
+import android.graphics.Color
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.graphics.ColorUtils
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
+import com.afollestad.materialdialogs.MaterialDialog
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.DatabaseHelper
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.smartsearch.SmartSearchEngine
+import eu.kanade.tachiyomi.source.CatalogueSource
+import eu.kanade.tachiyomi.source.Source
+import eu.kanade.tachiyomi.source.SourceManager
+import eu.kanade.tachiyomi.ui.base.controller.BaseController
+import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
+import eu.kanade.tachiyomi.ui.migration.MigrationMangaDialog
+import eu.kanade.tachiyomi.ui.migration.SearchController
+import eu.kanade.tachiyomi.util.await
+import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
+import eu.kanade.tachiyomi.util.lang.launchUI
+import eu.kanade.tachiyomi.util.system.toast
+import eu.kanade.tachiyomi.util.view.RecyclerWindowInsetsListener
+import java.util.concurrent.atomic.AtomicInteger
+import kotlin.coroutines.CoroutineContext
+import kotlinx.android.synthetic.main.chapters_controller.*
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.async
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Semaphore
+import kotlinx.coroutines.sync.withPermit
+import kotlinx.coroutines.withContext
+import rx.schedulers.Schedulers
+import uy.kohesive.injekt.injectLazy
+
+class MigrationListController(bundle: Bundle? = null) : BaseController(bundle),
+ MigrationProcessAdapter.MigrationProcessInterface,
+ CoroutineScope {
+
+ init {
+ setHasOptionsMenu(true)
+ }
+
+ private var titleText = "Migrate manga"
+
+ private var adapter: MigrationProcessAdapter? = null
+
+ override val coroutineContext: CoroutineContext = Job() + Dispatchers.Default
+
+ val config: MigrationProcedureConfig? = args.getParcelable(CONFIG_EXTRA)
+
+ private val db: DatabaseHelper by injectLazy()
+ private val sourceManager: SourceManager by injectLazy()
+
+ private val smartSearchEngine = SmartSearchEngine(coroutineContext, config?.extraSearchParams)
+
+ private var migrationsJob: Job? = null
+ private var migratingManga: MutableList? = null
+ private var selectedPosition: Int? = null
+
+ override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
+ return inflater.inflate(R.layout.migration_list_controller, container, false)
+ }
+
+ override fun getTitle(): String {
+ return titleText
+ }
+
+ override fun onViewCreated(view: View) {
+ super.onViewCreated(view)
+ setTitle()
+ val config = this.config ?: return
+ activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
+
+ val newMigratingManga = migratingManga ?: run {
+ val new = config.mangaIds.map {
+ MigratingManga(db, sourceManager, it, coroutineContext)
+ }
+ migratingManga = new.toMutableList()
+ new
+ }
+
+ adapter = MigrationProcessAdapter(this, view.context)
+
+ recycler.adapter = adapter
+ recycler.layoutManager = LinearLayoutManager(view.context)
+ // recycler.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration
+ // .VERTICAL))
+ recycler.setHasFixedSize(true)
+ recycler.setOnApplyWindowInsetsListener(RecyclerWindowInsetsListener)
+ // recycler.isEnabled = false
+
+ adapter?.updateDataSet(newMigratingManga.map { it.toModal() })
+
+ if (migrationsJob == null) {
+ migrationsJob = launch {
+ runMigrations(newMigratingManga)
+ }
+ }
+ }
+
+ /*fun nextMigration() {
+ adapter?.let { adapter ->
+ if(pager.currentItem >= adapter.count - 1) {
+ applicationContext?.toast("All migrations complete!")
+ router.popCurrentController()
+ } else {
+ adapter.migratingManga[pager.currentItem].migrationJob.cancel()
+ pager.setCurrentItem(pager.currentItem + 1, true)
+ launch(Dispatchers.Main) {
+ updateTitle()
+ }
+ }
+ }
+ }*/
+
+ fun migrationFailure() {
+ activity?.let {
+ MaterialDialog.Builder(it)
+ .title("Migration failure")
+ .content("An unknown error occured while migrating this manga!")
+ .positiveText("Ok")
+ .show()
+ }
+ }
+
+ suspend fun runMigrations(mangas: List) {
+ val sources = config?.targetSourceIds?.mapNotNull { sourceManager.get(it) as? CatalogueSource } ?: return
+
+ for (manga in mangas) {
+ 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 = sources.filter {
+ it.id != mangaSource.id
+ }
+ if (config.useSourceWithMostChapters) {
+ val sourceSemaphore = Semaphore(3)
+ val processedSources = AtomicInteger()
+
+ validSources.map { source ->
+ async {
+ sourceSemaphore.withPermit {
+ try {
+ val searchResult = if (config.enableLenientSearch) {
+ smartSearchEngine.smartSearch(source, mangaObj.title)
+ } else {
+ smartSearchEngine.normalSearch(source, mangaObj.title)
+ }
+
+ if (searchResult != null) {
+ val localManga = smartSearchEngine.networkToLocalManga(searchResult, source.id)
+ val chapters = source.fetchChapterList(localManga).toSingle().await(
+ Schedulers.io())
+ withContext(Dispatchers.IO) {
+ syncChaptersWithSource(db, chapters, localManga, source)
+ }
+ manga.progress.send(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() }.maxBy { it.second }?.first
+ } else {
+ validSources.forEachIndexed { index, source ->
+ val searchResult = try {
+ val searchResult = if (config.enableLenientSearch) {
+ smartSearchEngine.smartSearch(source, mangaObj.title)
+ } else {
+ smartSearchEngine.normalSearch(source, mangaObj.title)
+ }
+
+ if (searchResult != null) {
+ val localManga = smartSearchEngine.networkToLocalManga(searchResult, source.id)
+ val chapters = source.fetchChapterList(localManga).toSingle().await(
+ Schedulers.io())
+ withContext(Dispatchers.IO) {
+ syncChaptersWithSource(db, chapters, localManga, source)
+ }
+ localManga
+ } else null
+ } catch (e: CancellationException) {
+ // Ignore cancellations
+ throw e
+ } catch (e: Exception) {
+ null
+ }
+
+ manga.progress.send(validSources.size to (index + 1))
+
+ if (searchResult != null) return@async searchResult
+ }
+
+ null
+ }
+ }.await()
+ } catch (e: CancellationException) {
+ // Ignore canceled migrations
+ continue
+ }
+
+ if (result != null && result.thumbnail_url == null) {
+ try {
+ val newManga = sourceManager.getOrStub(result.source)
+ .fetchMangaDetails(result)
+ .toSingle()
+ .await()
+ result.copyFrom(newManga)
+
+ db.insertManga(result).executeAsBlocking()
+ } catch (e: CancellationException) {
+ // Ignore cancellations
+ throw e
+ } catch (e: Exception) {
+ }
+ }
+
+ manga.searchResult.initialize(result?.id)
+ }
+ }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+
+ activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
+ }
+
+ override fun enableButtons() {
+ activity?.invalidateOptionsMenu()
+ }
+
+ override fun removeManga(position: Int) {
+ val ids = config?.mangaIds?.toMutableList() ?: return
+ ids.removeAt(position)
+ migratingManga?.removeAt(position)
+ config.mangaIds = ids
+ }
+
+ override fun noMigration() {
+ activity?.toast(R.string.no_migrations)
+ router.popCurrentController()
+ }
+
+ override fun onMenuItemClick(position: Int, item: MenuItem) {
+
+ when (item.itemId) {
+ R.id.action_search_manually -> {
+ launchUI {
+ val manga = adapter?.getItem(position) ?: return@launchUI
+ selectedPosition = position
+ val searchController = SearchController(manga.manga.manga())
+ searchController.targetController = this@MigrationListController
+ router.pushController(searchController.withFadeTransaction())
+ }
+ }
+ R.id.action_skip -> adapter?.removeManga(position)
+ R.id.action_migrate_now -> adapter?.migrateManga(position, false)
+ R.id.action_copy_now -> adapter?.migrateManga(position, true)
+ }
+ }
+
+ fun useMangaForMigration(manga: Manga, source: Source) {
+ val firstIndex = selectedPosition ?: return
+ val migratingManga = adapter?.getItem(firstIndex) ?: return
+ migratingManga.showSpinner()
+ launchUI {
+ val result = CoroutineScope(migratingManga.manga.migrationJob).async {
+ val localManga = smartSearchEngine.networkToLocalManga(manga, source.id)
+ val chapters = source.fetchChapterList(localManga).toSingle().await(
+ Schedulers.io()
+ )
+ withContext(Dispatchers.IO) {
+ syncChaptersWithSource(db, chapters, localManga, source)
+ }
+ localManga
+ }.await()
+
+ try {
+ val newManga =
+ sourceManager.getOrStub(result.source).fetchMangaDetails(result).toSingle()
+ .await()
+ result.copyFrom(newManga)
+
+ db.insertManga(result).executeAsBlocking()
+ } catch (e: CancellationException) {
+ // Ignore cancellations
+ throw e
+ } catch (e: Exception) {
+ }
+
+ migratingManga.manga.searchResult.set(result.id)
+ adapter?.notifyDataSetChanged()
+ }
+ }
+
+ fun migrateMangas() {
+ launchUI {
+ adapter?.performMigrations(false)
+ router.popCurrentController()
+ }
+ }
+
+ fun copyMangas() {
+ launchUI {
+ adapter?.performMigrations(true)
+ router.popCurrentController()
+ }
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ inflater.inflate(R.menu.migration_list, menu)
+ }
+
+ override fun onPrepareOptionsMenu(menu: Menu) {
+ // Initialize menu items.
+
+ val allMangasDone = adapter?.allMangasDone() ?: return
+
+ val menuCopy = menu.findItem(R.id.action_copy_manga)
+ val menuMigrate = menu.findItem(R.id.action_migrate_manga)
+
+ if (adapter?.itemCount == 1) {
+ menuMigrate.icon = VectorDrawableCompat.create(
+ resources!!, R.drawable.ic_done, null
+ )
+ }
+ val translucentWhite = ColorUtils.setAlphaComponent(Color.WHITE, 127)
+ menuCopy.icon?.setTint(if (allMangasDone) Color.WHITE else translucentWhite)
+ menuMigrate?.icon?.setTint(if (allMangasDone) Color.WHITE else translucentWhite)
+ menuCopy.isEnabled = allMangasDone
+ menuMigrate.isEnabled = allMangasDone
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ val itemsCount = adapter?.itemCount ?: 0
+ val mangasSkipped = adapter?.mangasSkipped() ?: 0
+ when (item.itemId) {
+ R.id.action_copy_manga -> MigrationMangaDialog(this, true, itemsCount, mangasSkipped)
+ .showDialog(router)
+ R.id.action_migrate_manga -> MigrationMangaDialog(this, false, itemsCount, mangasSkipped)
+ .showDialog(router)
+ else -> return super.onOptionsItemSelected(item)
+ }
+ return true
+ }
+
+ companion object {
+ const val CONFIG_EXTRA = "config_extra"
+
+ fun create(config: MigrationProcedureConfig): MigrationListController {
+ return MigrationListController(Bundle().apply {
+ putParcelable(CONFIG_EXTRA, config)
+ })
+ }
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcedureAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcedureAdapter.kt
index a3f395522..0a4b4781c 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcedureAdapter.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcedureAdapter.kt
@@ -74,7 +74,7 @@ class MigrationProcedureAdapter(
container.addView(view)
view.skip_migration.setOnClickListener {
- controller.nextMigration()
+ // controller.nextMigration()
}
val viewTag = ViewTag(coroutineContext)
@@ -100,19 +100,19 @@ class MigrationProcedureAdapter(
}
suspend fun performMigration(manga: MigratingManga) {
- if (!manga.searchResult.initialized) {
- return
- }
+ if (!manga.searchResult.initialized) {
+ return
+ }
- val toMangaObj = db.getManga(manga.searchResult.get() ?: return).executeAsBlocking() ?: return
+ val toMangaObj = db.getManga(manga.searchResult.get() ?: return).executeAsBlocking() ?: return
- withContext(Dispatchers.IO) {
- migrateMangaInternal(
+ withContext(Dispatchers.IO) {
+ migrateMangaInternal(
manga.manga() ?: return@withContext,
toMangaObj,
- !(controller.config?.copy ?: false)
- )
- }
+ false
+ )
+ }
}
private fun migrateMangaInternal(
@@ -121,7 +121,7 @@ class MigrationProcedureAdapter(
replace: Boolean
) {
val config = controller.config ?: return
- db.inTransaction {
+ // db.inTransaction {
// Update chapters read
if (MigrationFlags.hasChapters(controller.config.migrationFlags)) {
val prevMangaChapters = db.getChapters(prevManga).executeAsBlocking()
@@ -162,7 +162,7 @@ class MigrationProcedureAdapter(
// SearchPresenter#networkToLocalManga may have updated the manga title, so ensure db gets updated title
db.updateMangaTitle(manga).executeAsBlocking()
- }
+ // }
}
fun View.setupView(tag: ViewTag, migratingManga: MigratingManga) {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcedureConfig.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcedureConfig.kt
index 69279858f..d3bf84a89 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcedureConfig.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcedureConfig.kt
@@ -5,11 +5,10 @@ import kotlinx.android.parcel.Parcelize
@Parcelize
data class MigrationProcedureConfig(
- val mangaIds: List,
+ var mangaIds: List,
val targetSourceIds: List,
val useSourceWithMostChapters: Boolean,
val enableLenientSearch: Boolean,
val migrationFlags: Int,
- val copy: Boolean,
val extraSearchParams: String?
) : Parcelable
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcedureController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcedureController.kt
index 776b57376..f31797935 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcedureController.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcedureController.kt
@@ -148,8 +148,7 @@ class MigrationProcedureController(bundle: Bundle? = null) : BaseController(bund
async {
sourceSemaphore.withPermit {
try {
- val searchResult = if (config?.enableLenientSearch ==
- true) {
+ val searchResult = if (config.enableLenientSearch) {
smartSearchEngine.smartSearch(source, mangaObj.title)
} else {
smartSearchEngine.normalSearch(source, mangaObj.title)
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcessAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcessAdapter.kt
new file mode 100644
index 000000000..8abf3d959
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcessAdapter.kt
@@ -0,0 +1,145 @@
+package eu.kanade.tachiyomi.ui.migration.manga.process
+
+import android.content.Context
+import android.view.MenuItem
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.kanade.tachiyomi.data.database.DatabaseHelper
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.database.models.MangaCategory
+import eu.kanade.tachiyomi.ui.migration.MigrationFlags
+import eu.kanade.tachiyomi.util.lang.launchUI
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.withContext
+import uy.kohesive.injekt.injectLazy
+
+class MigrationProcessAdapter(
+ val controller: MigrationListController,
+ context: Context
+) : FlexibleAdapter(null, controller, true) {
+
+ private val db: DatabaseHelper by injectLazy()
+ var items: List = emptyList()
+
+ val menuItemListener: MigrationProcessInterface = controller
+
+ override fun updateDataSet(items: List?) {
+ this.items = items ?: emptyList()
+ super.updateDataSet(items)
+ }
+
+ fun indexOf(item: MigrationProcessItem): Int {
+ return items.indexOf(item)
+ }
+
+ interface MigrationProcessInterface {
+ fun onMenuItemClick(position: Int, item: MenuItem)
+ fun enableButtons()
+ fun removeManga(position: Int)
+ fun noMigration()
+ }
+
+ fun sourceFinished() {
+ if (mangasSkipped() == itemCount || itemCount == 0) menuItemListener.noMigration()
+ if (allMangasDone()) menuItemListener.enableButtons()
+ }
+
+ fun allMangasDone() = (items.all { it.manga.searchResult.initialized || !it.manga.migrationJob
+ .isActive } && items.any { it.manga
+ .searchResult.content != null })
+
+ fun mangasSkipped() = (items.count { (!it.manga.searchResult.initialized || it.manga
+ .searchResult.content == null) && !it.manga.migrationJob.isActive })
+
+ suspend fun performMigrations(copy: Boolean) {
+ withContext(Dispatchers.IO) {
+ db.inTransaction {
+ currentItems.forEach { migratingManga ->
+ val manga = migratingManga.manga
+ if (manga.searchResult.initialized) {
+ val toMangaObj =
+ db.getManga(manga.searchResult.get() ?: return@forEach).executeAsBlocking()
+ ?: return@forEach
+ migrateMangaInternal(
+ manga.manga() ?: return@forEach,
+ toMangaObj,
+ !copy)
+ }
+ }
+ }
+ }
+ }
+
+ fun migrateManga(position: Int, copy: Boolean) {
+ launchUI {
+ val manga = getItem(position)?.manga ?: return@launchUI
+ db.inTransaction {
+ val toMangaObj = db.getManga(manga.searchResult.get() ?: return@launchUI).executeAsBlocking()
+ ?: return@launchUI
+ migrateMangaInternal(
+ manga.manga() ?: return@launchUI, toMangaObj, !copy
+ )
+ }
+ removeManga(position)
+ }
+ }
+
+ fun removeManga(position: Int) {
+ menuItemListener.removeManga(position)
+ getItem(position)?.manga?.migrationJob?.cancel()
+ removeItem(position)
+ items = currentItems
+ sourceFinished()
+ }
+
+ private fun migrateMangaInternal(
+ prevManga: Manga,
+ manga: Manga,
+ replace: Boolean
+ ) {
+ if (controller.config == null) return
+ // db.inTransaction {
+ // Update chapters read
+ if (MigrationFlags.hasChapters(controller.config.migrationFlags)) {
+ val prevMangaChapters = db.getChapters(prevManga).executeAsBlocking()
+ val maxChapterRead = prevMangaChapters.filter { it.read }
+ .maxBy { it.chapter_number }?.chapter_number
+ if (maxChapterRead != null) {
+ val dbChapters = db.getChapters(manga).executeAsBlocking()
+ for (chapter in dbChapters) {
+ if (chapter.isRecognizedNumber && chapter.chapter_number <= maxChapterRead) {
+ chapter.read = true
+ }
+ }
+ db.insertChapters(dbChapters).executeAsBlocking()
+ }
+ }
+ // Update categories
+ if (MigrationFlags.hasCategories(controller.config.migrationFlags)) {
+ val categories = db.getCategoriesForManga(prevManga).executeAsBlocking()
+ val mangaCategories = categories.map { MangaCategory.create(manga, it) }
+ db.setMangaCategories(mangaCategories, listOf(manga))
+ }
+ // Update track
+ if (MigrationFlags.hasTracks(controller.config.migrationFlags)) {
+ val tracks = db.getTracks(prevManga).executeAsBlocking()
+ for (track in tracks) {
+ track.id = null
+ track.manga_id = manga.id!!
+ }
+ db.insertTracks(tracks).executeAsBlocking()
+ }
+ // Update favorite status
+ if (replace) {
+ prevManga.favorite = false
+ db.updateMangaFavorite(prevManga).executeAsBlocking()
+ }
+ manga.favorite = true
+ db.updateMangaFavorite(manga).executeAsBlocking()
+
+ // SearchPresenter#networkToLocalManga may have updated the manga title, so ensure db gets updated title
+ db.updateMangaTitle(manga).executeAsBlocking()
+ // }
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcessHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcessHolder.kt
new file mode 100644
index 000000000..b678d5467
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcessHolder.kt
@@ -0,0 +1,173 @@
+package eu.kanade.tachiyomi.ui.migration.manga.process
+
+import android.view.View
+import android.widget.PopupMenu
+import com.bumptech.glide.load.engine.DiskCacheStrategy
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.DatabaseHelper
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.glide.GlideApp
+import eu.kanade.tachiyomi.source.Source
+import eu.kanade.tachiyomi.source.SourceManager
+import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
+import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
+import eu.kanade.tachiyomi.ui.manga.MangaController
+import eu.kanade.tachiyomi.util.lang.launchUI
+import eu.kanade.tachiyomi.util.system.getResourceColor
+import eu.kanade.tachiyomi.util.view.gone
+import eu.kanade.tachiyomi.util.view.setVectorCompat
+import eu.kanade.tachiyomi.util.view.visible
+import java.text.DecimalFormat
+import kotlinx.android.synthetic.main.migration_new_manga_card.view.*
+import kotlinx.android.synthetic.main.migration_new_process_item.*
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import uy.kohesive.injekt.injectLazy
+
+class MigrationProcessHolder(
+ private val view: View,
+ private val adapter: MigrationProcessAdapter
+) : BaseFlexibleViewHolder(view, adapter) {
+
+ private val db: DatabaseHelper by injectLazy()
+ private val sourceManager: SourceManager by injectLazy()
+
+ 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.
+ migration_menu.setOnClickListener { it.post { showPopupMenu(it) } }
+ skip_manga.setOnClickListener { it.post { adapter.removeManga(adapterPosition) } }
+ }
+
+ fun bind(item: MigrationProcessItem) {
+ launchUI {
+ val manga = item.manga.manga()
+ val source = item.manga.mangaSource()
+
+ migration_menu.setVectorCompat(R.drawable.ic_more_vert_24dp, view.context.getResourceColor(R.attr.colorOnPrimary))
+ skip_manga.setVectorCompat(R.drawable.ic_close_24dp, view.context.getResourceColor(R
+ .attr.colorOnPrimary))
+ migration_menu.gone()
+ if (manga != null) {
+ withContext(Dispatchers.Main) {
+ migration_manga_card_from.loading_group.gone()
+ attachManga(migration_manga_card_from, manga, source)
+ migration_manga_card_from.setOnClickListener {
+ adapter.controller.router.pushController(
+ MangaController(
+ manga,
+ true
+ ).withFadeTransaction()
+ )
+ }
+ }
+
+ /*launchUI {
+ item.manga.progress.asFlow().collect { (max, progress) ->
+ withContext(Dispatchers.Main) {
+ migration_manga_card_to.search_progress.let { progressBar ->
+ progressBar.max = max
+ progressBar.progress = progress
+ }
+ }
+ }
+ }*/
+
+ val searchResult = item.manga.searchResult.get()?.let {
+ db.getManga(it).executeAsBlocking()
+ }
+ val resultSource = searchResult?.source?.let {
+ sourceManager.get(it)
+ }
+ withContext(Dispatchers.Main) {
+ if (searchResult != null && resultSource != null) {
+ migration_manga_card_to.loading_group.gone()
+ attachManga(migration_manga_card_to, searchResult, resultSource)
+ migration_manga_card_to.setOnClickListener {
+ adapter.controller.router.pushController(
+ MangaController(
+ searchResult, true
+ ).withFadeTransaction()
+ )
+ }
+ } else {
+ migration_manga_card_to.loading_group.gone()
+ migration_manga_card_to.title.text = "No Alternatives Found"
+ }
+ migration_menu.visible()
+ skip_manga.gone()
+ adapter.sourceFinished()
+ }
+ }
+ }
+ }
+
+ fun showSpinner() {
+ migration_manga_card_to.loading_group.visible()
+ }
+
+ fun attachManga(view: View, manga: Manga, source: Source) {
+ view.loading_group.gone()
+ GlideApp.with(view.context.applicationContext)
+ .load(manga)
+ .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
+ .centerCrop()
+ .into(view.thumbnail)
+
+ view.title.text = if (manga.title.isBlank()) {
+ view.context.getString(R.string.unknown)
+ } else {
+ manga.title
+ }
+
+ view.gradient.visible()
+ view.manga_source_label.text = /*if (source.id == MERGED_SOURCE_ID) {
+ MergedSource.MangaConfig.readFromUrl(gson, manga.url).children.map {
+ sourceManager.getOrStub(it.source).toString()
+ }.distinct().joinToString()
+ } else {*/
+ source.toString()
+ // }
+
+ val mangaChapters = db.getChapters(manga).executeAsBlocking()
+ view.manga_chapters.visible()
+ view.manga_chapters.text = mangaChapters.size.toString()
+ val latestChapter = mangaChapters.maxBy { it.chapter_number }?.chapter_number ?: -1f
+
+ if (latestChapter > 0f) {
+ view.manga_last_chapter_label.text = view.context.getString(R.string.latest_x,
+ DecimalFormat("#.#").format(latestChapter))
+ } else {
+ view.manga_last_chapter_label.setText(R.string.unknown)
+ }
+ }
+
+ private fun showPopupMenu(view: View) {
+ val item = adapter.getItem(adapterPosition) ?: return
+
+ // Create a PopupMenu, giving it the clicked view for an anchor
+ val popup = PopupMenu(view.context, view)
+
+ // Inflate our menu resource into the PopupMenu's Menu
+ popup.menuInflater.inflate(R.menu.migration_single, popup.menu)
+
+ val mangas = item.manga
+
+ 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) {
+ 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(adapterPosition, menuItem)
+ true
+ }
+
+ // Finally show the PopupMenu
+ popup.show()
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcessItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcessItem.kt
new file mode 100644
index 000000000..2f08a1aff
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcessItem.kt
@@ -0,0 +1,48 @@
+package eu.kanade.tachiyomi.ui.migration.manga.process
+
+import android.view.View
+import androidx.recyclerview.widget.RecyclerView
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
+import eu.davidea.flexibleadapter.items.IFlexible
+import eu.kanade.tachiyomi.R
+
+class MigrationProcessItem(val manga: MigratingManga) :
+ AbstractFlexibleItem() {
+
+ var holder: MigrationProcessHolder? = null
+ override fun getLayoutRes(): Int {
+ return R.layout.migration_new_process_item
+ }
+
+ override fun createViewHolder(view: View, adapter: FlexibleAdapter>): MigrationProcessHolder {
+ return MigrationProcessHolder(view, adapter as MigrationProcessAdapter)
+ }
+
+ override fun bindViewHolder(
+ adapter: FlexibleAdapter>,
+ holder: MigrationProcessHolder,
+ position: Int,
+ payloads: MutableList?
+ ) {
+
+ this.holder = holder
+ holder.bind(this)
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other is MigrationProcessItem) {
+ return manga.mangaId == other.manga.mangaId
+ }
+ return false
+ }
+
+ fun showSpinner() {
+ holder?.showSpinner()
+ }
+
+ override fun hashCode(): Int {
+ return manga.mangaId.hashCode()
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/DeferredField.kt b/app/src/main/java/eu/kanade/tachiyomi/util/DeferredField.kt
index 90282c9da..2f5895188 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/util/DeferredField.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/util/DeferredField.kt
@@ -11,7 +11,7 @@ import kotlinx.coroutines.sync.withLock
class DeferredField {
@Volatile
- private var content: T? = null
+ var content: T? = null
@Volatile
var initialized = false
@@ -31,6 +31,14 @@ class DeferredField {
mutex.unlock()
}
+ fun set(content: T) {
+ mutex.tryLock()
+ this.content = content
+ initialized = true
+ // Notify current listeners
+ mutex.unlock()
+ }
+
/**
* Will only suspend if !initialized.
*/
diff --git a/app/src/main/res/drawable/ic_keyboard_arrow_right_black_24dp.xml b/app/src/main/res/drawable/ic_keyboard_arrow_right_black_24dp.xml
new file mode 100644
index 000000000..58d1a85f9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_keyboard_arrow_right_black_24dp.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/layout/migration_design_controller.xml b/app/src/main/res/layout/migration_design_controller.xml
index c295339ba..5469f761d 100644
--- a/app/src/main/res/layout/migration_design_controller.xml
+++ b/app/src/main/res/layout/migration_design_controller.xml
@@ -114,30 +114,6 @@
android:gravity="start|center_vertical"
android:text="@string/use_intelligent_search"
android:clickable="true"
- app:layout_constraintBottom_toTopOf="@+id/copy_manga"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toEndOf="@+id/prioritize_chapter_count"
- android:focusable="true" />
-
-
-
-
+ app:constraint_referenced_ids="migration_mode,use_smart_search,fuzzy_search,action_copy_manga,extra_search_param_desc,mig_tracking,textView,mig_chapters,copy_manga_desc,textView2,prioritize_chapter_count,mig_categories,extra_search_param" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/migration_list_controller.xml b/app/src/main/res/layout/migration_list_controller.xml
new file mode 100644
index 000000000..7abb2eba7
--- /dev/null
+++ b/app/src/main/res/layout/migration_list_controller.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/migration_new_manga_card.xml b/app/src/main/res/layout/migration_new_manga_card.xml
new file mode 100644
index 000000000..5e7746779
--- /dev/null
+++ b/app/src/main/res/layout/migration_new_manga_card.xml
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/migration_new_process_item.xml b/app/src/main/res/layout/migration_new_process_item.xml
new file mode 100644
index 000000000..388891486
--- /dev/null
+++ b/app/src/main/res/layout/migration_new_process_item.xml
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/migration_list.xml b/app/src/main/res/menu/migration_list.xml
new file mode 100644
index 000000000..b19ff0b7b
--- /dev/null
+++ b/app/src/main/res/menu/migration_list.xml
@@ -0,0 +1,15 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/migration_single.xml b/app/src/main/res/menu/migration_single.xml
new file mode 100644
index 000000000..af3ab4772
--- /dev/null
+++ b/app/src/main/res/menu/migration_single.xml
@@ -0,0 +1,21 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index fde13bdb9..7111a164b 100755
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -49,6 +49,7 @@
Drag & Drop
Search
Global search
+ Skip manga
Select all
Select inverse
Mark as read
@@ -112,6 +113,9 @@
Back
Forward
Refresh
+ Search manually
+ Migrate now
+ Copy now
Loading…
@@ -482,6 +486,10 @@
Unread
Are you sure you want to delete the selected chapters?
Invalid download location
+ Migrate %1$d%2$s mangas?
+ Copy %1$d%2$s mangas?
+ (skipping %1$d)
+ No manga migrated
Tracking
@@ -557,6 +565,8 @@
Select
Migrate
Copy
+ Migrating…
+ Latest: %1$s
Could not download chapters. You can try again in the downloads section