Various updates to migration list

* Migration list now shows language + number of items in each source
* Option to sort alphabetically (default), by most entries, or by obsolete/uninstalled sources
* Show if a source is obsolete or uninstalled in the extension list
* Refactor main migration controller to reduce similar code + use flows
This commit is contained in:
Jays2Kings 2022-07-11 19:14:00 -04:00
parent 201c334b13
commit 6b1725fd3b
15 changed files with 328 additions and 215 deletions

View file

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.data.preference package eu.kanade.tachiyomi.data.preference
import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
// Library // Library
@ -25,4 +26,16 @@ object PreferenceValues {
LOW(R.string.pref_low, 31), LOW(R.string.pref_low, 31),
LOWEST(R.string.pref_lowest, 47), LOWEST(R.string.pref_lowest, 47),
} }
enum class MigrationSourceOrder(val value: Int, @StringRes val titleResId: Int) {
Alphabetically(0, R.string.alphabetically),
MostEntries(1, R.string.most_entries),
Obsolete(2, R.string.obsolete),
;
companion object {
fun fromValue(preference: Int) = values().find { it.value == preference } ?: Alphabetically
fun fromPreference(pref: PreferencesHelper) = fromValue(pref.migrationSourceOrder().get())
}
}
} }

View file

@ -298,6 +298,8 @@ class PreferencesHelper(val context: Context) {
fun installedExtensionsOrder() = flowPrefs.getInt(Keys.installedExtensionsOrder, InstalledExtensionsOrder.Name.value) fun installedExtensionsOrder() = flowPrefs.getInt(Keys.installedExtensionsOrder, InstalledExtensionsOrder.Name.value)
fun migrationSourceOrder() = flowPrefs.getInt("migration_source_order", Values.MigrationSourceOrder.Alphabetically.value)
fun collapsedCategories() = flowPrefs.getStringSet("collapsed_categories", mutableSetOf()) fun collapsedCategories() = flowPrefs.getStringSet("collapsed_categories", mutableSetOf())
fun collapsedDynamicCategories() = flowPrefs.getStringSet("collapsed_dynamic_categories", mutableSetOf()) fun collapsedDynamicCategories() = flowPrefs.getStringSet("collapsed_dynamic_categories", mutableSetOf())

View file

@ -3,23 +3,13 @@ package eu.kanade.tachiyomi.ui.extension
import android.content.pm.PackageInstaller import android.content.pm.PackageInstaller
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import eu.kanade.tachiyomi.R 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.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.ExtensionInstallService import eu.kanade.tachiyomi.extension.ExtensionInstallService
import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.extension.model.InstalledExtensionsOrder import eu.kanade.tachiyomi.extension.model.InstalledExtensionsOrder
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.ui.migration.BaseMigrationPresenter
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.base.presenter.BaseCoroutinePresenter
import eu.kanade.tachiyomi.ui.migration.MangaItem
import eu.kanade.tachiyomi.ui.migration.SelectionHeader
import eu.kanade.tachiyomi.ui.migration.SourceItem
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.executeOnIO
import eu.kanade.tachiyomi.util.system.withUIContext import eu.kanade.tachiyomi.util.system.withUIContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
@ -28,7 +18,6 @@ import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
typealias ExtensionTuple = typealias ExtensionTuple =
@ -38,26 +27,13 @@ typealias ExtensionIntallInfo = Pair<InstallStep, PackageInstaller.SessionInfo?>
/** /**
* Presenter of [ExtensionBottomSheet]. * Presenter of [ExtensionBottomSheet].
*/ */
class ExtensionBottomPresenter( class ExtensionBottomPresenter() : BaseMigrationPresenter<ExtensionBottomSheet>() {
private val extensionManager: ExtensionManager = Injekt.get(),
val preferences: PreferencesHelper = Injekt.get(),
) : BaseCoroutinePresenter<ExtensionBottomSheet>() {
private var extensions = emptyList<ExtensionItem>() private var extensions = emptyList<ExtensionItem>()
var sourceItems = emptyList<SourceItem>()
private set
var mangaItems = hashMapOf<Long, List<MangaItem>>()
private set
private var currentDownloads = hashMapOf<String, ExtensionIntallInfo>() private var currentDownloads = hashMapOf<String, ExtensionIntallInfo>()
private val sourceManager: SourceManager = Injekt.get()
private var selectedSource: Long? = null
private var firstLoad = true private var firstLoad = true
private val db: DatabaseHelper = Injekt.get()
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -73,25 +49,7 @@ class ExtensionBottomPresenter(
) )
withContext(Dispatchers.Main) { controller?.setExtensions(extensions, false) } withContext(Dispatchers.Main) { controller?.setExtensions(extensions, false) }
} }
val migrationJob = async { val migrationJob = async { firstTimeMigration() }
val favs = db.getFavoriteMangas().executeOnIO()
sourceItems = findSourcesWithManga(favs)
mangaItems = HashMap(
sourceItems.associate {
it.source.id to this@ExtensionBottomPresenter.libraryToMigrationItem(
favs,
it.source.id,
)
},
)
withContext(Dispatchers.Main) {
if (selectedSource != null) {
controller?.setMigrationManga(mangaItems[selectedSource])
} else {
controller?.setMigrationSources(sourceItems)
}
}
}
listOf(migrationJob, extensionJob).awaitAll() listOf(migrationJob, extensionJob).awaitAll()
} }
presenterScope.launch { presenterScope.launch {
@ -129,18 +87,6 @@ class ExtensionBottomPresenter(
} }
} }
private fun findSourcesWithManga(library: List<Manga>): List<SourceItem> {
val header = SelectionHeader()
return library.map { it.source }.toSet()
.mapNotNull { if (it != LocalSource.ID) sourceManager.getOrStub(it) else null }
.sortedBy { it.name }
.map { SourceItem(it, header) }
}
private fun libraryToMigrationItem(library: List<Manga>, sourceId: Long): List<MangaItem> {
return library.filter { it.source == sourceId }.map(::MangaItem)
}
fun refreshExtensions() { fun refreshExtensions() {
presenterScope.launch { presenterScope.launch {
extensions = toItems( extensions = toItems(
@ -154,25 +100,6 @@ class ExtensionBottomPresenter(
} }
} }
fun refreshMigrations() {
presenterScope.launch {
val favs = db.getFavoriteMangas().executeOnIO()
sourceItems = findSourcesWithManga(favs)
mangaItems = HashMap(
sourceItems.associate {
it.source.id to this@ExtensionBottomPresenter.libraryToMigrationItem(favs, it.source.id)
},
)
withContext(Dispatchers.Main) {
if (selectedSource != null) {
controller?.setMigrationManga(mangaItems[selectedSource])
} else {
controller?.setMigrationSources(sourceItems)
}
}
}
}
@Synchronized @Synchronized
private fun toItems(tuple: ExtensionTuple): List<ExtensionItem> { private fun toItems(tuple: ExtensionTuple): List<ExtensionItem> {
val context = controller?.context ?: return emptyList() val context = controller?.context ?: return emptyList()
@ -355,18 +282,4 @@ class ExtensionBottomPresenter(
fun trustSignature(signatureHash: String) { fun trustSignature(signatureHash: String) {
extensionManager.trustSignature(signatureHash) extensionManager.trustSignature(signatureHash)
} }
fun setSelectedSource(source: Source) {
selectedSource = source.id
presenterScope.launch {
withContext(Dispatchers.Main) { controller?.setMigrationManga(mangaItems[source.id]) }
}
}
fun deselectSource() {
selectedSource = null
presenterScope.launch {
withContext(Dispatchers.Main) { controller?.setMigrationSources(sourceItems) }
}
}
} }

View file

@ -21,6 +21,7 @@ import eu.kanade.tachiyomi.databinding.RecyclerWithScrollerBinding
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstalledExtensionsOrder import eu.kanade.tachiyomi.extension.model.InstalledExtensionsOrder
import eu.kanade.tachiyomi.ui.extension.details.ExtensionDetailsController import eu.kanade.tachiyomi.ui.extension.details.ExtensionDetailsController
import eu.kanade.tachiyomi.ui.migration.BaseMigrationInterface
import eu.kanade.tachiyomi.ui.migration.MangaAdapter import eu.kanade.tachiyomi.ui.migration.MangaAdapter
import eu.kanade.tachiyomi.ui.migration.MangaItem import eu.kanade.tachiyomi.ui.migration.MangaItem
import eu.kanade.tachiyomi.ui.migration.SourceAdapter import eu.kanade.tachiyomi.ui.migration.SourceAdapter
@ -46,7 +47,8 @@ class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: At
FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener, FlexibleAdapter.OnItemLongClickListener,
ExtensionTrustDialog.Listener, ExtensionTrustDialog.Listener,
SourceAdapter.OnAllClickListener { SourceAdapter.OnAllClickListener,
BaseMigrationInterface {
var sheetBehavior: BottomSheetBehavior<*>? = null var sheetBehavior: BottomSheetBehavior<*>? = null
@ -62,6 +64,7 @@ class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: At
get() = listOf(extAdapter, migAdapter) get() = listOf(extAdapter, migAdapter)
val presenter = ExtensionBottomPresenter() val presenter = ExtensionBottomPresenter()
var currentSourceTitle: String? = null
private var extensions: List<ExtensionItem> = emptyList() private var extensions: List<ExtensionItem> = emptyList()
var canExpand = false var canExpand = false
@ -323,22 +326,28 @@ class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: At
drawExtensions() drawExtensions()
} }
fun setMigrationSources(sources: List<SourceItem>) { override fun setMigrationSources(sources: List<SourceItem>) {
currentSourceTitle = null
val changingAdapters = migAdapter !is SourceAdapter
if (migAdapter !is SourceAdapter) { if (migAdapter !is SourceAdapter) {
migAdapter = SourceAdapter(this) migAdapter = SourceAdapter(this)
migrationFrameLayout?.onBind(migAdapter!!) migrationFrameLayout?.onBind(migAdapter!!)
migAdapter?.stateRestorationPolicy = RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY migAdapter?.stateRestorationPolicy = RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY
} }
migAdapter?.updateDataSet(sources, true) migAdapter?.updateDataSet(sources, changingAdapters)
controller.updateTitleAndMenu()
} }
fun setMigrationManga(manga: List<MangaItem>?) { override fun setMigrationManga(title: String, manga: List<MangaItem>?) {
currentSourceTitle = title
val changingAdapters = migAdapter !is MangaAdapter
if (migAdapter !is MangaAdapter) { if (migAdapter !is MangaAdapter) {
migAdapter = MangaAdapter(this, presenter.preferences.outlineOnCovers().get()) migAdapter = MangaAdapter(this, presenter.preferences.outlineOnCovers().get())
migrationFrameLayout?.onBind(migAdapter!!) migrationFrameLayout?.onBind(migAdapter!!)
migAdapter?.stateRestorationPolicy = RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY migAdapter?.stateRestorationPolicy = RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY
} }
migAdapter?.updateDataSet(manga, true) migAdapter?.updateDataSet(manga, changingAdapters)
controller.updateTitleAndMenu()
} }
fun drawExtensions() { fun drawExtensions() {

View file

@ -0,0 +1,130 @@
package eu.kanade.tachiyomi.ui.migration
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferenceValues
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.base.presenter.BaseCoroutinePresenter
import eu.kanade.tachiyomi.util.system.executeOnIO
import eu.kanade.tachiyomi.util.system.withUIContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
abstract class BaseMigrationPresenter<T : BaseMigrationInterface>(
protected val sourceManager: SourceManager = Injekt.get(),
protected val db: DatabaseHelper = Injekt.get(),
val preferences: PreferencesHelper = Injekt.get(),
) : BaseCoroutinePresenter<T>() {
private var selectedSource: Pair<String, Long>? = null
var sourceItems = emptyList<SourceItem>()
protected set
var mangaItems = hashMapOf<Long, List<MangaItem>>()
protected set
protected val extensionManager: ExtensionManager by injectLazy()
fun refreshMigrations() {
presenterScope.launch {
val favs = db.getFavoriteMangas().executeOnIO()
sourceItems = findSourcesWithManga(favs)
mangaItems = HashMap(
sourceItems.associate {
it.source.id to libraryToMigrationItem(favs, it.source.id)
},
)
withContext(Dispatchers.Main) {
if (selectedSource != null) {
controller?.setMigrationManga(selectedSource!!.first, mangaItems[selectedSource!!.second])
} else {
controller?.setMigrationSources(sourceItems)
}
}
}
}
private fun findSourcesWithManga(library: List<Manga>): List<SourceItem> {
val header = SelectionHeader()
val sourceGroup = library.groupBy { it.source }
val sortOrder = PreferenceValues.MigrationSourceOrder.fromPreference(preferences)
val extensions = extensionManager.installedExtensions
val obsoleteSources =
extensions.filter { it.isObsolete }.map { it.sources }.flatten().map { it.id }
return sourceGroup
.mapNotNull { if (it.key != LocalSource.ID) sourceManager.getOrStub(it.key) to it.value.size else null }
.sortedWith(
compareBy(
{
when (sortOrder) {
PreferenceValues.MigrationSourceOrder.Alphabetically -> it.first.name
PreferenceValues.MigrationSourceOrder.MostEntries -> Long.MAX_VALUE - it.second
PreferenceValues.MigrationSourceOrder.Obsolete ->
it.first !is SourceManager.StubSource &&
it.first.id !in obsoleteSources
}
},
{ it.first.name },
),
)
.map {
SourceItem(
it.first,
header,
it.second,
it.first is SourceManager.StubSource,
it.first.id in obsoleteSources,
)
}
}
private fun libraryToMigrationItem(library: List<Manga>, sourceId: Long): List<MangaItem> {
return library.filter { it.source == sourceId }.map(::MangaItem)
}
protected suspend fun firstTimeMigration() {
val favs = db.getFavoriteMangas().executeOnIO()
sourceItems = findSourcesWithManga(favs)
mangaItems = HashMap(
sourceItems.associate {
it.source.id to libraryToMigrationItem(
favs,
it.source.id,
)
},
)
withContext(Dispatchers.Main) {
if (selectedSource != null) {
controller?.setMigrationManga(selectedSource!!.first, mangaItems[selectedSource!!.second])
} else {
controller?.setMigrationSources(sourceItems)
}
}
}
fun setSelectedSource(source: Source) {
selectedSource = source.name to source.id
presenterScope.launch {
withUIContext { controller?.setMigrationManga(source.name, mangaItems[source.id]) }
}
}
fun deselectSource() {
selectedSource = null
presenterScope.launch {
withUIContext { controller?.setMigrationSources(sourceItems) }
}
}
}
interface BaseMigrationInterface {
fun setMigrationManga(title: String, manga: List<MangaItem>?)
fun setMigrationSources(sources: List<SourceItem>)
}

View file

@ -1,18 +1,24 @@
package eu.kanade.tachiyomi.ui.migration package eu.kanade.tachiyomi.ui.migration
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View import android.view.View
import androidx.core.view.doOnNextLayout import androidx.core.view.doOnNextLayout
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
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.preference.PreferenceValues
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.MigrationControllerBinding import eu.kanade.tachiyomi.databinding.MigrationControllerBinding
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.BaseCoroutineController
import eu.kanade.tachiyomi.ui.migration.manga.design.PreMigrationController import eu.kanade.tachiyomi.ui.migration.manga.design.PreMigrationController
import eu.kanade.tachiyomi.ui.source.BrowseController
import eu.kanade.tachiyomi.util.system.await import eu.kanade.tachiyomi.util.system.await
import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.system.launchUI
import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.util.view.activityBinding import eu.kanade.tachiyomi.util.view.activityBinding
import eu.kanade.tachiyomi.util.view.scrollViewWith import eu.kanade.tachiyomi.util.view.scrollViewWith
import eu.kanade.tachiyomi.widget.LinearLayoutManagerAccurateOffset import eu.kanade.tachiyomi.widget.LinearLayoutManagerAccurateOffset
@ -23,9 +29,10 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class MigrationController : class MigrationController :
NucleusController<MigrationControllerBinding, MigrationPresenter>(), BaseCoroutineController<MigrationControllerBinding, MigrationPresenter>(),
FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemClickListener,
SourceAdapter.OnAllClickListener { SourceAdapter.OnAllClickListener,
BaseMigrationInterface {
private var adapter: FlexibleAdapter<IFlexible<*>>? = null private var adapter: FlexibleAdapter<IFlexible<*>>? = null
@ -35,9 +42,7 @@ class MigrationController :
setTitle() setTitle()
} }
override fun createPresenter(): MigrationPresenter { override val presenter = MigrationPresenter()
return MigrationPresenter()
}
override fun createBinding(inflater: LayoutInflater) = MigrationControllerBinding.inflate(inflater) override fun createBinding(inflater: LayoutInflater) = MigrationControllerBinding.inflate(inflater)
@ -59,10 +64,10 @@ class MigrationController :
return title return title
} }
override fun canStillGoBack(): Boolean = presenter.state.selectedSource != null override fun canStillGoBack(): Boolean = adapter is MangaAdapter
override fun handleBack(): Boolean { override fun handleBack(): Boolean {
return if (presenter.state.selectedSource != null) { return if (adapter is MangaAdapter) {
presenter.deselectSource() presenter.deselectSource()
true true
} else { } else {
@ -70,27 +75,6 @@ class MigrationController :
} }
} }
fun render(state: ViewState) {
if (state.selectedSource == null) {
title = resources?.getString(R.string.source_migration)
if (adapter !is SourceAdapter) {
adapter = SourceAdapter(this)
binding.migrationRecycler.adapter = adapter
}
adapter?.updateDataSet(state.sourcesWithManga)
} else {
title = state.selectedSource.toString()
if (adapter !is MangaAdapter) {
adapter = MangaAdapter(this, presenter.preferences.outlineOnCovers().get())
binding.migrationRecycler.adapter = adapter
}
adapter?.updateDataSet(state.mangaForSource, true)
activityBinding?.appBar?.doOnNextLayout {
binding.migrationRecycler.requestApplyInsets()
}
}
}
override fun onItemClick(view: View?, position: Int): Boolean { override fun onItemClick(view: View?, position: Int): Boolean {
val item = adapter?.getItem(position) ?: return false val item = adapter?.getItem(position) ?: return false
@ -124,4 +108,56 @@ class MigrationController :
} }
} }
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.migration_main, menu)
menu.findItem(R.id.action_sources_settings).isVisible = false
val id = when (PreferenceValues.MigrationSourceOrder.fromPreference(presenter.preferences)) {
PreferenceValues.MigrationSourceOrder.Alphabetically -> R.id.action_sort_alpha
PreferenceValues.MigrationSourceOrder.MostEntries -> R.id.action_sort_largest
PreferenceValues.MigrationSourceOrder.Obsolete -> R.id.action_sort_obsolete
}
menu.findItem(id).isChecked = true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val sorting = when (item.itemId) {
R.id.action_sort_alpha -> PreferenceValues.MigrationSourceOrder.Alphabetically
R.id.action_sort_largest -> PreferenceValues.MigrationSourceOrder.MostEntries
R.id.action_sort_obsolete -> PreferenceValues.MigrationSourceOrder.Obsolete
else -> null
}
if (sorting != null) {
presenter.preferences.migrationSourceOrder().set(sorting.value)
presenter.refreshMigrations()
item.isChecked = true
}
when (item.itemId) {
R.id.action_migration_guide -> {
activity?.openInBrowser(BrowseController.HELP_URL)
}
}
return super.onOptionsItemSelected(item)
}
override fun setMigrationManga(title: String, manga: List<MangaItem>?) {
this.title = title
if (adapter !is MangaAdapter) {
adapter = MangaAdapter(this, presenter.preferences.outlineOnCovers().get())
binding.migrationRecycler.adapter = adapter
}
adapter?.updateDataSet(manga, true)
activityBinding?.appBar?.doOnNextLayout {
binding.migrationRecycler.requestApplyInsets()
}
}
override fun setMigrationSources(sources: List<SourceItem>) {
title = resources?.getString(R.string.source_migration)
if (adapter !is SourceAdapter) {
adapter = SourceAdapter(this)
binding.migrationRecycler.adapter = adapter
}
adapter?.updateDataSet(sources)
}
} }

