Finished Part 1 of new auto source migration
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()
|
||||
}
|
||||
}
|
@ -5,16 +5,19 @@ import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
|
||||
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchPresenter
|
||||
import eu.kanade.tachiyomi.ui.migration.manga.process.MigrationListController
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class SearchController(
|
||||
@ -25,6 +28,14 @@ class SearchController(
|
||||
private var progress = 1
|
||||
var totalProgress = 0
|
||||
|
||||
/**
|
||||
* Called when controller is initialized.
|
||||
*/
|
||||
init {
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
|
||||
override fun getTitle(): String? {
|
||||
if (totalProgress > 1) {
|
||||
return "($progress/$totalProgress) ${super.getTitle()}"
|
||||
@ -49,7 +60,7 @@ class SearchController(
|
||||
newManga = savedInstanceState.getSerializable(::newManga.name) as? Manga
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
/*override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
if (totalProgress > 1) {
|
||||
val menuItem = menu.add(Menu.NONE, 1, Menu.NONE, R.string.action_skip_manga)
|
||||
menuItem.icon = VectorDrawableCompat.create(resources!!, R.drawable
|
||||
@ -66,7 +77,7 @@ class SearchController(
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}*/
|
||||
|
||||
fun migrateManga() {
|
||||
val target = targetController as? MigrationInterface ?: return
|
||||
@ -98,6 +109,14 @@ class SearchController(
|
||||
}
|
||||
|
||||
override fun onMangaClick(manga: Manga) {
|
||||
if (targetController is MigrationListController) {
|
||||
val migrationListController = targetController as? MigrationListController
|
||||
val sourceManager: SourceManager by injectLazy()
|
||||
val source = sourceManager.get(manga.source) ?: return
|
||||
migrationListController?.useMangaForMigration(manga, source)
|
||||
router.popCurrentController()
|
||||
return
|
||||
}
|
||||
newManga = manga
|
||||
val dialog = MigrationDialog()
|
||||
dialog.targetController = this
|
||||
@ -142,4 +161,40 @@ class SearchController(
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds items to the options menu.
|
||||
*
|
||||
* @param menu menu containing options.
|
||||
* @param inflater used to load the menu xml.
|
||||
*/
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
// Inflate menu.
|
||||
inflater.inflate(R.menu.catalogue_new_list, menu)
|
||||
|
||||
// Initialize search menu
|
||||
val searchItem = menu.findItem(R.id.action_search)
|
||||
val searchView = searchItem.actionView as SearchView
|
||||
|
||||
searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
|
||||
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
|
||||
searchView.onActionViewExpanded() // Required to show the query in the view
|
||||
searchView.setQuery(presenter.query, false)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
searchView.queryTextChangeEvents()
|
||||
.filter { it.isSubmitted }
|
||||
.subscribeUntilDestroy {
|
||||
presenter.search(it.queryText().toString())
|
||||
searchItem.collapseActionView()
|
||||
setTitle() // Update toolbar title
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -14,7 +14,7 @@ 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.MigrationProcedureController
|
||||
import eu.kanade.tachiyomi.ui.migration.manga.process.MigrationListController
|
||||
import eu.kanade.tachiyomi.util.gone
|
||||
import eu.kanade.tachiyomi.util.visible
|
||||
import exh.ui.migration.manga.process.MigrationProcedureConfig
|
||||
@ -60,10 +60,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()
|
||||
}
|
||||
@ -93,7 +89,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 {
|
||||
@ -102,7 +98,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
|
||||
|
@ -4,6 +4,7 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.ui.migration.manga.process.MigrationProcessItem
|
||||
import eu.kanade.tachiyomi.util.DeferredField
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@ -31,4 +32,9 @@ class MigratingManga(private val db: DatabaseHelper,
|
||||
suspend fun mangaSource(): Source {
|
||||
return sourceManager.getOrStub(manga()?.source ?: -1)
|
||||
}
|
||||
|
||||
fun toModal(): MigrationProcessItem {
|
||||
// Create the model object.
|
||||
return MigrationProcessItem(this)
|
||||
}
|
||||
}
|
@ -0,0 +1,388 @@
|
||||
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.RecyclerWindowInsetsListener
|
||||
import eu.kanade.tachiyomi.util.await
|
||||
import eu.kanade.tachiyomi.util.launchUI
|
||||
import eu.kanade.tachiyomi.util.syncChaptersWithSource
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import exh.ui.migration.manga.process.MigratingManga
|
||||
import exh.ui.migration.manga.process.MigrationProcedureConfig
|
||||
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
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@ -4,7 +4,6 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.viewpager.widget.PagerAdapter
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.google.gson.Gson
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
@ -40,7 +39,6 @@ class MigrationProcedureAdapter(val controller: MigrationProcedureController,
|
||||
val migratingManga: List<MigratingManga>,
|
||||
override val coroutineContext: CoroutineContext) : PagerAdapter(), CoroutineScope {
|
||||
private val db: DatabaseHelper by injectLazy()
|
||||
private val gson: Gson by injectLazy()
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
|
||||
override fun isViewFromObject(p0: View, p1: Any): Boolean {
|
||||
@ -55,7 +53,7 @@ class MigrationProcedureAdapter(val controller: MigrationProcedureController,
|
||||
container.addView(view)
|
||||
|
||||
view.skip_migration.setOnClickListener {
|
||||
controller.nextMigration()
|
||||
//controller.nextMigration()
|
||||
}
|
||||
|
||||
val viewTag = ViewTag(coroutineContext)
|
||||
@ -81,26 +79,26 @@ class MigrationProcedureAdapter(val controller: MigrationProcedureController,
|
||||
}
|
||||
|
||||
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(prevManga: Manga,
|
||||
manga: Manga,
|
||||
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()
|
||||
@ -141,7 +139,7 @@ class MigrationProcedureAdapter(val controller: MigrationProcedureController,
|
||||
|
||||
// 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
|
@ -150,8 +150,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,144 @@
|
||||
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.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.getResourceColor
|
||||
import eu.kanade.tachiyomi.util.gone
|
||||
import eu.kanade.tachiyomi.util.launchUI
|
||||
import eu.kanade.tachiyomi.util.setVectorCompat
|
||||
import eu.kanade.tachiyomi.util.visible
|
||||
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
|
||||
import java.text.DecimalFormat
|
||||
|
||||
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_black_24dp, view.context.getResourceColor(R.attr.icon_color))
|
||||
skip_manga.setVectorCompat(R.drawable.baseline_close_24, view.context.getResourceColor(R
|
||||
.attr.icon_color))
|
||||
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
|
||||
import exh.ui.migration.manga.process.MigratingManga
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
@ -12,7 +12,7 @@ import kotlinx.coroutines.sync.withLock
|
||||
class DeferredField<T> {
|
||||
|
||||
@Volatile
|
||||
private var content: T? = null
|
||||
var content: T? = null
|
||||
|
||||
@Volatile
|
||||
var initialized = false
|
||||
@ -32,6 +32,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>
|
124
app/src/main/res/layout/migration_new_manga_card.xml
Normal file
@ -0,0 +1,124 @@
|
||||
<?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="@drawable/card_background"
|
||||
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"/>
|
||||
|
||||
<eu.kanade.tachiyomi.widget.PTSansTextView
|
||||
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"
|
||||
app:typeface="ptsansNarrowBold"
|
||||
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_black_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/baseline_close_24"
|
||||
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>
|
@ -104,6 +104,9 @@
|
||||
<string name="action_webview_back">Back</string>
|
||||
<string name="action_webview_forward">Forward</string>
|
||||
<string name="action_auto_check_extensions">Auto-check for updates</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="deleting">Deleting…</string>
|
||||
@ -416,6 +419,10 @@
|
||||
<string name="download_all">Download all</string>
|
||||
<string name="download_unread">Download unread</string>
|
||||
<string name="confirm_delete_chapters">Are you sure you want to delete selected chapters?</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>
|
||||
@ -486,6 +493,7 @@
|
||||
<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">An error occurred while downloading chapters. You can try again in the downloads section</string>
|
||||
|