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
.subscribe { onSelectionChanged(it) }
subscriptions += controller.selectAllRelay
.subscribe {
if (it == category.id) {
adapter.currentItems.forEach { item ->
controller.setSelection(item.manga, true)
}
controller.invalidateActionMode()
}
}
}
fun onRecycle() {

View File

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

View File

@ -13,7 +13,8 @@ import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy
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
private val db: DatabaseHelper by injectLazy()
@ -26,7 +27,11 @@ class SmartSearchEngine(parentContext: CoroutineContext): CoroutineScope {
val eligibleManga = supervisorScope {
queries.map { query ->
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 {
val cleanedMangaTitle = cleanSmartSearchTitle(it.title)
@ -44,7 +49,10 @@ class SmartSearchEngine(parentContext: CoroutineContext): CoroutineScope {
suspend fun normalSearch(source: CatalogueSource, title: String): SManga? {
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 {
val normalizedDistance = NormalizedLevenshtein().similarity(title, it.title)
@ -80,7 +88,7 @@ class SmartSearchEngine(parentContext: CoroutineContext): CoroutineScope {
)
return searchQueries.map {
it.joinToString().trim()
it.joinToString(" ").trim()
}.distinct()
}
@ -167,8 +175,8 @@ class SmartSearchEngine(parentContext: CoroutineContext): CoroutineScope {
}
companion object {
const val MIN_SMART_ELIGIBLE_THRESHOLD = 0.7
const val MIN_NORMAL_ELIGIBLE_THRESHOLD = 0.5
const val MIN_SMART_ELIGIBLE_THRESHOLD = 0.4
const val MIN_NORMAL_ELIGIBLE_THRESHOLD = 0.4
private val titleRegex = Regex("[^a-zA-Z0-9- ]")
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.ui.base.controller.withFadeTransaction
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.migration.manga.process.MigrationProcedureConfig
import exh.ui.migration.manga.process.MigrationProcedureController
import kotlinx.android.synthetic.main.eh_migration_design.*
import uy.kohesive.injekt.injectLazy
// TODO Handle config changes
// TODO Select all in library
class MigrationDesignController(bundle: Bundle? = null) : BaseExhController(bundle), FlexibleAdapter.OnItemClickListener {
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 var showingOptions = false
override fun getTitle() = "Select target sources"
override fun onViewCreated(view: View) {
@ -53,13 +56,33 @@ class MigrationDesignController(bundle: Bundle? = null) : BaseExhController(bund
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 ->
updatePrioritizeChapterCount(b)
}
extra_search_param.setOnCheckedChangeListener { _, b ->
updateOptionsState()
}
updatePrioritizeChapterCount(prioritize_chapter_count.isChecked)
updateOptionsState()
begin_migration_btn.setOnClickListener {
if(!showingOptions) {
showingOptions = true
updateOptionsState()
return@setOnClickListener
}
var flags = 0
if(mig_chapters.isChecked) flags = flags or MigrationFlags.CHAPTERS
if(mig_categories.isChecked) flags = flags or MigrationFlags.CATEGORIES
@ -73,12 +96,41 @@ class MigrationDesignController(bundle: Bundle? = null) : BaseExhController(bund
}.map { it.source.id },
useSourceWithMostChapters = prioritize_chapter_count.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())
}
}
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) {
super.onSaveInstanceState(outState)
adapter?.onSaveInstanceState(outState)
@ -92,9 +144,9 @@ class MigrationDesignController(bundle: Bundle? = null) : BaseExhController(bund
private fun updatePrioritizeChapterCount(migrationMode: Boolean) {
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 {
"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.await
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.flow.Flow
import kotlin.coroutines.CoroutineContext
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.ViewGroup
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.elvishew.xlog.XLog
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
import eu.kanade.tachiyomi.data.database.models.MangaCategory
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.SourceManager
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.all.MergedSource
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
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.util.gone
import eu.kanade.tachiyomi.util.inflate
import eu.kanade.tachiyomi.util.syncChaptersWithSource
import eu.kanade.tachiyomi.util.visible
import eu.kanade.tachiyomi.util.*
import exh.MERGED_SOURCE_ID
import exh.debug.DebugFunctions.sourceManager
import exh.util.await
import kotlinx.android.synthetic.main.eh_manga_card.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.collect
import uy.kohesive.injekt.injectLazy
import java.text.DateFormat
import java.text.DecimalFormat
import java.util.*
import kotlin.coroutines.CoroutineContext
class MigrationProcedureAdapter(val controller: MigrationProcedureController,
@ -42,6 +39,8 @@ class MigrationProcedureAdapter(val controller: MigrationProcedureController,
private val gson: Gson by injectLazy()
private val sourceManager: SourceManager by injectLazy()
private val logger = XLog.tag(this::class.simpleName)
override fun isViewFromObject(p0: View, p1: Any): Boolean {
return p0 == p1
}
@ -57,34 +56,51 @@ class MigrationProcedureAdapter(val controller: MigrationProcedureController,
controller.nextMigration()
}
view.accept_migration.setOnClickListener {
view.migrating_frame.visible()
}
val viewTag = ViewTag(coroutineContext)
view.tag = viewTag
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
}
fun performMigration() {
suspend fun performMigration(manga: MigratingManga) {
if(!manga.searchResult.initialized) {
return
}
private fun migrateMangaInternal(source: Source,
sourceChapters: List<SChapter>,
prevManga: Manga,
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(prevManga: Manga,
manga: Manga,
replace: Boolean) {
db.inTransaction {
// Update chapters read
if (migrateChapters) {
try {
syncChaptersWithSource(db, sourceChapters, manga, source)
} catch (e: Exception) {
// Worst case, chapters won't be synced
}
if (MigrationFlags.hasChapters(controller.config.migrationFlags)) {
val prevMangaChapters = db.getChapters(prevManga).executeAsBlocking()
val maxChapterRead = prevMangaChapters.filter { it.read }
.maxBy { it.chapter_number }?.chapter_number
@ -99,13 +115,13 @@ class MigrationProcedureAdapter(val controller: MigrationProcedureController,
}
}
// Update categories
if (migrateCategories) {
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 (migrateTracks) {
if (MigrationFlags.hasTracks(controller.config.migrationFlags)) {
val tracks = db.getTracks(prevManga).executeAsBlocking()
for (track in tracks) {
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
GlideApp.with(context)
@ -221,6 +237,23 @@ class MigrationProcedureAdapter(val controller: MigrationProcedureController,
SManga.LICENSED -> R.string.licensed
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) {

View File

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

View File

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

View File

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

View File

@ -19,11 +19,10 @@
android:layout_height="0dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:contentDescription="@string/description_cover"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="h,3:2"
app:layout_constraintDimensionRatio="l,2:3"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/card_scroll_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_min="100dp"
tools:background="@color/material_grey_700" />
@ -31,10 +30,11 @@
<android.support.constraint.ConstraintLayout
android:id="@+id/card_scroll_content"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="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_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="@+id/manga_cover">
@ -130,6 +130,75 @@
app:layout_constraintLeft_toRightOf="@+id/manga_status_label"
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
android:id="@+id/manga_source_label"
style="@style/TextAppearance.Medium.Body2"
@ -139,7 +208,7 @@
android:text="@string/manga_info_source_label"
android:textIsSelectable="false"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/manga_status_label" />
app:layout_constraintTop_toBottomOf="@+id/manga_last_update_label" />
<TextView
android:id="@+id/manga_source"
@ -151,7 +220,7 @@
android:textIsSelectable="false"
app:layout_constraintLeft_toRightOf="@+id/manga_source_label"
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>

View File

@ -35,8 +35,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="@string/chapters"
android:checked="true"
android:text="@string/chapters"
app:layout_constraintBottom_toTopOf="@+id/textView"
app:layout_constraintStart_toStartOf="@+id/textView2" />
@ -46,8 +46,8 @@
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:text="@string/categories"
android:checked="true"
android:text="@string/categories"
app:layout_constraintBottom_toBottomOf="@+id/mig_chapters"
app:layout_constraintStart_toEndOf="@+id/mig_chapters" />
@ -57,8 +57,8 @@
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:text="@string/track"
android:checked="true"
android:text="@string/track"
app:layout_constraintBottom_toBottomOf="@+id/mig_categories"
app:layout_constraintStart_toEndOf="@+id/mig_categories" />
@ -92,6 +92,7 @@
android:layout_marginRight="8dp"
android:layout_marginBottom="8dp"
android:gravity="start|center_vertical"
android:clickable="true"
app:layout_constraintBottom_toTopOf="@+id/fuzzy_search"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/prioritize_chapter_count" />
@ -100,7 +101,6 @@
android:id="@+id/use_smart_search"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
app:layout_constraintStart_toStartOf="@+id/textView"
app:layout_constraintTop_toTopOf="@+id/fuzzy_search" />
@ -114,11 +114,73 @@
android:layout_marginRight="8dp"
android:layout_marginBottom="8dp"
android:gravity="start|center_vertical"
android:text="Use smart search algorithm"
app:layout_constraintBottom_toTopOf="@+id/begin_migration_btn"
android:text="Use intelligent search algorithm"
android:clickable="true"
app:layout_constraintBottom_toTopOf="@+id/copy_manga"
app:layout_constraintEnd_toEndOf="parent"
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
android:id="@+id/begin_migration_btn"
style="@style/Theme.Widget.Button.Colored"
@ -134,4 +196,10 @@
app:layout_constraintEnd_toEndOf="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>

View File

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

View File

@ -18,6 +18,11 @@
android:icon="@drawable/ic_delete_white_24dp"
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
android:id="@+id/action_auto_source_migration"
android:title="Source migration (automatic)"