View file

@ -1,71 +1,10 @@
package eu.kanade.tachiyomi.ui.migration package eu.kanade.tachiyomi.ui.migration
import android.os.Bundle import kotlinx.coroutines.launch
import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.lang.combineLatest
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MigrationPresenter( class MigrationPresenter : BaseMigrationPresenter<MigrationController>() {
private val sourceManager: SourceManager = Injekt.get(), override fun onCreate() {
private val db: DatabaseHelper = Injekt.get(), super.onCreate()
val preferences: PreferencesHelper = Injekt.get(), presenterScope.launch { firstTimeMigration() }
) : BasePresenter<MigrationController>() {
var state = ViewState()
private set(value) {
field = value
stateRelay.call(value)
}
private val stateRelay = BehaviorRelay.create(state)
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
db.getFavoriteMangas().asRxObservable().observeOn(AndroidSchedulers.mainThread())
.doOnNext { state = state.copy(sourcesWithManga = findSourcesWithManga(it)) }
.combineLatest(
stateRelay.map { it.selectedSource }
.distinctUntilChanged(),
) { library, source -> library to source }
.filter { (_, source) -> source != null }.observeOn(Schedulers.io())
.map { (library, source) -> libraryToMigrationItem(library, source!!.id) }
.observeOn(AndroidSchedulers.mainThread())
.doOnNext { state = state.copy(mangaForSource = it) }.subscribe()
stateRelay
// Render the view when any field other than isReplacingManga changes
.distinctUntilChanged { t1, t2 -> t1.isReplacingManga != t2.isReplacingManga }
.subscribeLatestCache(MigrationController::render)
}
fun setSelectedSource(source: Source) {
state = state.copy(selectedSource = source, mangaForSource = emptyList())
}
fun deselectSource() {
state = state.copy(selectedSource = null, mangaForSource = emptyList())
}
private fun findSourcesWithManga(library: List<Manga>): List<SourceItem> {
val header = SelectionHeader()
return library.asSequence().map { it.source }.toSet()
.mapNotNull { if (it != LocalSource.ID) sourceManager.getOrStub(it) else null }
.sortedBy { it.name }
.map { SourceItem(it, header) }.toList()
}
private fun libraryToMigrationItem(library: List<Manga>, sourceId: Long): List<MangaItem> {
return library.filter { it.source == sourceId }.map(::MangaItem)
} }
} }

