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
import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R
// Library
@ -25,4 +26,16 @@ object PreferenceValues {
LOW(R.string.pref_low, 31),
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 migrationSourceOrder() = flowPrefs.getInt("migration_source_order", Values.MigrationSourceOrder.Alphabetically.value)
fun collapsedCategories() = flowPrefs.getStringSet("collapsed_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 androidx.core.content.ContextCompat
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.ExtensionManager
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.extension.model.InstalledExtensionsOrder
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.ui.migration.MangaItem
import eu.kanade.tachiyomi.ui.migration.SelectionHeader
import eu.kanade.tachiyomi.ui.migration.SourceItem
import eu.kanade.tachiyomi.ui.migration.BaseMigrationPresenter
import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.executeOnIO
import eu.kanade.tachiyomi.util.system.withUIContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
@ -28,7 +18,6 @@ import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
typealias ExtensionTuple =
@ -38,26 +27,13 @@ typealias ExtensionIntallInfo = Pair<InstallStep, PackageInstaller.SessionInfo?>
/**
* Presenter of [ExtensionBottomSheet].
*/
class ExtensionBottomPresenter(
private val extensionManager: ExtensionManager = Injekt.get(),
val preferences: PreferencesHelper = Injekt.get(),
) : BaseCoroutinePresenter<ExtensionBottomSheet>() {
class ExtensionBottomPresenter() : BaseMigrationPresenter<ExtensionBottomSheet>() {
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 val sourceManager: SourceManager = Injekt.get()
private var selectedSource: Long? = null
private var firstLoad = true
private val db: DatabaseHelper = Injekt.get()
override fun onCreate() {
super.onCreate()
@ -73,25 +49,7 @@ class ExtensionBottomPresenter(
)
withContext(Dispatchers.Main) { controller?.setExtensions(extensions, false) }
}
val migrationJob = async {
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)
}
}
}
val migrationJob = async { firstTimeMigration() }
listOf(migrationJob, extensionJob).awaitAll()
}
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() {
presenterScope.launch {
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
private fun toItems(tuple: ExtensionTuple): List<ExtensionItem> {
val context = controller?.context ?: return emptyList()
@ -355,18 +282,4 @@ class ExtensionBottomPresenter(
fun trustSignature(signatureHash: String) {
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.InstalledExtensionsOrder
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.MangaItem
import eu.kanade.tachiyomi.ui.migration.SourceAdapter
@ -46,7 +47,8 @@ class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: At
FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener,
ExtensionTrustDialog.Listener,
SourceAdapter.OnAllClickListener {
SourceAdapter.OnAllClickListener,
BaseMigrationInterface {
var sheetBehavior: BottomSheetBehavior<*>? = null
@ -62,6 +64,7 @@ class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: At
get() = listOf(extAdapter, migAdapter)
val presenter = ExtensionBottomPresenter()
var currentSourceTitle: String? = null
private var extensions: List<ExtensionItem> = emptyList()
var canExpand = false
@ -323,22 +326,28 @@ class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: At
drawExtensions()
}
fun setMigrationSources(sources: List<SourceItem>) {
override fun setMigrationSources(sources: List<SourceItem>) {
currentSourceTitle = null
val changingAdapters = migAdapter !is SourceAdapter
if (migAdapter !is SourceAdapter) {
migAdapter = SourceAdapter(this)
migrationFrameLayout?.onBind(migAdapter!!)
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) {
migAdapter = MangaAdapter(this, presenter.preferences.outlineOnCovers().get())
migrationFrameLayout?.onBind(migAdapter!!)
migAdapter?.stateRestorationPolicy = RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY
}
migAdapter?.updateDataSet(manga, true)
migAdapter?.updateDataSet(manga, changingAdapters)
controller.updateTitleAndMenu()
}
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
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.core.view.doOnNextLayout
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
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.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.source.BrowseController
import eu.kanade.tachiyomi.util.system.await
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.scrollViewWith
import eu.kanade.tachiyomi.widget.LinearLayoutManagerAccurateOffset
@ -23,9 +29,10 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MigrationController :
NucleusController<MigrationControllerBinding, MigrationPresenter>(),
BaseCoroutineController<MigrationControllerBinding, MigrationPresenter>(),
FlexibleAdapter.OnItemClickListener,
SourceAdapter.OnAllClickListener {
SourceAdapter.OnAllClickListener,
BaseMigrationInterface {
private var adapter: FlexibleAdapter<IFlexible<*>>? = null
@ -35,9 +42,7 @@ class MigrationController :
setTitle()
}
override fun createPresenter(): MigrationPresenter {
return MigrationPresenter()
}
override val presenter = MigrationPresenter()
override fun createBinding(inflater: LayoutInflater) = MigrationControllerBinding.inflate(inflater)
@ -59,10 +64,10 @@ class MigrationController :
return title
}
override fun canStillGoBack(): Boolean = presenter.state.selectedSource != null
override fun canStillGoBack(): Boolean = adapter is MangaAdapter
override fun handleBack(): Boolean {
return if (presenter.state.selectedSource != null) {
return if (adapter is MangaAdapter) {
presenter.deselectSource()
true
} 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 {
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
import android.os.Bundle
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
import kotlinx.coroutines.launch
class MigrationPresenter(
private val sourceManager: SourceManager = Injekt.get(),
private val db: DatabaseHelper = Injekt.get(),
val preferences: PreferencesHelper = Injekt.get(),
) : 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)
class MigrationPresenter : BaseMigrationPresenter<MigrationController>() {
override fun onCreate() {
super.onCreate()
presenterScope.launch { firstTimeMigration() }
}
}

View file

@ -29,11 +29,4 @@ class SourceAdapter(val allClickListener: OnAllClickListener) :
interface OnAllClickListener {
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
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.source.icon
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
class SourceHolder(view: View, val adapter: SourceAdapter) :
@ -23,8 +29,20 @@ class SourceHolder(view: View, val adapter: SourceAdapter) :
val sourceName =
if (adapter.isMultiLanguage) source.toString() else source.name.replaceFirstChar {
it.titlecase(Locale.getDefault())
}
} + " (${item.numberOfItems})"
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.
itemView.post {

View file

@ -14,7 +14,13 @@ import eu.kanade.tachiyomi.source.Source
* @param source Instance of [Source] containing source information.
* @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) {
/**

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.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferenceValues
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.BrowseControllerBinding
import eu.kanade.tachiyomi.source.CatalogueSource
@ -238,10 +239,12 @@ class BrowseController :
private fun updateSheetMenu() {
binding.bottomSheet.sheetToolbar.title =
view?.context?.getString(
if (binding.bottomSheet.tabs.selectedTabPosition == 0) R.string.extensions
else R.string.source_migration,
)
if (binding.bottomSheet.tabs.selectedTabPosition != 0) {
binding.bottomSheet.root.currentSourceTitle
?: view?.context?.getString(R.string.source_migration)
} else {
view?.context?.getString(R.string.extensions)
}
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) {
return
@ -254,6 +257,13 @@ class BrowseController :
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.
binding.bottomSheet.sheetToolbar.menu.findItem(R.id.action_search)?.let { searchItem ->
val searchView = searchItem.actionView as SearchView
@ -279,6 +289,18 @@ class BrowseController :
private fun setSheetToolbar() {
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) {
// Initialize option to open catalogue settings.
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) =
withSubtitle(context, context.getString(subtitleRes))

View file

@ -32,13 +32,31 @@
android:paddingStart="0dp"
android:paddingEnd="8dp"
android:ellipsize="end"
style="?textAppearanceBodyLarge"
app:layout_constraintBottom_toBottomOf="parent"
style="?textAppearanceBodyMedium"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintBottom_toTopOf="@id/lang"
app:layout_constraintStart_toEndOf="@id/source_image"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@+id/migration_all"
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
android:id="@+id/migration_all"
style="@style/Widget.Tachiyomi.Button.Small"

View file

@ -1,6 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
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
android:id="@+id/action_sources_settings"
android:icon="@drawable/ic_tune_24dp"