Finished Part 1 of new auto source migration
(cherry picked from commit 10206ae7b30fbd521308a6d725e107e708b97dd0)
							
								
								
									
										11
									
								
								app/src/debug/res/drawable-anydpi/ic_copy.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,11 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:width="24dp" | ||||
|     android:height="24dp" | ||||
|     android:viewportWidth="24" | ||||
|     android:viewportHeight="24" | ||||
|     android:tint="#FFFFFF" | ||||
|     android:alpha="0.8"> | ||||
|     <path | ||||
|         android:fillColor="#FF000000" | ||||
|         android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z"/> | ||||
| </vector> | ||||
							
								
								
									
										11
									
								
								app/src/debug/res/drawable-anydpi/ic_done.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,11 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:width="24dp" | ||||
|     android:height="24dp" | ||||
|     android:viewportWidth="24" | ||||
|     android:viewportHeight="24" | ||||
|     android:tint="#FFFFFF" | ||||
|     android:alpha="0.8"> | ||||
|     <path | ||||
|         android:fillColor="#FF000000" | ||||
|         android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z"/> | ||||
| </vector> | ||||
							
								
								
									
										11
									
								
								app/src/debug/res/drawable-anydpi/ic_done_all.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,11 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:width="24dp" | ||||
|     android:height="24dp" | ||||
|     android:viewportWidth="24" | ||||
|     android:viewportHeight="24" | ||||
|     android:tint="#FFFFFF" | ||||
|     android:alpha="0.8"> | ||||
|     <path | ||||
|         android:fillColor="#FF000000" | ||||
|         android:pathData="M18,7l-1.41,-1.41 -6.34,6.34 1.41,1.41L18,7zM22.24,5.59L11.66,16.17 7.48,12l-1.41,1.41L11.66,19l12,-12 -1.42,-1.41zM0.41,13.41L6,19l1.41,-1.41L1.83,12 0.41,13.41z"/> | ||||
| </vector> | ||||
							
								
								
									
										
											BIN
										
									
								
								app/src/debug/res/drawable-hdpi/ic_copy.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 220 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/debug/res/drawable-hdpi/ic_done.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 197 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/debug/res/drawable-hdpi/ic_done_all.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 289 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/debug/res/drawable-mdpi/ic_copy.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 146 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/debug/res/drawable-mdpi/ic_done.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 151 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/debug/res/drawable-mdpi/ic_done_all.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 213 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/debug/res/drawable-xhdpi/ic_copy.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 209 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/debug/res/drawable-xhdpi/ic_done.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 219 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/debug/res/drawable-xhdpi/ic_done_all.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 323 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/debug/res/drawable-xxhdpi/ic_copy.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 303 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/debug/res/drawable-xxhdpi/ic_done.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 279 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/debug/res/drawable-xxhdpi/ic_done_all.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 416 B | 
							
								
								
									
										6
									
								
								app/src/debug/res/drawable/ic_migrate_direction.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | ||||
