Finish auto-migration feature

This commit is contained in:
NerdNumber9 2019-08-03 02:23:21 -04:00
parent 9cc24a3be3
commit 4f2985469c
13 changed files with 360 additions and 95 deletions

View File

@ -165,6 +165,16 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
subscriptions += controller.selectionRelay subscriptions += controller.selectionRelay
.subscribe { onSelectionChanged(it) } .subscribe { onSelectionChanged(it) }
subscriptions += controller.selectAllRelay
.subscribe {
if (it == category.id) {
adapter.currentItems.forEach { item ->
controller.setSelection(item.manga, true)
}
controller.invalidateActionMode()
}
}
} }
fun onRecycle() { fun onRecycle() {

View File

@ -99,6 +99,8 @@ class LibraryController(
*/ */
val libraryMangaRelay: BehaviorRelay<LibraryMangaEvent> = BehaviorRelay.create() val libraryMangaRelay: BehaviorRelay<LibraryMangaEvent> = BehaviorRelay.create()
val selectAllRelay: PublishRelay<Int> = PublishRelay.create()
/** /**
* Number of manga per row in grid mode. * Number of manga per row in grid mode.
*/ */
@ -436,6 +438,9 @@ class LibraryController(
} }
R.id.action_move_to_category -> showChangeMangaCategoriesDialog() R.id.action_move_to_category -> showChangeMangaCategoriesDialog()
R.id.action_delete -> showDeleteMangaDialog() R.id.action_delete -> showDeleteMangaDialog()
R.id.action_select_all -> {
selectAllRelay.call(activeCategory)
}
R.id.action_auto_source_migration -> { R.id.action_auto_source_migration -> {
router.pushController(MigrationDesignController.create( router.pushController(MigrationDesignController.create(
selectedMangas.mapNotNull { it.id } selectedMangas.mapNotNull { it.id }

View File

@ -13,7 +13,8 @@ import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
class SmartSearchEngine(parentContext: CoroutineContext): CoroutineScope { class SmartSearchEngine(parentContext: CoroutineContext,
val extraSearchParams: String? = null): CoroutineScope {
override val coroutineContext: CoroutineContext = parentContext + Job() + Dispatchers.Default override val coroutineContext: CoroutineContext = parentContext + Job() + Dispatchers.Default
private val db: DatabaseHelper by injectLazy() private val db: DatabaseHelper by injectLazy()
@ -26,7 +27,11 @@ class SmartSearchEngine(parentContext: CoroutineContext): CoroutineScope {
val eligibleManga = supervisorScope { val eligibleManga = supervisorScope {
queries.map { query -> queries.map { query ->
async(Dispatchers.Default) { async(Dispatchers.Default) {
val searchResults = source.fetchSearchManga(1, query, FilterList()).toSingle().await(Schedulers.io()) val builtQuery = if(extraSearchParams != null) {
"$query ${extraSearchParams.trim()}"
} else title
val searchResults = source.fetchSearchManga(1, builtQuery, FilterList()).toSingle().await(Schedulers.io())
searchResults.mangas.map { searchResults.mangas.map {
val cleanedMangaTitle = cleanSmartSearchTitle(it.title) val cleanedMangaTitle = cleanSmartSearchTitle(it.title)
@ -44,7 +49,10 @@ class SmartSearchEngine(parentContext: CoroutineContext): CoroutineScope {
suspend fun normalSearch(source: CatalogueSource, title: String): SManga? { suspend fun normalSearch(source: CatalogueSource, title: String): SManga? {
val eligibleManga = supervisorScope { val eligibleManga = supervisorScope {
val searchResults = source.fetchSearchManga(1, title, FilterList()).toSingle().await(Schedulers.io()) val searchQuery = if(extraSearchParams != null) {
"$title ${extraSearchParams.trim()}"
} else title
val searchResults = source.fetchSearchManga(1, searchQuery, FilterList()).toSingle().await(Schedulers.io())
searchResults.mangas.map { searchResults.mangas.map {
val normalizedDistance = NormalizedLevenshtein().similarity(title, it.title) val normalizedDistance = NormalizedLevenshtein().similarity(title, it.title)
@ -80,7 +88,7 @@ class SmartSearchEngine(parentContext: CoroutineContext): CoroutineScope {
) )
return searchQueries.map { return searchQueries.map {
it.joinToString().trim() it.joinToString(" ").trim()
}.distinct() }.distinct()
} }
@ -167,8 +175,8 @@ class SmartSearchEngine(parentContext: CoroutineContext): CoroutineScope {
} }
companion object { companion object {
const val MIN_SMART_ELIGIBLE_THRESHOLD = 0.7 const val MIN_SMART_ELIGIBLE_THRESHOLD = 0.4
const val MIN_NORMAL_ELIGIBLE_THRESHOLD = 0.5 const val MIN_NORMAL_ELIGIBLE_THRESHOLD = 0.4
private val titleRegex = Regex("[^a-zA-Z0-9- ]") private val titleRegex = Regex("[^a-zA-Z0-9- ]")
private val consecutiveSpacesRegex = Regex(" +") private val consecutiveSpacesRegex = Regex(" +")

View File

@ -11,13 +11,14 @@ import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.migration.MigrationFlags import eu.kanade.tachiyomi.ui.migration.MigrationFlags
import eu.kanade.tachiyomi.util.gone
import eu.kanade.tachiyomi.util.visible
import exh.ui.base.BaseExhController import exh.ui.base.BaseExhController
import exh.ui.migration.manga.process.MigrationProcedureConfig import exh.ui.migration.manga.process.MigrationProcedureConfig
import exh.ui.migration.manga.process.MigrationProcedureController import exh.ui.migration.manga.process.MigrationProcedureController
import kotlinx.android.synthetic.main.eh_migration_design.* import kotlinx.android.synthetic.main.eh_migration_design.*
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
// TODO Handle config changes
// TODO Select all in library // TODO Select all in library
class MigrationDesignController(bundle: Bundle? = null) : BaseExhController(bundle), FlexibleAdapter.OnItemClickListener { class MigrationDesignController(bundle: Bundle? = null) : BaseExhController(bundle), FlexibleAdapter.OnItemClickListener {
private val sourceManager: SourceManager by injectLazy() private val sourceManager: SourceManager by injectLazy()
@ -29,6 +30,8 @@ class MigrationDesignController(bundle: Bundle? = null) : BaseExhController(bund
private val config: LongArray = args.getLongArray(MANGA_IDS_EXTRA) ?: LongArray(0) private val config: LongArray = args.getLongArray(MANGA_IDS_EXTRA) ?: LongArray(0)
private var showingOptions = false
override fun getTitle() = "Select target sources" override fun getTitle() = "Select target sources"
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
@ -53,13 +56,33 @@ class MigrationDesignController(bundle: Bundle? = null) : BaseExhController(bund
use_smart_search.toggle() use_smart_search.toggle()
} }
copy_manga_desc.setOnClickListener {
copy_manga.toggle()
}
extra_search_param_desc.setOnClickListener {
extra_search_param.toggle()
}
prioritize_chapter_count.setOnCheckedChangeListener { _, b -> prioritize_chapter_count.setOnCheckedChangeListener { _, b ->
updatePrioritizeChapterCount(b) updatePrioritizeChapterCount(b)
} }
extra_search_param.setOnCheckedChangeListener { _, b ->
updateOptionsState()
}
updatePrioritizeChapterCount(prioritize_chapter_count.isChecked) updatePrioritizeChapterCount(prioritize_chapter_count.isChecked)
updateOptionsState()
begin_migration_btn.setOnClickListener { begin_migration_btn.setOnClickListener {
if(!showingOptions) {
showingOptions = true
updateOptionsState()
return@setOnClickListener
}
var flags = 0 var flags = 0
if(mig_chapters.isChecked) flags = flags or MigrationFlags.CHAPTERS if(mig_chapters.isChecked) flags = flags or MigrationFlags.CHAPTERS
if(mig_categories.isChecked) flags = flags or MigrationFlags.CATEGORIES if(mig_categories.isChecked) flags = flags or MigrationFlags.CATEGORIES
@ -73,12 +96,41 @@ class MigrationDesignController(bundle: Bundle? = null) : BaseExhController(bund
}.map { it.source.id }, }.map { it.source.id },
useSourceWithMostChapters = prioritize_chapter_count.isChecked, useSourceWithMostChapters = prioritize_chapter_count.isChecked,
enableLenientSearch = use_smart_search.isChecked, enableLenientSearch = use_smart_search.isChecked,
migrationFlags = flags 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
) )
).withFadeTransaction()) ).withFadeTransaction())
} }
} }
fun updateOptionsState() {
if (showingOptions) {
begin_migration_btn.text = "Begin migration"
options_group.visible()
if(extra_search_param.isChecked) {
extra_search_param_text.visible()
} else {
extra_search_param_text.gone()
}
} else {
begin_migration_btn.text = "Next step"
options_group.gone()
extra_search_param_text.gone()
}
}
override fun handleBack(): Boolean {
if(showingOptions) {
showingOptions = false
updateOptionsState()
return true
}
return super.handleBack()
}
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
adapter?.onSaveInstanceState(outState) adapter?.onSaveInstanceState(outState)
@ -92,9 +144,9 @@ class MigrationDesignController(bundle: Bundle? = null) : BaseExhController(bund
private fun updatePrioritizeChapterCount(migrationMode: Boolean) { private fun updatePrioritizeChapterCount(migrationMode: Boolean) {
migration_mode.text = if(migrationMode) { migration_mode.text = if(migrationMode) {
"Use the source with the most chapters and use the above list to break ties (slow with many sources or smart search)" "Currently using the source with the most chapters and the above list to break ties (slow with many sources or smart search)"
} else { } else {
"Use the first source in the list that has the manga" "Currently using the first source in the list that has the manga"
} }
} }

View File

@ -7,11 +7,7 @@ import eu.kanade.tachiyomi.source.SourceManager
import exh.util.DeferredField import exh.util.DeferredField
import exh.util.await import exh.util.await
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ConflatedBroadcastChannel import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.flow.Flow
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
class MigratingManga(private val db: DatabaseHelper, class MigratingManga(private val db: DatabaseHelper,

View File

@ -4,28 +4,22 @@ import android.support.v4.view.PagerAdapter
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.elvishew.xlog.XLog
import com.google.gson.Gson import com.google.gson.Gson
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.all.MergedSource import eu.kanade.tachiyomi.source.online.all.MergedSource
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoController
import eu.kanade.tachiyomi.ui.migration.MigrationFlags import eu.kanade.tachiyomi.ui.migration.MigrationFlags
import eu.kanade.tachiyomi.util.gone import eu.kanade.tachiyomi.util.*
import eu.kanade.tachiyomi.util.inflate
import eu.kanade.tachiyomi.util.syncChaptersWithSource
import eu.kanade.tachiyomi.util.visible
import exh.MERGED_SOURCE_ID import exh.MERGED_SOURCE_ID
import exh.debug.DebugFunctions.sourceManager
import exh.util.await import exh.util.await
import kotlinx.android.synthetic.main.eh_manga_card.view.* import kotlinx.android.synthetic.main.eh_manga_card.view.*
import kotlinx.android.synthetic.main.eh_migration_process_item.view.* import kotlinx.android.synthetic.main.eh_migration_process_item.view.*
@ -33,6 +27,9 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.text.DateFormat
import java.text.DecimalFormat
import java.util.*
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
class MigrationProcedureAdapter(val controller: MigrationProcedureController, class MigrationProcedureAdapter(val controller: MigrationProcedureController,
@ -42,6 +39,8 @@ class MigrationProcedureAdapter(val controller: MigrationProcedureController,
private val gson: Gson by injectLazy() private val gson: Gson by injectLazy()
private val sourceManager: SourceManager by injectLazy() private val sourceManager: SourceManager by injectLazy()
private val logger = XLog.tag(this::class.simpleName)
override fun isViewFromObject(p0: View, p1: Any): Boolean { override fun isViewFromObject(p0: View, p1: Any): Boolean {
return p0 == p1 return p0 == p1
} }
@ -57,34 +56,51 @@ class MigrationProcedureAdapter(val controller: MigrationProcedureController,
controller.nextMigration() controller.nextMigration()
} }
view.accept_migration.setOnClickListener {
view.migrating_frame.visible()
}
val viewTag = ViewTag(coroutineContext) val viewTag = ViewTag(coroutineContext)
view.tag = viewTag view.tag = viewTag
view.setupView(viewTag, item) view.setupView(viewTag, item)
view.accept_migration.setOnClickListener {
viewTag.launch(Dispatchers.Main) {
view.migrating_frame.visible()
try {
withContext(Dispatchers.Default) {
performMigration(item)
}
controller.nextMigration()
} catch(e: Exception) {
logger.e("Migration failure!", e)
controller.migrationFailure()
}
view.migrating_frame.gone()
}
}
return view return view
} }
fun performMigration() { suspend fun performMigration(manga: MigratingManga) {
if(!manga.searchResult.initialized) {
return
}
val toMangaObj = db.getManga(manga.searchResult.get() ?: return).await() ?: return
withContext(Dispatchers.IO) {
migrateMangaInternal(
manga.manga() ?: return@withContext,
toMangaObj,
!controller.config.copy
)
}
} }
private fun migrateMangaInternal(source: Source,
sourceChapters: List<SChapter>, private fun migrateMangaInternal(prevManga: Manga,
prevManga: Manga,
manga: Manga, manga: Manga,
replace: Boolean) { replace: Boolean) {
db.inTransaction { db.inTransaction {
// Update chapters read // Update chapters read
if (migrateChapters) { if (MigrationFlags.hasChapters(controller.config.migrationFlags)) {
try {
syncChaptersWithSource(db, sourceChapters, manga, source)
} catch (e: Exception) {
// Worst case, chapters won't be synced
}
val prevMangaChapters = db.getChapters(prevManga).executeAsBlocking() val prevMangaChapters = db.getChapters(prevManga).executeAsBlocking()
val maxChapterRead = prevMangaChapters.filter { it.read } val maxChapterRead = prevMangaChapters.filter { it.read }
.maxBy { it.chapter_number }?.chapter_number .maxBy { it.chapter_number }?.chapter_number
@ -99,13 +115,13 @@ class MigrationProcedureAdapter(val controller: MigrationProcedureController,
} }
} }
// Update categories // Update categories
if (migrateCategories) { if (MigrationFlags.hasCategories(controller.config.migrationFlags)) {
val categories = db.getCategoriesForManga(prevManga).executeAsBlocking() val categories = db.getCategoriesForManga(prevManga).executeAsBlocking()
val mangaCategories = categories.map { MangaCategory.create(manga, it) } val mangaCategories = categories.map { MangaCategory.create(manga, it) }
db.setMangaCategories(mangaCategories, listOf(manga)) db.setMangaCategories(mangaCategories, listOf(manga))
} }
// Update track // Update track
if (migrateTracks) { if (MigrationFlags.hasTracks(controller.config.migrationFlags)) {
val tracks = db.getTracks(prevManga).executeAsBlocking() val tracks = db.getTracks(prevManga).executeAsBlocking()
for (track in tracks) { for (track in tracks) {
track.id = null track.id = null
@ -174,7 +190,7 @@ class MigrationProcedureAdapter(val controller: MigrationProcedureController,
} }
} }
fun View.attachManga(tag: ViewTag, manga: Manga, source: Source) { suspend fun View.attachManga(tag: ViewTag, manga: Manga, source: Source) {
// TODO Duplicated in MangaInfoController // TODO Duplicated in MangaInfoController
GlideApp.with(context) GlideApp.with(context)
@ -221,6 +237,23 @@ class MigrationProcedureAdapter(val controller: MigrationProcedureController,
SManga.LICENSED -> R.string.licensed SManga.LICENSED -> R.string.licensed
else -> R.string.unknown else -> R.string.unknown
}) })
val mangaChapters = db.getChapters(manga).await()
manga_chapters.text = mangaChapters.size.toString()
val latestChapter = mangaChapters.maxBy { it.chapter_number }?.chapter_number ?: -1f
val lastUpdate = Date(mangaChapters.maxBy { it.date_upload }?.date_upload ?: 0)
if (latestChapter > 0f) {
manga_last_chapter.text = DecimalFormat("#.#").format(count)
} else {
manga_last_chapter.setText(R.string.unknown)
}
if (lastUpdate.time != 0L) {
manga_last_update.text = DateFormat.getDateInstance(DateFormat.SHORT).format(lastUpdate)
} else {
manga_last_update.setText(R.string.unknown)
}
} }
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) { override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {

View File

@ -9,5 +9,7 @@ data class MigrationProcedureConfig(
val targetSourceIds: List<Long>, val targetSourceIds: List<Long>,
val useSourceWithMostChapters: Boolean, val useSourceWithMostChapters: Boolean,
val enableLenientSearch: Boolean, val enableLenientSearch: Boolean,
val migrationFlags: Int val migrationFlags: Int,
val copy: Boolean,
val extraSearchParams: String?
): Parcelable ): Parcelable

View File

@ -4,12 +4,13 @@ import android.content.pm.ActivityInfo
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import com.afollestad.materialdialogs.MaterialDialog
import com.elvishew.xlog.XLog import com.elvishew.xlog.XLog
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.util.syncChaptersWithSource
import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.util.toast
import exh.smartsearch.SmartSearchEngine import exh.smartsearch.SmartSearchEngine
import exh.ui.base.BaseExhController import exh.ui.base.BaseExhController
@ -22,6 +23,7 @@ import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
// TODO Will probably implode if activity is fully destroyed
class MigrationProcedureController(bundle: Bundle? = null) : BaseExhController(bundle), CoroutineScope { class MigrationProcedureController(bundle: Bundle? = null) : BaseExhController(bundle), CoroutineScope {
override val layoutId = R.layout.eh_migration_process override val layoutId = R.layout.eh_migration_process
@ -29,12 +31,12 @@ class MigrationProcedureController(bundle: Bundle? = null) : BaseExhController(b
private var adapter: MigrationProcedureAdapter? = null private var adapter: MigrationProcedureAdapter? = null
private val config: MigrationProcedureConfig = args.getParcelable(CONFIG_EXTRA) val config: MigrationProcedureConfig = args.getParcelable(CONFIG_EXTRA)
private val db: DatabaseHelper by injectLazy() private val db: DatabaseHelper by injectLazy()
private val sourceManager: SourceManager by injectLazy() private val sourceManager: SourceManager by injectLazy()
private val smartSearchEngine = SmartSearchEngine(coroutineContext) private val smartSearchEngine = SmartSearchEngine(coroutineContext, config.extraSearchParams)
private val logger = XLog.tag("MigrationProcedureController") private val logger = XLog.tag("MigrationProcedureController")
@ -74,11 +76,15 @@ class MigrationProcedureController(bundle: Bundle? = null) : BaseExhController(b
} }
} }
updateTitle() pager.post {
// pager.currentItem doesn't appear to be valid if we don't do this in a post
updateTitle()
}
} }
fun updateTitle() { fun updateTitle() {
titleText = "Migrate manga (${pager.currentItem + 1}/${adapter?.count ?: 0})" titleText = "Migrate manga (${pager.currentItem + 1}/${adapter?.count ?: 0})"
setTitle()
} }
fun nextMigration() { fun nextMigration() {
@ -88,14 +94,23 @@ class MigrationProcedureController(bundle: Bundle? = null) : BaseExhController(b
router.popCurrentController() router.popCurrentController()
} else { } else {
pager.setCurrentItem(pager.currentItem + 1, true) pager.setCurrentItem(pager.currentItem + 1, true)
updateTitle()
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
setTitle() 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>) { suspend fun runMigrations(mangas: List<MigratingManga>) {
val sources = config.targetSourceIds.mapNotNull { sourceManager.get(it) as? CatalogueSource } val sources = config.targetSourceIds.mapNotNull { sourceManager.get(it) as? CatalogueSource }
@ -123,21 +138,22 @@ class MigrationProcedureController(bundle: Bundle? = null) : BaseExhController(b
async { async {
sourceSemaphore.withPermit { sourceSemaphore.withPermit {
try { try {
supervisorScope { val searchResult = if (config.enableLenientSearch) {
val searchResult = if (config.enableLenientSearch) { smartSearchEngine.smartSearch(source, mangaObj.title)
smartSearchEngine.smartSearch(source, mangaObj.title) } else {
} else { smartSearchEngine.normalSearch(source, mangaObj.title)
smartSearchEngine.normalSearch(source, mangaObj.title) }
}
if(searchResult != null) { if(searchResult != null) {
val localManga = smartSearchEngine.networkToLocalManga(searchResult, source.id) val localManga = smartSearchEngine.networkToLocalManga(searchResult, source.id)
val chapters = source.fetchChapterList(localManga).toSingle().await(Schedulers.io()) val chapters = source.fetchChapterList(localManga).toSingle().await(Schedulers.io())
manga.progress.send(validSources.size to processedSources.incrementAndGet()) withContext(Dispatchers.IO) {
localManga to chapters.size syncChaptersWithSource(db, chapters, localManga, source)
} else {
null
} }
manga.progress.send(validSources.size to processedSources.incrementAndGet())
localManga to chapters.size
} else {
null
} }
} catch(e: Exception) { } catch(e: Exception) {
logger.e("Failed to search in source: ${source.id}!", e) logger.e("Failed to search in source: ${source.id}!", e)
@ -149,17 +165,20 @@ class MigrationProcedureController(bundle: Bundle? = null) : BaseExhController(b
} else { } else {
validSources.forEachIndexed { index, source -> validSources.forEachIndexed { index, source ->
val searchResult = try { val searchResult = try {
supervisorScope { val searchResult = if (config.enableLenientSearch) {
val searchResult = if (config.enableLenientSearch) { smartSearchEngine.smartSearch(source, mangaObj.title)
smartSearchEngine.smartSearch(source, mangaObj.title) } else {
} else { smartSearchEngine.normalSearch(source, mangaObj.title)
smartSearchEngine.normalSearch(source, mangaObj.title)
}
if (searchResult != null) {
smartSearchEngine.networkToLocalManga(searchResult, source.id)
} else null
} }
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: Exception) { } catch(e: Exception) {
logger.e("Failed to search in source: ${source.id}!", e) logger.e("Failed to search in source: ${source.id}!", e)
null null
@ -180,15 +199,13 @@ class MigrationProcedureController(bundle: Bundle? = null) : BaseExhController(b
if(result != null && result.thumbnail_url == null) { if(result != null && result.thumbnail_url == null) {
try { try {
supervisorScope { val newManga = sourceManager.getOrStub(result.source)
val newManga = sourceManager.getOrStub(result.source) .fetchMangaDetails(result)
.fetchMangaDetails(result) .toSingle()
.toSingle() .await()
.await() result.copyFrom(newManga)
result.copyFrom(newManga)
db.insertManga(result).await() db.insertManga(result).await()
}
} catch(e: Exception) { } catch(e: Exception) {
logger.e("Could not load search manga details", e) logger.e("Could not load search manga details", e)
} }

View File

@ -41,6 +41,6 @@ class DeferredField<T> {
mutex.withLock {} mutex.withLock {}
// Field is initialized, return value // Field is initialized, return value
return content!! return content as T
} }
} }

View File

@ -19,11 +19,10 @@
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginLeft="16dp" android:layout_marginLeft="16dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:contentDescription="@string/description_cover" android:contentDescription="@string/description_cover"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintDimensionRatio="l,2:3"
app:layout_constraintDimensionRatio="h,3:2"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/card_scroll_content"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_min="100dp" app:layout_constraintWidth_min="100dp"
tools:background="@color/material_grey_700" /> tools:background="@color/material_grey_700" />
@ -31,10 +30,11 @@
<android.support.constraint.ConstraintLayout <android.support.constraint.ConstraintLayout
android:id="@+id/card_scroll_content" android:id="@+id/card_scroll_content"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="wrap_content"
android:layout_marginLeft="16dp" android:layout_marginLeft="16dp"
android:layout_marginRight="16dp" android:layout_marginRight="16dp"
app:layout_constraintBottom_toBottomOf="@+id/manga_cover" android:paddingBottom="16dp"
app:layout_constraintHorizontal_weight="2"
app:layout_constraintLeft_toRightOf="@+id/manga_cover" app:layout_constraintLeft_toRightOf="@+id/manga_cover"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="@+id/manga_cover"> app:layout_constraintTop_toTopOf="@+id/manga_cover">
@ -130,6 +130,75 @@
app:layout_constraintLeft_toRightOf="@+id/manga_status_label" app:layout_constraintLeft_toRightOf="@+id/manga_status_label"
app:layout_constraintRight_toRightOf="parent" /> app:layout_constraintRight_toRightOf="parent" />
<TextView
android:id="@+id/manga_chapters_label"
style="@style/TextAppearance.Medium.Body2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="false"
android:text="@string/manga_info_chapters_label"
android:textIsSelectable="false"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/manga_status_label" />
<TextView
android:id="@+id/manga_chapters"
style="@style/TextAppearance.Regular.Body1.Secondary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:clickable="false"
android:textIsSelectable="false"
app:layout_constraintLeft_toRightOf="@+id/manga_chapters_label"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/manga_status_label" />
<TextView
android:id="@+id/manga_last_chapter_label"
style="@style/TextAppearance.Medium.Body2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="false"
android:text="@string/manga_info_last_chapter_label"
android:textIsSelectable="false"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/manga_chapters_label" />
<TextView
android:id="@+id/manga_last_chapter"
style="@style/TextAppearance.Regular.Body1.Secondary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:clickable="false"
android:textIsSelectable="false"
app:layout_constraintLeft_toRightOf="@+id/manga_last_chapter_label"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/manga_chapters_label" />
<TextView
android:id="@+id/manga_last_update_label"
style="@style/TextAppearance.Medium.Body2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="false"
android:text="@string/manga_info_latest_data_label"
android:textIsSelectable="false"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/manga_last_chapter_label" />
<TextView
android:id="@+id/manga_last_update"
style="@style/TextAppearance.Regular.Body1.Secondary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:clickable="false"
android:textIsSelectable="false"
app:layout_constraintLeft_toRightOf="@+id/manga_last_update_label"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/manga_last_chapter_label" />
<TextView <TextView
android:id="@+id/manga_source_label" android:id="@+id/manga_source_label"
style="@style/TextAppearance.Medium.Body2" style="@style/TextAppearance.Medium.Body2"
@ -139,7 +208,7 @@
android:text="@string/manga_info_source_label" android:text="@string/manga_info_source_label"
android:textIsSelectable="false" android:textIsSelectable="false"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/manga_status_label" /> app:layout_constraintTop_toBottomOf="@+id/manga_last_update_label" />
<TextView <TextView
android:id="@+id/manga_source" android:id="@+id/manga_source"
@ -151,7 +220,7 @@
android:textIsSelectable="false" android:textIsSelectable="false"
app:layout_constraintLeft_toRightOf="@+id/manga_source_label" app:layout_constraintLeft_toRightOf="@+id/manga_source_label"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/manga_status_label" /> app:layout_constraintTop_toBottomOf="@+id/manga_last_update_label" />
</android.support.constraint.ConstraintLayout> </android.support.constraint.ConstraintLayout>

View File

@ -35,8 +35,8 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:text="@string/chapters"
android:checked="true" android:checked="true"
android:text="@string/chapters"
app:layout_constraintBottom_toTopOf="@+id/textView" app:layout_constraintBottom_toTopOf="@+id/textView"
app:layout_constraintStart_toStartOf="@+id/textView2" /> app:layout_constraintStart_toStartOf="@+id/textView2" />
@ -46,8 +46,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
android:text="@string/categories"
android:checked="true" android:checked="true"
android:text="@string/categories"
app:layout_constraintBottom_toBottomOf="@+id/mig_chapters" app:layout_constraintBottom_toBottomOf="@+id/mig_chapters"
app:layout_constraintStart_toEndOf="@+id/mig_chapters" /> app:layout_constraintStart_toEndOf="@+id/mig_chapters" />
@ -57,8 +57,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
android:text="@string/track"
android:checked="true" android:checked="true"
android:text="@string/track"
app:layout_constraintBottom_toBottomOf="@+id/mig_categories" app:layout_constraintBottom_toBottomOf="@+id/mig_categories"
app:layout_constraintStart_toEndOf="@+id/mig_categories" /> app:layout_constraintStart_toEndOf="@+id/mig_categories" />
@ -92,6 +92,7 @@
android:layout_marginRight="8dp" android:layout_marginRight="8dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:gravity="start|center_vertical" android:gravity="start|center_vertical"
android:clickable="true"
app:layout_constraintBottom_toTopOf="@+id/fuzzy_search" app:layout_constraintBottom_toTopOf="@+id/fuzzy_search"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/prioritize_chapter_count" /> app:layout_constraintStart_toEndOf="@+id/prioritize_chapter_count" />
@ -100,7 +101,6 @@
android:id="@+id/use_smart_search" android:id="@+id/use_smart_search"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
app:layout_constraintStart_toStartOf="@+id/textView" app:layout_constraintStart_toStartOf="@+id/textView"
app:layout_constraintTop_toTopOf="@+id/fuzzy_search" /> app:layout_constraintTop_toTopOf="@+id/fuzzy_search" />
@ -114,11 +114,73 @@
android:layout_marginRight="8dp" android:layout_marginRight="8dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:gravity="start|center_vertical" android:gravity="start|center_vertical"
android:text="Use smart search algorithm" android:text="Use intelligent search algorithm"
app:layout_constraintBottom_toTopOf="@+id/begin_migration_btn" android:clickable="true"
app:layout_constraintBottom_toTopOf="@+id/copy_manga"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/prioritize_chapter_count" /> app:layout_constraintStart_toEndOf="@+id/prioritize_chapter_count" />
<android.support.v7.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="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" />
<android.support.v7.widget.SwitchCompat
android:id="@+id/extra_search_param"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="@+id/textView"
app:layout_constraintTop_toTopOf="@+id/extra_search_param_desc" />
<TextView
android:id="@+id/extra_search_param_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="Include extra search parameter when searching"
android:clickable="true"
app:layout_constraintBottom_toTopOf="@+id/extra_search_param_text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/prioritize_chapter_count" />
<EditText
android:id="@+id/extra_search_param_text"
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:ems="10"
android:hint="Search parameter (e.g. language:english)"
android:inputType="textPersonName"
app:layout_constraintBottom_toTopOf="@+id/begin_migration_btn"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button <Button
android:id="@+id/begin_migration_btn" android:id="@+id/begin_migration_btn"
style="@style/Theme.Widget.Button.Colored" style="@style/Theme.Widget.Button.Colored"
@ -134,4 +196,10 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" /> app:layout_constraintStart_toStartOf="parent" />
<android.support.constraint.Group
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" />
</android.support.constraint.ConstraintLayout> </android.support.constraint.ConstraintLayout>

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:animateLayoutChanges="true">
<android.support.constraint.ConstraintLayout <android.support.constraint.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -18,6 +18,11 @@
android:icon="@drawable/ic_delete_white_24dp" android:icon="@drawable/ic_delete_white_24dp"
app:showAsAction="ifRoom"/> app:showAsAction="ifRoom"/>
<item android:id="@+id/action_select_all"
android:title="@string/action_select_all"
android:icon="@drawable/ic_select_all_white_24dp"
app:showAsAction="ifRoom"/>
<item <item
android:id="@+id/action_auto_source_migration" android:id="@+id/action_auto_source_migration"
android:title="Source migration (automatic)" android:title="Source migration (automatic)"