Finished Part 1 of new auto source migration

This commit is contained in:
Jay 2020-01-04 02:28:43 -08:00
parent 8ba75831e6
commit 10206ae7b3
36 changed files with 1196 additions and 54 deletions

View 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>

View 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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 B

View 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>

View File

@ -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()
}
}

View File

@ -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
}
}
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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)
})
}
}
}

View File

@ -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)
@ -91,7 +89,7 @@ class MigrationProcedureAdapter(val controller: MigrationProcedureController,
migrateMangaInternal(
manga.manga() ?: return@withContext,
toMangaObj,
!(controller.config?.copy ?: false)
false
)
}
}
@ -100,7 +98,7 @@ class MigrationProcedureAdapter(val controller: MigrationProcedureController,
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) {

View File

@ -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

View File

@ -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)

View File

@ -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()
//}
}
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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.
*/

View File

@ -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>

View File

@ -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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@ -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>