View file

@ -29,11 +29,4 @@ class SourceAdapter(val allClickListener: OnAllClickListener) :
interface OnAllClickListener { interface OnAllClickListener {
fun onAllClick(position: Int) fun onAllClick(position: Int)
} }
override fun updateDataSet(items: MutableList<IFlexible<*>>?) {
if (this.items !== items) {
this.items = items
super.updateDataSet(items)
}
}
} }

View file

@ -1,9 +1,15 @@
package eu.kanade.tachiyomi.ui.migration package eu.kanade.tachiyomi.ui.migration
import android.view.View import android.view.View
import androidx.core.text.buildSpannedString
import androidx.core.text.color
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.MigrationCardItemBinding import eu.kanade.tachiyomi.databinding.MigrationCardItemBinding
import eu.kanade.tachiyomi.source.icon import eu.kanade.tachiyomi.source.icon
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.lang.withColor
import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.getResourceColor
import java.util.Locale import java.util.Locale
class SourceHolder(view: View, val adapter: SourceAdapter) : class SourceHolder(view: View, val adapter: SourceAdapter) :
@ -23,8 +29,20 @@ class SourceHolder(view: View, val adapter: SourceAdapter) :
val sourceName = val sourceName =
if (adapter.isMultiLanguage) source.toString() else source.name.replaceFirstChar { if (adapter.isMultiLanguage) source.toString() else source.name.replaceFirstChar {
it.titlecase(Locale.getDefault()) it.titlecase(Locale.getDefault())
} } + " (${item.numberOfItems})"
binding.title.text = sourceName binding.title.text = sourceName
binding.lang.text = when {
item.isUninstalled -> itemView.context.getString(R.string.source_not_installed)
.withColor(itemView.context.getResourceColor(R.attr.colorError))
item.isObsolete -> buildSpannedString {
append(LocaleHelper.getSourceDisplayName(source.lang, itemView.context))
append(" ")
color(itemView.context.getResourceColor(R.attr.colorError)) {
append(itemView.context.getString(R.string.obsolete).uppercase())
}
}
else -> LocaleHelper.getSourceDisplayName(source.lang, itemView.context)
}
// Set circle letter image. // Set circle letter image.
itemView.post { itemView.post {

View file

@ -14,7 +14,13 @@ import eu.kanade.tachiyomi.source.Source
* @param source Instance of [Source] containing source information. * @param source Instance of [Source] containing source information.
* @param header The header for this item. * @param header The header for this item.
*/ */
data class SourceItem(val source: Source, val header: SelectionHeader? = null) : data class SourceItem(
val source: Source,
val header: SelectionHeader? = null,
val numberOfItems: Int,
val isUninstalled: Boolean,
val isObsolete: Boolean,
) :
AbstractSectionableItem<SourceHolder, SelectionHeader>(header) { AbstractSectionableItem<SourceHolder, SelectionHeader>(header) {
/** /**

View file

@ -1,10 +0,0 @@
package eu.kanade.tachiyomi.ui.migration
import eu.kanade.tachiyomi.source.Source
data class ViewState(
val selectedSource: Source? = null,
val mangaForSource: List<MangaItem> = emptyList(),
val sourcesWithManga: List<SourceItem> = emptyList(),
val isReplacingManga: Boolean = false,
)

View file

@ -27,6 +27,7 @@ import com.google.android.material.snackbar.Snackbar
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferenceValues
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.BrowseControllerBinding import eu.kanade.tachiyomi.databinding.BrowseControllerBinding
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
@ -238,10 +239,12 @@ class BrowseController :
private fun updateSheetMenu() { private fun updateSheetMenu() {
binding.bottomSheet.sheetToolbar.title = binding.bottomSheet.sheetToolbar.title =
view?.context?.getString( if (binding.bottomSheet.tabs.selectedTabPosition != 0) {
if (binding.bottomSheet.tabs.selectedTabPosition == 0) R.string.extensions binding.bottomSheet.root.currentSourceTitle
else R.string.source_migration, ?: view?.context?.getString(R.string.source_migration)
) } else {
view?.context?.getString(R.string.extensions)
}
val onExtensionTab = binding.bottomSheet.tabs.selectedTabPosition == 0 val onExtensionTab = binding.bottomSheet.tabs.selectedTabPosition == 0
if (binding.bottomSheet.sheetToolbar.menu.findItem(if (onExtensionTab) R.id.action_search else R.id.action_migration_guide) != null) { if (binding.bottomSheet.sheetToolbar.menu.findItem(if (onExtensionTab) R.id.action_search else R.id.action_migration_guide) != null) {
return return
@ -254,6 +257,13 @@ class BrowseController :
else R.menu.migration_main, else R.menu.migration_main,
) )
val id = when (PreferenceValues.MigrationSourceOrder.fromPreference(preferences)) {
PreferenceValues.MigrationSourceOrder.Alphabetically -> R.id.action_sort_alpha
PreferenceValues.MigrationSourceOrder.MostEntries -> R.id.action_sort_largest
PreferenceValues.MigrationSourceOrder.Obsolete -> R.id.action_sort_obsolete
}
binding.bottomSheet.sheetToolbar.menu.findItem(id)?.isChecked = true
// Initialize search option. // Initialize search option.
binding.bottomSheet.sheetToolbar.menu.findItem(R.id.action_search)?.let { searchItem -> binding.bottomSheet.sheetToolbar.menu.findItem(R.id.action_search)?.let { searchItem ->
val searchView = searchItem.actionView as SearchView val searchView = searchItem.actionView as SearchView
@ -279,6 +289,18 @@ class BrowseController :
private fun setSheetToolbar() { private fun setSheetToolbar() {
binding.bottomSheet.sheetToolbar.setOnMenuItemClickListener { item -> binding.bottomSheet.sheetToolbar.setOnMenuItemClickListener { item ->
val sorting = when (item.itemId) {
R.id.action_sort_alpha -> PreferenceValues.MigrationSourceOrder.Alphabetically
R.id.action_sort_largest -> PreferenceValues.MigrationSourceOrder.MostEntries
R.id.action_sort_obsolete -> PreferenceValues.MigrationSourceOrder.Obsolete
else -> null
}
if (sorting != null) {
preferences.migrationSourceOrder().set(sorting.value)
binding.bottomSheet.root.presenter.refreshMigrations()
item.isChecked = true
return@setOnMenuItemClickListener true
}
when (item.itemId) { when (item.itemId) {
// Initialize option to open catalogue settings. // Initialize option to open catalogue settings.
R.id.action_filter -> { R.id.action_filter -> {

View file

@ -152,6 +152,9 @@ fun String.indexesOf(substr: String, ignoreCase: Boolean = true): List<Int> {
} }
} }
fun String.withColor(@ColorInt colorInt: Int) =
buildSpannedString { color(colorInt) { append(this@withColor) } }
fun String.withSubtitle(context: Context, @StringRes subtitleRes: Int) = fun String.withSubtitle(context: Context, @StringRes subtitleRes: Int) =
withSubtitle(context, context.getString(subtitleRes)) withSubtitle(context, context.getString(subtitleRes))

View file

@ -32,13 +32,31 @@
android:paddingStart="0dp" android:paddingStart="0dp"
android:paddingEnd="8dp" android:paddingEnd="8dp"
android:ellipsize="end" android:ellipsize="end"
style="?textAppearanceBodyLarge" style="?textAppearanceBodyMedium"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintBottom_toTopOf="@id/lang"
app:layout_constraintStart_toEndOf="@id/source_image" app:layout_constraintStart_toEndOf="@id/source_image"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@+id/migration_all" app:layout_constraintEnd_toStartOf="@+id/migration_all"
tools:text="Source title"/> tools:text="Source title"/>
<TextView
android:id="@+id/lang"
style="?textAppearanceBodySmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:layout_marginEnd="4dp"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintVertical_bias="0.0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintStart_toStartOf="@id/title"
app:layout_constraintEnd_toEndOf="@id/title"
tools:text="English"
tools:visibility="visible" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/migration_all" android:id="@+id/migration_all"
style="@style/Widget.Tachiyomi.Button.Small" style="@style/Widget.Tachiyomi.Button.Small"

View file

@ -1,6 +1,27 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu 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">
<item
android:id="@+id/action_migration_sort"
android:icon="@drawable/ic_sort_24dp"
android:title="@string/sort_by"
app:showAsAction="ifRoom" >
<menu>
<group android:checkableBehavior="single"
android:id="@+id/action_sort_group">
<item
android:id="@+id/action_sort_alpha"
android:title="@string/alphabetically"/>
<item
android:id="@+id/action_sort_largest"
android:title="@string/most_entries"/>
<item
android:id="@+id/action_sort_obsolete"
android:title="@string/obsolete"/>
</group>
</menu>
</item>
<item <item
android:id="@+id/action_sources_settings" android:id="@+id/action_sources_settings"
android:icon="@drawable/ic_tune_24dp" android:icon="@drawable/ic_tune_24dp"