| <vector android:height="100dp" | ||||
|     android:viewportHeight="24.0" android:viewportWidth="24.0" | ||||
|     android:width="100dp" xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:tint="?attr/colorControlNormal"> | ||||
|     <path android:fillColor="@android:color/white" android:pathData="M7,10l5,5 -5,5z"/> | ||||
| </vector> | ||||
| @@ -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<T>(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() | ||||
|     } | ||||
| } | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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<MigratingManga>? = 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<MigratingManga>) { | ||||
|         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) | ||||
|             }) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -5,11 +5,10 @@ import kotlinx.android.parcel.Parcelize | ||||
|  | ||||
| @Parcelize | ||||
| data class MigrationProcedureConfig( | ||||
|     val mangaIds: List<Long>, | ||||
|     var mangaIds: List<Long>, | ||||
|     val targetSourceIds: List<Long>, | ||||
|     val useSourceWithMostChapters: Boolean, | ||||
|     val enableLenientSearch: Boolean, | ||||
|     val migrationFlags: Int, | ||||
|     val copy: Boolean, | ||||
|     val extraSearchParams: String? | ||||
| ) : Parcelable | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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<MigrationProcessItem>(null, controller, true) { | ||||
|  | ||||
|     private val db: DatabaseHelper by injectLazy() | ||||
|     var items: List<MigrationProcessItem> = emptyList() | ||||
|  | ||||
|     val menuItemListener: MigrationProcessInterface = controller | ||||
|  | ||||
|     override fun updateDataSet(items: List<MigrationProcessItem>?) { | ||||
|         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() | ||||
|         // } | ||||
|     } | ||||
| } | ||||
| @@ -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() | ||||
|     } | ||||
| } | ||||
| @@ -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<MigrationProcessHolder>() { | ||||
|  | ||||
|     var holder: MigrationProcessHolder? = null | ||||
|     override fun getLayoutRes(): Int { | ||||
|         return R.layout.migration_new_process_item | ||||
|     } | ||||
|  | ||||
|     override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): MigrationProcessHolder { | ||||
|         return MigrationProcessHolder(view, adapter as MigrationProcessAdapter) | ||||
|     } | ||||
|  | ||||
|     override fun bindViewHolder( | ||||
|         adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, | ||||
|         holder: MigrationProcessHolder, | ||||
|         position: Int, | ||||
|         payloads: MutableList<Any?>? | ||||
|     ) { | ||||
|  | ||||
|         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() | ||||
|     } | ||||
| } | ||||
| @@ -11,7 +11,7 @@ import kotlinx.coroutines.sync.withLock | ||||
| class DeferredField<T> { | ||||
|  | ||||
|     @Volatile | ||||
|     private var content: T? = null | ||||
|     var content: T? = null | ||||
|  | ||||
|     @Volatile | ||||
|     var initialized = false | ||||
| @@ -31,6 +31,14 @@ class DeferredField<T> { | ||||
|         mutex.unlock() | ||||
|     } | ||||
|  | ||||
|     fun set(content: T) { | ||||
|         mutex.tryLock() | ||||
|         this.content = content | ||||
|         initialized = true | ||||
|         // Notify current listeners | ||||
|         mutex.unlock() | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Will only suspend if !initialized. | ||||
|      */ | ||||
|   | ||||
| @@ -0,0 +1,10 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|         android:width="24dp" | ||||
|         android:height="24dp" | ||||
|         android:viewportWidth="24.0" | ||||
|         android:viewportHeight="24.0" | ||||
|         android:tint="?attr/colorControlNormal"> | ||||
|     <path | ||||
|         android:fillColor="@android:color/white" | ||||
|         android:pathData="M8.59,16.34l4.58,-4.59 -4.58,-4.59L10,5.75l6,6 -6,6z"/> | ||||
| </vector> | ||||
| @@ -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" /> | ||||
|  | ||||
|     <androidx.appcompat.widget.SwitchCompat | ||||
|         android:id="@+id/copy_manga" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         app:layout_constraintStart_toStartOf="@+id/textView" | ||||
|         app:layout_constraintTop_toTopOf="@+id/copy_manga_desc" /> | ||||
|  | ||||
|     <TextView | ||||
|         android:id="@+id/copy_manga_desc" | ||||
|         android:layout_width="0dp" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_marginStart="8dp" | ||||
|         android:layout_marginLeft="8dp" | ||||
|         android:layout_marginEnd="8dp" | ||||
|         android:layout_marginRight="8dp" | ||||
|         android:layout_marginBottom="8dp" | ||||
|         android:gravity="start|center_vertical" | ||||
|         android:text="@string/keep_old_manga" | ||||
|         android:clickable="true" | ||||
|         app:layout_constraintBottom_toTopOf="@+id/extra_search_param" | ||||
|         app:layout_constraintEnd_toEndOf="parent" | ||||
|         app:layout_constraintStart_toEndOf="@+id/prioritize_chapter_count" | ||||
| @@ -202,6 +178,6 @@ | ||||
|         android:id="@+id/options_group" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         app:constraint_referenced_ids="migration_mode,use_smart_search,fuzzy_search,copy_manga,extra_search_param_desc,mig_tracking,textView,mig_chapters,copy_manga_desc,textView2,prioritize_chapter_count,mig_categories,extra_search_param" /> | ||||
|         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" /> | ||||
|  | ||||
| </androidx.constraintlayout.widget.ConstraintLayout> | ||||
							
								
								
									
										16
									
								
								app/src/main/res/layout/migration_list_controller.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,16 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent" | ||||
|     android:animateLayoutChanges="true"> | ||||
|  | ||||
|     <androidx.recyclerview.widget.RecyclerView | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:clipToPadding="false" | ||||
|         android:id="@+id/recycler" | ||||
|         tools:listitem="@layout/migration_new_process_item" /> | ||||
|  | ||||
| </FrameLayout> | ||||
							
								
								
									
										123
									
								
								app/src/main/res/layout/migration_new_manga_card.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,123 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     android:layout_width="wrap_content" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:background="?selectable_library_drawable"> | ||||
|  | ||||
|     <FrameLayout | ||||
|         android:id="@+id/card" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="220dp" | ||||
|         android:background="?attr/colorSurface" | ||||
|         app:layout_constraintDimensionRatio="0.75" | ||||
|         app:layout_constraintEnd_toEndOf="parent" | ||||
|         app:layout_constraintStart_toStartOf="parent" | ||||
|         app:layout_constraintTop_toTopOf="parent" | ||||
|         app:layout_constraintWidth_min="100dp" | ||||
|         app:layout_constraintHeight_min="100dp"> | ||||
|  | ||||
|         <ImageView | ||||
|             android:id="@+id/thumbnail" | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="match_parent" | ||||
|             android:background="?android:attr/colorBackground" | ||||
|             tools:background="?android:attr/colorBackground" | ||||
|             tools:ignore="ContentDescription" | ||||
|             tools:src="@mipmap/ic_launcher" /> | ||||
|  | ||||
|         <View | ||||
|             android:id="@+id/gradient" | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:layout_gravity="bottom" | ||||
|             android:background="@drawable/gradient_shape" /> | ||||
|  | ||||
|         <ProgressBar | ||||
|             android:id="@+id/loading_group" | ||||
|             android:layout_width="56dp" | ||||
|             android:layout_height="56dp" | ||||
|             android:layout_gravity="center" /> | ||||
|  | ||||
|         <TextView | ||||
|             android:id="@+id/manga_chapters" | ||||
|             style="@style/TextAppearance.Regular.Caption.Light" | ||||
|             android:layout_width="wrap_content" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:background="@color/md_teal_500" | ||||
|             android:paddingBottom="1dp" | ||||
|             android:paddingLeft="3dp" | ||||
|             android:paddingRight="3dp" | ||||
|             android:paddingTop="1dp" | ||||
|             android:visibility="gone" | ||||
|             tools:visibility="visible" | ||||
|             android:text="101" | ||||
|             android:layout_marginStart="4dp" | ||||
|             app:layout_constraintLeft_toLeftOf="parent" | ||||
|             app:layout_constraintTop_toTopOf="parent" | ||||
|             android:layout_marginTop="4dp"/> | ||||
|  | ||||
|         <com.google.android.material.textview.MaterialTextView | ||||
|             android:id="@+id/title" | ||||
|             style="@style/TextAppearance.Regular.Body1.Light" | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:layout_gravity="bottom" | ||||
|             android:ellipsize="end" | ||||
|             android:lineSpacingExtra="-4dp" | ||||
|             android:maxLines="2" | ||||
|             android:padding="8dp" | ||||
|             android:shadowColor="@color/textColorPrimaryLight" | ||||
|             android:shadowDx="0" | ||||
|             android:shadowDy="0" | ||||
|             android:shadowRadius="4" | ||||
|             tools:text="Sample name" /> | ||||
|  | ||||
|         <ProgressBar | ||||
|             android:id="@+id/progress" | ||||
|             style="?android:attr/progressBarStyleSmall" | ||||
|             android:layout_width="wrap_content" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:layout_gravity="center" | ||||
|             android:visibility="gone" /> | ||||
|  | ||||
|     </FrameLayout> | ||||
|  | ||||
|     <LinearLayout | ||||
|         android:id="@+id/card_scroll_content" | ||||
|         android:layout_width="0dp" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_marginStart="8dp" | ||||
|         android:paddingBottom="20dp" | ||||
|         android:gravity="start" | ||||
|         android:orientation="vertical" | ||||
|         app:layout_constraintBottom_toBottomOf="parent" | ||||
|         app:layout_constraintEnd_toEndOf="@id/card" | ||||
|         app:layout_constraintStart_toStartOf="@id/card" | ||||
|         app:layout_constraintTop_toBottomOf="@id/card"> | ||||
|  | ||||
|  | ||||
|         <TextView | ||||
|             android:id="@+id/manga_source_label" | ||||
|             style="@style/TextAppearance.Medium.Body2" | ||||
|             android:layout_width="wrap_content" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:clickable="false" | ||||
|             android:textIsSelectable="false" | ||||
|             app:layout_constraintLeft_toLeftOf="parent" | ||||
|             android:ellipsize="end" | ||||
|             android:maxLines="1" | ||||
|             tools:layout_editor_absoluteY="57dp" /> | ||||
|  | ||||
|         <TextView | ||||
|             android:id="@+id/manga_last_chapter_label" | ||||
|             style="@style/TextAppearance.Regular.Body1.Secondary" | ||||
|             android:layout_width="wrap_content" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:clickable="false" | ||||
|             android:textIsSelectable="false" /> | ||||
|  | ||||
|     </LinearLayout> | ||||
|  | ||||
| </androidx.constraintlayout.widget.ConstraintLayout> | ||||
							
								
								
									
										79
									
								
								app/src/main/res/layout/migration_new_process_item.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,79 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:gravity="center|start"> | ||||
|  | ||||
|     <include | ||||
|         android:id="@+id/migration_manga_card_from" | ||||
|         layout="@layout/migration_new_manga_card" | ||||
|         android:layout_width="150dp" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_marginStart="16dp" | ||||
|         android:layout_marginLeft="16dp" | ||||
|         android:layout_marginEnd="16dp" | ||||
|         android:layout_marginRight="16dp" | ||||
|         app:layout_constraintEnd_toEndOf="parent" | ||||
|         app:layout_constraintStart_toStartOf="parent" | ||||
|         app:layout_constraintTop_toTopOf="parent" | ||||
|         app:layout_constraintWidth_max="450dp" /> | ||||
|  | ||||
|     <ImageView | ||||
|         android:id="@+id/imageView" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="25dp" | ||||
|         android:layout_marginStart="-10dp" | ||||
|         android:layout_marginEnd="-10dp" | ||||
|         android:layout_marginBottom="30dp" | ||||
|         android:adjustViewBounds="true" | ||||
|         android:contentDescription="migrating to" | ||||
|         android:scaleType="center" | ||||
|         app:layout_constraintEnd_toEndOf="parent" | ||||
|         app:layout_constraintStart_toStartOf="parent" | ||||
|         app:layout_constraintTop_toBottomOf="@+id/migration_manga_card_from" | ||||
|         app:srcCompat="@drawable/ic_keyboard_arrow_right_black_24dp" /> | ||||
|  | ||||
|     <include | ||||
|         android:id="@+id/migration_manga_card_to" | ||||
|         layout="@layout/migration_new_manga_card" | ||||
|         android:layout_width="150dp" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_marginStart="16dp" | ||||
|         android:layout_marginLeft="16dp" | ||||
|         android:layout_marginEnd="16dp" | ||||
|         android:layout_marginRight="16dp" | ||||
|         app:layout_constraintEnd_toEndOf="parent" | ||||
|         app:layout_constraintStart_toStartOf="parent" | ||||
|         app:layout_constraintTop_toBottomOf="@+id/imageView" | ||||
|         app:layout_constraintWidth_max="450dp" /> | ||||
|  | ||||
|     <ImageView | ||||
|         android:id="@+id/migration_menu" | ||||
|         android:layout_width="48dp" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_marginEnd="8dp" | ||||
|         android:layout_marginBottom="30dp" | ||||
|         android:contentDescription="@string/description_cover" | ||||
|         android:paddingTop="30dp" | ||||
|         android:paddingBottom="30dp" | ||||
|         app:layout_constraintRight_toRightOf="parent" | ||||
|         app:layout_constraintTop_toTopOf="parent" | ||||
|         app:srcCompat="@drawable/ic_more_vert_24dp" /> | ||||
|  | ||||
|  | ||||
|     <ImageView | ||||
|         android:id="@+id/skip_manga" | ||||
|         android:layout_width="48dp" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_marginEnd="8dp" | ||||
|         android:layout_marginBottom="30dp" | ||||
|         android:contentDescription="@string/description_cover" | ||||
|         android:paddingTop="30dp" | ||||
|         android:paddingBottom="30dp" | ||||
|         app:layout_constraintRight_toRightOf="parent" | ||||
|         app:layout_constraintTop_toTopOf="parent" | ||||
|         app:srcCompat="@drawable/ic_close_24dp" | ||||
|         android:visibility="gone"/> | ||||
| </LinearLayout> | ||||
							
								
								
									
										15
									
								
								app/src/main/res/menu/migration_list.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,15 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <menu xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|  | ||||
|     <item | ||||
|         android:id="@+id/action_copy_manga" | ||||
|         android:icon="@drawable/ic_copy" | ||||
|         android:title="@string/copy" | ||||
|         app:showAsAction="always" /> | ||||
|     <item | ||||
|         android:id="@+id/action_migrate_manga" | ||||
|         android:icon="@drawable/ic_done_all" | ||||
|         android:title="@string/migrate" | ||||
|         app:showAsAction="always" /> | ||||
| </menu> | ||||
							
								
								
									
										21
									
								
								app/src/main/res/menu/migration_single.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,21 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
|  | ||||
| <menu xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|  | ||||
|     <item android:id="@+id/action_search_manually" | ||||
|         android:title="@string/action_search_manually" | ||||
|         android:visible="false" /> | ||||
|  | ||||
|     <item | ||||
|         android:id="@+id/action_skip" | ||||
|         android:title="@string/action_skip_manga" | ||||
|         android:visible="true"/> | ||||
|  | ||||
|     <item android:id="@+id/action_migrate_now" | ||||
|         android:title="@string/action_migrate_now" | ||||
|         android:visible="false" /> | ||||
|  | ||||
|     <item android:id="@+id/action_copy_now" | ||||
|         android:title="@string/action_copy_now" | ||||
|         android:visible="false" /> | ||||
| </menu> | ||||
| @@ -49,6 +49,7 @@ | ||||
|     <string name="action_sort_drag_and_drop">Drag & Drop</string> | ||||
|     <string name="action_search">Search</string> | ||||
|     <string name="action_global_search">Global search</string> | ||||
|     <string name="action_skip_manga">Skip manga</string> | ||||
|     <string name="action_select_all">Select all</string> | ||||
|     <string name="action_select_inverse">Select inverse</string> | ||||
|     <string name="action_mark_as_read">Mark as read</string> | ||||
| @@ -112,6 +113,9 @@ | ||||
|     <string name="action_webview_back">Back</string> | ||||
|     <string name="action_webview_forward">Forward</string> | ||||
|     <string name="action_webview_refresh">Refresh</string> | ||||
|     <string name="action_search_manually">Search manually</string> | ||||
|     <string name="action_migrate_now">Migrate now</string> | ||||
|     <string name="action_copy_now">Copy now</string> | ||||
|  | ||||
|     <!-- Operations --> | ||||
|     <string name="loading">Loading…</string> | ||||
| @@ -482,6 +486,10 @@ | ||||
|     <string name="download_unread">Unread</string> | ||||
|     <string name="confirm_delete_chapters">Are you sure you want to delete the selected chapters?</string> | ||||
|     <string name="invalid_download_dir">Invalid download location</string> | ||||
|     <string name="confirm_migration">Migrate %1$d%2$s mangas?</string> | ||||
|     <string name="confirm_copy">Copy %1$d%2$s mangas?</string> | ||||
|     <string name="skipping_x">(skipping %1$d)</string> | ||||
|     <string name="no_migrations">No manga migrated</string> | ||||
|  | ||||
|     <!-- Tracking Screen --> | ||||
|     <string name="manga_tracking_tab">Tracking</string> | ||||
| @@ -557,6 +565,8 @@ | ||||
|     <string name="select">Select</string> | ||||
|     <string name="migrate">Migrate</string> | ||||
|     <string name="copy">Copy</string> | ||||
|     <string name="migrating">Migrating…</string> | ||||
|     <string name="latest_x">Latest: %1$s</string> | ||||
|  | ||||
|     <!-- Downloads activity and service --> | ||||
|     <string name="download_queue_error">Could not download chapters. You can try again in the downloads section</string> | ||||
|   | ||||