diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d69ab462b..97d9cefbb 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -221,6 +221,9 @@
+
+ @Query("SELECT * FROM sources WHERE enabled = 0 ORDER BY sort_key")
+ abstract suspend fun findAllDisabled(): List
+
@Query("SELECT * FROM sources WHERE enabled = 1 ORDER BY sort_key")
abstract fun observeEnabled(): Flow>
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/MangaSource.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/MangaSource.kt
index 578773a3c..5ca61685b 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/MangaSource.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/MangaSource.kt
@@ -1,5 +1,8 @@
package org.koitharu.kotatsu.core.model
+import android.content.Context
+import androidx.annotation.StringRes
+import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.toTitleCase
@@ -18,3 +21,18 @@ fun MangaSource(name: String): MangaSource {
}
fun MangaSource.isNsfw() = contentType == ContentType.HENTAI
+
+@get:StringRes
+val ContentType.titleResId
+ get() = when (this) {
+ ContentType.MANGA -> R.string.content_type_manga
+ ContentType.HENTAI -> R.string.content_type_hentai
+ ContentType.COMICS -> R.string.content_type_comics
+ ContentType.OTHER -> R.string.content_type_other
+ }
+
+fun MangaSource.getSummary(context: Context): String {
+ val type = context.getString(contentType.titleResId)
+ val locale = getLocaleTitle() ?: context.getString(R.string.various_languages)
+ return context.getString(R.string.source_summary_pattern, type, locale)
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/LocaleComparator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/LocaleComparator.kt
new file mode 100644
index 000000000..a92b50445
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/LocaleComparator.kt
@@ -0,0 +1,26 @@
+package org.koitharu.kotatsu.core.util
+
+import androidx.core.os.LocaleListCompat
+import org.koitharu.kotatsu.core.util.ext.map
+import java.util.Locale
+
+class LocaleComparator : Comparator {
+
+ private val deviceLocales = LocaleListCompat.getAdjustedDefault()//LocaleManagerCompat.getSystemLocales(context)
+ .map { it.language }
+ .distinct()
+
+ override fun compare(a: Locale?, b: Locale?): Int {
+ return if (a === b) {
+ 0
+ } else {
+ val indexA = if (a == null) -1 else deviceLocales.indexOf(a.language)
+ val indexB = if (b == null) -1 else deviceLocales.indexOf(b.language)
+ if (indexA < 0 && indexB < 0) {
+ compareValues(a?.language, b?.language)
+ } else {
+ -2 - (indexA - indexB)
+ }
+ }
+ }
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/MangaSourcesRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/MangaSourcesRepository.kt
index f2780b6ab..9ba481839 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/MangaSourcesRepository.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/MangaSourcesRepository.kt
@@ -46,6 +46,10 @@ class MangaSourcesRepository @Inject constructor(
return dao.findAllEnabled().toSources(settings.isNsfwContentDisabled)
}
+ suspend fun getDisabledSources(): List {
+ return dao.findAllDisabled().toSources(settings.isNsfwContentDisabled)
+ }
+
fun observeEnabledSources(): Flow> = observeIsNsfwDisabled().flatMapLatest { skipNsfw ->
dao.observeEnabled().map {
it.toSources(skipNsfw)
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/adapter/ExploreAdapterDelegates.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/adapter/ExploreAdapterDelegates.kt
index 83d40985d..ec68c5823 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/adapter/ExploreAdapterDelegates.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/adapter/ExploreAdapterDelegates.kt
@@ -7,6 +7,7 @@ import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.core.model.getSummary
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
@@ -48,8 +49,8 @@ fun exploreButtonsAD(
icon.setColorSchemeColors(
context.getThemeColor(
materialR.attr.colorPrimary,
- Color.DKGRAY
- )
+ Color.DKGRAY,
+ ),
)
binding.buttonRandom.icon = icon
icon.start()
@@ -98,7 +99,7 @@ fun exploreSourceListItemAD(
ItemExploreSourceListBinding.inflate(
layoutInflater,
parent,
- false
+ false,
)
},
on = { item, _, _ -> item is MangaSourceItem && !item.isGrid },
@@ -112,6 +113,7 @@ fun exploreSourceListItemAD(
bind {
binding.textViewTitle.text = item.source.title
+ binding.textViewSubtitle.text = item.source.getSummary(context)
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
fallback(fallbackIcon)
@@ -132,7 +134,7 @@ fun exploreSourceGridItemAD(
ItemExploreSourceGridBinding.inflate(
layoutInflater,
parent,
- false
+ false,
)
},
on = { item, _, _ -> item is MangaSourceItem && item.isGrid },
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionSourceAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionSourceAD.kt
index 8655239b4..5b22c96c5 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionSourceAD.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionSourceAD.kt
@@ -5,6 +5,7 @@ import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.core.model.getSummary
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
import org.koitharu.kotatsu.core.util.ext.enqueueWith
@@ -40,6 +41,7 @@ fun searchSuggestionSourceAD(
} else {
item.source.title
}
+ binding.textViewSubtitle.text = item.source.getSummary(context)
binding.switchLocal.isChecked = item.isEnabled
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
binding.imageViewCover.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt
index 688384c99..202a5b371 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt
@@ -1,6 +1,5 @@
package org.koitharu.kotatsu.settings
-import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri
@@ -9,7 +8,6 @@ import android.os.Bundle
import android.provider.Settings
import android.view.View
import androidx.appcompat.app.AppCompatDelegate
-import androidx.core.app.LocaleManagerCompat
import androidx.preference.ListPreference
import androidx.preference.Preference
import dagger.hilt.android.AndroidEntryPoint
@@ -18,8 +16,8 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.ui.util.ActivityRecreationHandle
+import org.koitharu.kotatsu.core.util.LocaleComparator
import org.koitharu.kotatsu.core.util.ext.getLocalesConfig
-import org.koitharu.kotatsu.core.util.ext.map
import org.koitharu.kotatsu.core.util.ext.postDelayed
import org.koitharu.kotatsu.core.util.ext.setDefaultValueCompat
import org.koitharu.kotatsu.core.util.ext.toList
@@ -27,7 +25,6 @@ import org.koitharu.kotatsu.parsers.util.names
import org.koitharu.kotatsu.parsers.util.toTitleCase
import org.koitharu.kotatsu.settings.utils.ActivityListPreference
import org.koitharu.kotatsu.settings.utils.SliderPreference
-import java.util.Locale
import javax.inject.Inject
@AndroidEntryPoint
@@ -110,7 +107,7 @@ class AppearanceSettingsFragment :
private fun initLocalePicker(preference: ListPreference) {
val locales = preference.context.getLocalesConfig()
.toList()
- .sortedWith(LocaleComparator(preference.context))
+ .sortedWith(LocaleComparator())
preference.entries = Array(locales.size + 1) { i ->
if (i == 0) {
getString(R.string.automatic)
@@ -134,25 +131,4 @@ class AppearanceSettingsFragment :
getString(it.title)
}
}
-
- private class LocaleComparator(context: Context) : Comparator {
-
- private val deviceLocales = LocaleManagerCompat.getSystemLocales(context)
- .map { it.language }
- .distinct()
-
- override fun compare(a: Locale, b: Locale): Int {
- return if (a === b) {
- 0
- } else {
- val indexA = deviceLocales.indexOf(a.language)
- val indexB = deviceLocales.indexOf(b.language)
- if (indexA == -1 && indexB == -1) {
- compareValues(a.language, b.language)
- } else {
- -2 - (indexA - indexB)
- }
- }
- }
- }
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/NewSourcesDialogFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/NewSourcesDialogFragment.kt
index 99443f6cc..455663d66 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/NewSourcesDialogFragment.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/NewSourcesDialogFragment.kt
@@ -65,8 +65,6 @@ class NewSourcesDialogFragment :
viewModel.onItemEnabledChanged(item, isEnabled)
}
- override fun onHeaderClick(header: SourceConfigItem.LocaleGroup) = Unit
-
override fun onCloseTip(tip: SourceConfigItem.Tip) = Unit
companion object {
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/NewSourcesViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/NewSourcesViewModel.kt
index f9ca6be3c..421cd2fee 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/NewSourcesViewModel.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/NewSourcesViewModel.kt
@@ -8,7 +8,6 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
-import org.koitharu.kotatsu.core.model.getLocaleTitle
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
@@ -35,7 +34,6 @@ class NewSourcesViewModel @Inject constructor(
SourceConfigItem.SourceItem(
source = source,
isEnabled = enabled,
- summary = source.getLocaleTitle(),
isDraggable = false,
isAvailable = !skipNsfw || source.contentType != ContentType.HENTAI,
)
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesListProducer.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesListProducer.kt
index 82181357f..6dad709e8 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesListProducer.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesListProducer.kt
@@ -1,6 +1,5 @@
package org.koitharu.kotatsu.settings.sources
-import androidx.core.os.LocaleListCompat
import androidx.room.InvalidationTracker
import dagger.hilt.android.ViewModelLifecycle
import dagger.hilt.android.scopes.ViewModelScoped
@@ -15,19 +14,12 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.db.TABLE_SOURCES
-import org.koitharu.kotatsu.core.model.getLocaleTitle
import org.koitharu.kotatsu.core.model.isNsfw
import org.koitharu.kotatsu.core.prefs.AppSettings
-import org.koitharu.kotatsu.core.util.AlphanumComparator
import org.koitharu.kotatsu.core.util.ext.lifecycleScope
-import org.koitharu.kotatsu.core.util.ext.map
import org.koitharu.kotatsu.core.util.ext.toEnumSet
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
-import org.koitharu.kotatsu.parsers.model.MangaSource
-import org.koitharu.kotatsu.parsers.util.toTitleCase
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
-import java.util.Locale
-import java.util.TreeMap
import javax.inject.Inject
@ViewModelScoped
@@ -39,7 +31,6 @@ class SourcesListProducer @Inject constructor(
private val scope = lifecycle.lifecycleScope
private var query: String = ""
- private val expanded = HashSet()
val list = MutableStateFlow(emptyList())
private var job = scope.launch(Dispatchers.Default) {
@@ -67,27 +58,18 @@ class SourcesListProducer @Inject constructor(
onInvalidated(emptySet())
}
- fun expandCollapse(group: String?) {
- if (!expanded.remove(group)) {
- expanded.add(group)
- }
- onInvalidated(emptySet())
- }
-
private suspend fun buildList(): List {
- val allSources = repository.allMangaSources
val enabledSources = repository.getEnabledSources()
val isNsfwDisabled = settings.isNsfwContentDisabled
val withTip = settings.isTipEnabled(TIP_REORDER)
val enabledSet = enabledSources.toEnumSet()
if (query.isNotEmpty()) {
- return allSources.mapNotNull {
+ return enabledSources.mapNotNull {
if (!it.title.contains(query, ignoreCase = true)) {
return@mapNotNull null
}
SourceConfigItem.SourceItem(
source = it,
- summary = it.getLocaleTitle(),
isEnabled = it in enabledSet,
isDraggable = false,
isAvailable = !isNsfwDisabled || !it.isNsfw(),
@@ -96,17 +78,8 @@ class SourcesListProducer @Inject constructor(
listOf(SourceConfigItem.EmptySearchResult)
}
}
- val map = allSources.groupByTo(TreeMap(LocaleKeyComparator())) {
- if (it in enabledSet) {
- KEY_ENABLED
- } else {
- it.locale
- }
- }
- map.remove(KEY_ENABLED)
- val result = ArrayList(allSources.size + map.size + 2)
+ val result = ArrayList(enabledSources.size + 1)
if (enabledSources.isNotEmpty()) {
- result += SourceConfigItem.Header(R.string.enabled_sources)
if (withTip) {
result += SourceConfigItem.Tip(
TIP_REORDER,
@@ -117,70 +90,17 @@ class SourcesListProducer @Inject constructor(
enabledSources.mapTo(result) {
SourceConfigItem.SourceItem(
source = it,
- summary = it.getLocaleTitle(),
isEnabled = true,
isDraggable = true,
isAvailable = false,
)
}
}
- if (enabledSources.size != allSources.size) {
- result += SourceConfigItem.Header(R.string.available_sources)
- val comparator = compareBy(AlphanumComparator()) { it.name }
- for ((key, list) in map) {
- list.sortWith(comparator)
- val isExpanded = key in expanded
- result += SourceConfigItem.LocaleGroup(
- localeId = key,
- title = getLocaleTitle(key),
- isExpanded = isExpanded,
- )
- if (isExpanded) {
- list.mapTo(result) {
- SourceConfigItem.SourceItem(
- source = it,
- summary = null,
- isEnabled = false,
- isDraggable = false,
- isAvailable = !isNsfwDisabled || !it.isNsfw(),
- )
- }
- }
- }
- }
return result
}
- private class LocaleKeyComparator : Comparator {
-
- private val deviceLocales = LocaleListCompat.getAdjustedDefault()
- .map { it.language }
-
- override fun compare(a: String?, b: String?): Int {
- when {
- a == b -> return 0
- a == null -> return 1
- b == null -> return -1
- }
- val ai = deviceLocales.indexOf(a!!)
- val bi = deviceLocales.indexOf(b!!)
- return when {
- ai < 0 && bi < 0 -> a.compareTo(b)
- ai < 0 -> 1
- bi < 0 -> -1
- else -> ai.compareTo(bi)
- }
- }
- }
-
companion object {
- private fun getLocaleTitle(localeKey: String?): String? {
- val locale = Locale(localeKey ?: return null)
- return locale.getDisplayLanguage(locale).toTitleCase(locale)
- }
-
- private const val KEY_ENABLED = "!"
const val TIP_REORDER = "src_reorder"
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesManageFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesManageFragment.kt
index 30053898b..9a7cb22d5 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesManageFragment.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesManageFragment.kt
@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.settings.sources
+import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
@@ -32,6 +33,7 @@ import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigAdapter
import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigListener
+import org.koitharu.kotatsu.settings.sources.catalog.SourcesCatalogActivity
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
import javax.inject.Inject
@@ -77,7 +79,7 @@ class SourcesManageFragment :
viewModel.content.observe(viewLifecycleOwner, sourcesAdapter)
viewModel.onActionDone.observeEvent(
viewLifecycleOwner,
- ReversibleActionObserver(binding.recyclerView)
+ ReversibleActionObserver(binding.recyclerView),
)
addMenuProvider(SourcesMenuProvider())
}
@@ -119,10 +121,6 @@ class SourcesManageFragment :
viewModel.setEnabled(item.source, isEnabled)
}
- override fun onHeaderClick(header: SourceConfigItem.LocaleGroup) {
- viewModel.expandOrCollapse(header.localeId)
- }
-
override fun onCloseTip(tip: SourceConfigItem.Tip) {
viewModel.onTipClosed(tip)
}
@@ -143,6 +141,11 @@ class SourcesManageFragment :
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
+ R.id.action_catalog -> {
+ startActivity(Intent(context, SourcesCatalogActivity::class.java))
+ true
+ }
+
R.id.action_disable_all -> {
viewModel.disableAll()
true
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesManageViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesManageViewModel.kt
index 58a9f6eea..9ca2654b2 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesManageViewModel.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesManageViewModel.kt
@@ -103,10 +103,6 @@ class SourcesManageViewModel @Inject constructor(
}
}
- fun expandOrCollapse(headerId: String?) {
- listProducer.expandCollapse(headerId)
- }
-
fun performSearch(query: String?) {
listProducer.setQuery(query?.trim().orEmpty())
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapter.kt
index da45d8fe3..5f7505fad 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapter.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapter.kt
@@ -13,8 +13,6 @@ class SourceConfigAdapter(
init {
with(delegatesManager) {
- addDelegate(sourceConfigHeaderDelegate())
- addDelegate(sourceConfigGroupDelegate(listener))
addDelegate(sourceConfigItemDelegate2(listener, coil, lifecycleOwner))
addDelegate(sourceConfigEmptySearchDelegate())
addDelegate(sourceConfigTipDelegate(listener))
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt
index f727175a9..e3b055b40 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt
@@ -18,6 +18,7 @@ import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.core.model.getSummary
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
import org.koitharu.kotatsu.core.ui.list.OnTipCloseListener
@@ -26,144 +27,104 @@ import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
-import org.koitharu.kotatsu.core.util.ext.textAndVisible
-import org.koitharu.kotatsu.databinding.ItemExpandableBinding
-import org.koitharu.kotatsu.databinding.ItemFilterHeaderBinding
import org.koitharu.kotatsu.databinding.ItemSourceConfigBinding
import org.koitharu.kotatsu.databinding.ItemSourceConfigCheckableBinding
import org.koitharu.kotatsu.databinding.ItemTipBinding
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
-fun sourceConfigHeaderDelegate() =
- adapterDelegateViewBinding(
- { layoutInflater, parent ->
- ItemFilterHeaderBinding.inflate(
- layoutInflater,
- parent,
- false,
- )
- },
- ) {
-
- bind {
- binding.textViewTitle.setText(item.titleResId)
- }
- }
-
-fun sourceConfigGroupDelegate(
- listener: SourceConfigListener,
-) =
- adapterDelegateViewBinding(
- { layoutInflater, parent -> ItemExpandableBinding.inflate(layoutInflater, parent, false) },
- ) {
-
- binding.root.setOnClickListener {
- listener.onHeaderClick(item)
- }
-
- bind {
- binding.root.text = item.title ?: getString(R.string.various_languages)
- binding.root.isChecked = item.isExpanded
- }
- }
-
fun sourceConfigItemCheckableDelegate(
listener: SourceConfigListener,
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
-) =
- adapterDelegateViewBinding(
- { layoutInflater, parent ->
- ItemSourceConfigCheckableBinding.inflate(
- layoutInflater,
- parent,
- false,
- )
- },
- ) {
+) = adapterDelegateViewBinding(
+ { layoutInflater, parent ->
+ ItemSourceConfigCheckableBinding.inflate(
+ layoutInflater,
+ parent,
+ false,
+ )
+ },
+) {
- binding.switchToggle.setOnCheckedChangeListener { _, isChecked ->
- listener.onItemEnabledChanged(item, isChecked)
- }
+ binding.switchToggle.setOnCheckedChangeListener { _, isChecked ->
+ listener.onItemEnabledChanged(item, isChecked)
+ }
- bind {
- binding.textViewTitle.text = if (item.isNsfw) {
- buildSpannedString {
- append(item.source.title)
- append(' ')
- appendNsfwLabel(context)
- }
- } else {
- item.source.title
- }
- binding.switchToggle.isChecked = item.isEnabled
- binding.switchToggle.isEnabled = item.isAvailable
- binding.textViewDescription.textAndVisible = item.summary
- val fallbackIcon =
- FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
- binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
- crossfade(context)
- error(fallbackIcon)
- placeholder(fallbackIcon)
- fallback(fallbackIcon)
- source(item.source)
- enqueueWith(coil)
+ bind {
+ binding.textViewTitle.text = if (item.isNsfw) {
+ buildSpannedString {
+ append(item.source.title)
+ append(' ')
+ appendNsfwLabel(context)
}
+ } else {
+ item.source.title
+ }
+ binding.switchToggle.isChecked = item.isEnabled
+ binding.switchToggle.isEnabled = item.isAvailable
+ binding.textViewDescription.text = item.source.getSummary(context)
+ val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
+ binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
+ crossfade(context)
+ error(fallbackIcon)
+ placeholder(fallbackIcon)
+ fallback(fallbackIcon)
+ source(item.source)
+ enqueueWith(coil)
}
}
+}
fun sourceConfigItemDelegate2(
listener: SourceConfigListener,
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
-) =
- adapterDelegateViewBinding(
- { layoutInflater, parent ->
- ItemSourceConfigBinding.inflate(
- layoutInflater,
- parent,
- false,
- )
- },
- ) {
+) = adapterDelegateViewBinding(
+ { layoutInflater, parent ->
+ ItemSourceConfigBinding.inflate(
+ layoutInflater,
+ parent,
+ false,
+ )
+ },
+) {
- val eventListener = View.OnClickListener { v ->
- when (v.id) {
- R.id.imageView_add -> listener.onItemEnabledChanged(item, true)
- R.id.imageView_remove -> listener.onItemEnabledChanged(item, false)
- R.id.imageView_menu -> showSourceMenu(v, item, listener)
- }
+ val eventListener = View.OnClickListener { v ->
+ when (v.id) {
+ R.id.imageView_add -> listener.onItemEnabledChanged(item, true)
+ R.id.imageView_remove -> listener.onItemEnabledChanged(item, false)
+ R.id.imageView_menu -> showSourceMenu(v, item, listener)
}
- binding.imageViewRemove.setOnClickListener(eventListener)
- binding.imageViewAdd.setOnClickListener(eventListener)
- binding.imageViewMenu.setOnClickListener(eventListener)
+ }
+ binding.imageViewRemove.setOnClickListener(eventListener)
+ binding.imageViewAdd.setOnClickListener(eventListener)
+ binding.imageViewMenu.setOnClickListener(eventListener)
- bind {
- binding.textViewTitle.text = if (item.isNsfw) {
- buildSpannedString {
- append(item.source.title)
- append(' ')
- appendNsfwLabel(context)
- }
- } else {
- item.source.title
- }
- binding.imageViewAdd.isGone = item.isEnabled || !item.isAvailable
- binding.imageViewRemove.isVisible = item.isEnabled
- binding.imageViewMenu.isVisible = item.isEnabled
- binding.textViewDescription.textAndVisible = item.summary
- val fallbackIcon =
- FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
- binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
- crossfade(context)
- error(fallbackIcon)
- placeholder(fallbackIcon)
- fallback(fallbackIcon)
- source(item.source)
- enqueueWith(coil)
+ bind {
+ binding.textViewTitle.text = if (item.isNsfw) {
+ buildSpannedString {
+ append(item.source.title)
+ append(' ')
+ appendNsfwLabel(context)
}
+ } else {
+ item.source.title
+ }
+ binding.imageViewAdd.isGone = item.isEnabled || !item.isAvailable
+ binding.imageViewRemove.isVisible = item.isEnabled
+ binding.imageViewMenu.isVisible = item.isEnabled
+ binding.textViewDescription.text = item.source.getSummary(context)
+ val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
+ binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
+ crossfade(context)
+ error(fallbackIcon)
+ placeholder(fallbackIcon)
+ fallback(fallbackIcon)
+ source(item.source)
+ enqueueWith(coil)
}
}
+}
fun sourceConfigTipDelegate(
listener: OnTipCloseListener,
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigListener.kt
index d97027bc2..2a09d6516 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigListener.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigListener.kt
@@ -12,6 +12,4 @@ interface SourceConfigListener : OnTipCloseListener {
fun onItemShortcutClick(item: SourceConfigItem.SourceItem)
fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean)
-
- fun onHeaderClick(header: SourceConfigItem.LocaleGroup)
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourceCatalogItem.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourceCatalogItem.kt
new file mode 100644
index 000000000..9ea1e55c0
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourceCatalogItem.kt
@@ -0,0 +1,29 @@
+package org.koitharu.kotatsu.settings.sources.catalog
+
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import org.koitharu.kotatsu.list.ui.model.ListModel
+import org.koitharu.kotatsu.parsers.model.MangaSource
+
+sealed interface SourceCatalogItem : ListModel {
+
+ data class Source(
+ val source: MangaSource
+ ) : SourceCatalogItem {
+
+ override fun areItemsTheSame(other: ListModel): Boolean {
+ return other is Source && other.source == source
+ }
+ }
+
+ data class Hint(
+ @DrawableRes val icon: Int,
+ @StringRes val title: Int,
+ @StringRes val text: Int,
+ ) : SourceCatalogItem {
+
+ override fun areItemsTheSame(other: ListModel): Boolean {
+ return other is Hint && other.title == title
+ }
+ }
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourceCatalogItemAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourceCatalogItemAD.kt
new file mode 100644
index 000000000..21abf7aff
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourceCatalogItemAD.kt
@@ -0,0 +1,72 @@
+package org.koitharu.kotatsu.settings.sources.catalog
+
+import androidx.core.text.buildSpannedString
+import androidx.core.view.isVisible
+import androidx.lifecycle.LifecycleOwner
+import coil.ImageLoader
+import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
+import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.core.model.isNsfw
+import org.koitharu.kotatsu.core.parser.favicon.faviconUri
+import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
+import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
+import org.koitharu.kotatsu.core.util.ext.crossfade
+import org.koitharu.kotatsu.core.util.ext.enqueueWith
+import org.koitharu.kotatsu.core.util.ext.newImageRequest
+import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
+import org.koitharu.kotatsu.core.util.ext.source
+import org.koitharu.kotatsu.databinding.ItemEmptyHintBinding
+import org.koitharu.kotatsu.databinding.ItemSourceCatalogBinding
+import org.koitharu.kotatsu.settings.sources.adapter.appendNsfwLabel
+
+fun sourceCatalogItemSourceAD(
+ coil: ImageLoader,
+ lifecycleOwner: LifecycleOwner,
+ listener: OnListItemClickListener
+) = adapterDelegateViewBinding(
+ { layoutInflater, parent ->
+ ItemSourceCatalogBinding.inflate(layoutInflater, parent, false)
+ },
+) {
+
+ binding.imageViewAdd.setOnClickListener { v ->
+ listener.onItemClick(item, v)
+ }
+
+ bind {
+ binding.textViewTitle.text = if (item.source.isNsfw()) {
+ buildSpannedString {
+ append(item.source.title)
+ append(' ')
+ appendNsfwLabel(context)
+ }
+ } else {
+ item.source.title
+ }
+ val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
+ binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
+ crossfade(context)
+ error(fallbackIcon)
+ placeholder(fallbackIcon)
+ fallback(fallbackIcon)
+ source(item.source)
+ enqueueWith(coil)
+ }
+ }
+}
+
+fun sourceCatalogItemHintAD(
+ coil: ImageLoader,
+ lifecycleOwner: LifecycleOwner,
+) = adapterDelegateViewBinding(
+ { inflater, parent -> ItemEmptyHintBinding.inflate(inflater, parent, false) },
+) {
+
+ binding.buttonRetry.isVisible = false
+
+ bind {
+ binding.icon.newImageRequest(lifecycleOwner, item.icon)?.enqueueWith(coil)
+ binding.textPrimary.setText(item.title)
+ binding.textSecondary.setTextAndVisible(item.text)
+ }
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogActivity.kt
new file mode 100644
index 000000000..e23c47c99
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogActivity.kt
@@ -0,0 +1,103 @@
+package org.koitharu.kotatsu.settings.sources.catalog
+
+import android.os.Bundle
+import android.view.View
+import androidx.activity.viewModels
+import androidx.core.graphics.Insets
+import androidx.core.view.updatePadding
+import coil.ImageLoader
+import com.google.android.material.appbar.AppBarLayout
+import com.google.android.material.tabs.TabLayout
+import dagger.hilt.android.AndroidEntryPoint
+import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.core.model.titleResId
+import org.koitharu.kotatsu.core.ui.BaseActivity
+import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
+import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
+import org.koitharu.kotatsu.core.util.ext.firstVisibleItemPosition
+import org.koitharu.kotatsu.core.util.ext.observe
+import org.koitharu.kotatsu.core.util.ext.observeEvent
+import org.koitharu.kotatsu.databinding.ActivitySourcesCatalogBinding
+import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
+import org.koitharu.kotatsu.parsers.model.ContentType
+import org.koitharu.kotatsu.parsers.util.toTitleCase
+import java.util.Locale
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class SourcesCatalogActivity : BaseActivity(),
+ TabLayout.OnTabSelectedListener,
+ OnListItemClickListener,
+ AppBarOwner {
+
+ @Inject
+ lateinit var coil: ImageLoader
+
+ override val appBar: AppBarLayout
+ get() = viewBinding.appbar
+
+ private val viewModel by viewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(ActivitySourcesCatalogBinding.inflate(layoutInflater))
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ initTabs()
+ val sourcesAdapter = SourcesCatalogAdapter(this, coil, this)
+ with(viewBinding.recyclerView) {
+ setHasFixedSize(true)
+ adapter = sourcesAdapter
+ }
+ viewModel.content.observe(this, sourcesAdapter)
+ viewModel.onActionDone.observeEvent(
+ this,
+ ReversibleActionObserver(viewBinding.recyclerView),
+ )
+ viewModel.locale.observe(this) {
+ supportActionBar?.subtitle = it.getLocaleDisplayName()
+ }
+ addMenuProvider(SourcesCatalogMenuProvider(this, viewModel))
+ }
+
+ override fun onWindowInsetsChanged(insets: Insets) {
+ viewBinding.root.updatePadding(
+ left = insets.left,
+ right = insets.right,
+ )
+ viewBinding.recyclerView.updatePadding(
+ bottom = insets.bottom + viewBinding.recyclerView.paddingTop,
+ )
+ }
+
+ override fun onItemClick(item: SourceCatalogItem.Source, view: View) {
+ viewModel.addSource(item.source)
+ }
+
+ override fun onTabSelected(tab: TabLayout.Tab) {
+ viewModel.setContentType(ContentType.entries[tab.position])
+ }
+
+ override fun onTabUnselected(tab: TabLayout.Tab) = Unit
+
+ override fun onTabReselected(tab: TabLayout.Tab) {
+ viewBinding.recyclerView.firstVisibleItemPosition = 0
+ }
+
+ private fun initTabs() {
+ val tabs = viewBinding.tabs
+ for (type in ContentType.entries) {
+ val tab = tabs.newTab()
+ tab.setText(type.titleResId)
+ tabs.addTab(tab)
+ }
+ tabs.addOnTabSelectedListener(this)
+ }
+
+ private fun String?.getLocaleDisplayName(): String {
+ if (this == null) {
+ return getString(R.string.various_languages)
+ }
+ val lc = Locale(this)
+ return lc.getDisplayLanguage(lc).toTitleCase(lc)
+ }
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogAdapter.kt
new file mode 100644
index 000000000..0117be267
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogAdapter.kt
@@ -0,0 +1,25 @@
+package org.koitharu.kotatsu.settings.sources.catalog
+
+import android.content.Context
+import androidx.lifecycle.LifecycleOwner
+import coil.ImageLoader
+import org.koitharu.kotatsu.core.ui.BaseListAdapter
+import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
+import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
+import org.koitharu.kotatsu.list.ui.adapter.ListItemType
+
+class SourcesCatalogAdapter(
+ listener: OnListItemClickListener,
+ coil: ImageLoader,
+ lifecycleOwner: LifecycleOwner,
+) : BaseListAdapter(), FastScroller.SectionIndexer {
+
+ init {
+ addDelegate(ListItemType.CHAPTER, sourceCatalogItemSourceAD(coil, lifecycleOwner, listener))
+ addDelegate(ListItemType.HINT_EMPTY, sourceCatalogItemHintAD(coil, lifecycleOwner))
+ }
+
+ override fun getSectionText(context: Context, position: Int): CharSequence? {
+ return (items.getOrNull(position) as? SourceCatalogItem.Source)?.source?.title?.take(1)
+ }
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogListProducer.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogListProducer.kt
new file mode 100644
index 000000000..dfa0cc296
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogListProducer.kt
@@ -0,0 +1,103 @@
+package org.koitharu.kotatsu.settings.sources.catalog
+
+import androidx.room.InvalidationTracker
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import dagger.hilt.android.ViewModelLifecycle
+import dagger.hilt.android.lifecycle.RetainedLifecycle
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.cancelAndJoin
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.core.db.MangaDatabase
+import org.koitharu.kotatsu.core.db.TABLE_SOURCES
+import org.koitharu.kotatsu.core.db.removeObserverAsync
+import org.koitharu.kotatsu.core.util.ext.lifecycleScope
+import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
+import org.koitharu.kotatsu.parsers.model.ContentType
+
+class SourcesCatalogListProducer @AssistedInject constructor(
+ @Assisted private val locale: String?,
+ @Assisted private val contentType: ContentType,
+ @Assisted lifecycle: ViewModelLifecycle,
+ private val repository: MangaSourcesRepository,
+ private val database: MangaDatabase,
+) : InvalidationTracker.Observer(TABLE_SOURCES), RetainedLifecycle.OnClearedListener {
+
+ private val scope = lifecycle.lifecycleScope
+ private var query: String = ""
+ val list = MutableStateFlow(emptyList())
+
+ private var job = scope.launch(Dispatchers.Default) {
+ list.value = buildList()
+ }
+
+ init {
+ scope.launch(Dispatchers.Default) {
+ database.invalidationTracker.addObserver(this@SourcesCatalogListProducer)
+ }
+ lifecycle.addOnClearedListener(this)
+ }
+
+ override fun onCleared() {
+ database.invalidationTracker.removeObserverAsync(this)
+ }
+
+ override fun onInvalidated(tables: Set) {
+ val prevJob = job
+ job = scope.launch(Dispatchers.Default) {
+ prevJob.cancelAndJoin()
+ list.update { buildList() }
+ }
+ }
+
+ fun setQuery(value: String) {
+ this.query = value
+ onInvalidated(emptySet())
+ }
+
+ private suspend fun buildList(): List {
+ val sources = repository.getDisabledSources().toMutableList()
+ sources.retainAll { it.contentType == contentType && it.locale == locale }
+ if (query.isNotEmpty()) {
+ sources.retainAll { it.title.contains(query, ignoreCase = true) }
+ }
+ return if (sources.isEmpty()) {
+ listOf(
+ if (query.isEmpty()) {
+ SourceCatalogItem.Hint(
+ icon = R.drawable.ic_empty_feed,
+ title = R.string.no_manga_sources,
+ text = R.string.no_manga_sources_catalog_text,
+ )
+ } else {
+ SourceCatalogItem.Hint(
+ icon = R.drawable.ic_empty_feed,
+ title = R.string.nothing_found,
+ text = R.string.no_manga_sources_found,
+ )
+ },
+ )
+ } else {
+ sources.sortBy { it.title }
+ sources.map {
+ SourceCatalogItem.Source(
+ source = it,
+ )
+ }
+ }
+ }
+
+ @AssistedFactory
+ interface Factory {
+
+ fun create(
+ locale: String?,
+ contentType: ContentType,
+ lifecycle: ViewModelLifecycle,
+ ): SourcesCatalogListProducer
+ }
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogMenuProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogMenuProvider.kt
new file mode 100644
index 000000000..2c365bbaf
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogMenuProvider.kt
@@ -0,0 +1,74 @@
+package org.koitharu.kotatsu.settings.sources.catalog
+
+import android.app.Activity
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.View
+import androidx.appcompat.widget.PopupMenu
+import androidx.appcompat.widget.SearchView
+import androidx.core.view.MenuProvider
+import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
+import org.koitharu.kotatsu.parsers.util.toTitleCase
+
+class SourcesCatalogMenuProvider(
+ private val activity: Activity,
+ private val viewModel: SourcesCatalogViewModel,
+) : MenuProvider,
+ MenuItem.OnActionExpandListener,
+ SearchView.OnQueryTextListener {
+
+ override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
+ menuInflater.inflate(R.menu.opt_sources_catalog, menu)
+ val searchMenuItem = menu.findItem(R.id.action_search)
+ searchMenuItem.setOnActionExpandListener(this)
+ val searchView = searchMenuItem.actionView as SearchView
+ searchView.setOnQueryTextListener(this)
+ searchView.setIconifiedByDefault(false)
+ searchView.queryHint = searchMenuItem.title
+ }
+
+ override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
+ R.id.action_locales -> {
+ showLocalesMenu()
+ true
+ }
+
+ else -> false
+ }
+
+ override fun onMenuItemActionExpand(item: MenuItem): Boolean {
+ (activity as? AppBarOwner)?.appBar?.setExpanded(false, true)
+ return true
+ }
+
+ override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
+ (item.actionView as SearchView).setQuery("", false)
+ return true
+ }
+
+ override fun onQueryTextSubmit(query: String?): Boolean = false
+
+ override fun onQueryTextChange(newText: String?): Boolean {
+ viewModel.performSearch(newText.orEmpty())
+ return true
+ }
+
+ private fun showLocalesMenu() {
+ val locales = viewModel.locales
+ val anchor: View = (activity as AppBarOwner).appBar.let {
+ it.findViewById(R.id.toolbar) ?: it
+ }
+ val menu = PopupMenu(activity, anchor)
+ for ((i, lc) in locales.withIndex()) {
+ val title = lc?.getDisplayLanguage(lc)?.toTitleCase(lc) ?: activity.getString(R.string.various_languages)
+ menu.menu.add(Menu.NONE, Menu.NONE, i, title)
+ }
+ menu.setOnMenuItemClickListener {
+ viewModel.setLocale(locales.getOrNull(it.order)?.language)
+ true
+ }
+ menu.show()
+ }
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogViewModel.kt
new file mode 100644
index 000000000..08aa0f5a8
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogViewModel.kt
@@ -0,0 +1,79 @@
+package org.koitharu.kotatsu.settings.sources.catalog
+
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.internal.lifecycle.RetainedLifecycleImpl
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.stateIn
+import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.core.ui.BaseViewModel
+import org.koitharu.kotatsu.core.ui.util.ReversibleAction
+import org.koitharu.kotatsu.core.util.LocaleComparator
+import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
+import org.koitharu.kotatsu.core.util.ext.call
+import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
+import org.koitharu.kotatsu.parsers.model.ContentType
+import org.koitharu.kotatsu.parsers.model.MangaSource
+import org.koitharu.kotatsu.parsers.util.mapToSet
+import java.util.Locale
+import javax.inject.Inject
+
+@HiltViewModel
+class SourcesCatalogViewModel @Inject constructor(
+ private val repository: MangaSourcesRepository,
+ private val listProducerFactory: SourcesCatalogListProducer.Factory,
+) : BaseViewModel() {
+
+ private val lifecycle = RetainedLifecycleImpl()
+ val onActionDone = MutableEventFlow()
+ val contentType = MutableStateFlow(ContentType.entries.first())
+ val locales = getLocalesImpl()
+ val locale = MutableStateFlow(locales.firstOrNull()?.language)
+
+ private val listProducer: StateFlow = combine(
+ locale,
+ contentType,
+ ) { lc, type ->
+ listProducerFactory.create(lc, type, lifecycle)
+ }.stateIn(viewModelScope, SharingStarted.Eagerly, null)
+
+ val content = listProducer.flatMapLatest {
+ it?.list ?: emptyFlow()
+ }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
+
+ override fun onCleared() {
+ super.onCleared()
+ lifecycle.dispatchOnCleared()
+ }
+
+ fun performSearch(query: String) {
+ listProducer.value?.setQuery(query)
+ }
+
+ fun setLocale(value: String?) {
+ locale.value = value
+ }
+
+ fun setContentType(value: ContentType) {
+ contentType.value = value
+ }
+
+ fun addSource(source: MangaSource) {
+ launchJob(Dispatchers.Default) {
+ val rollback = repository.setSourceEnabled(source, true)
+ onActionDone.call(ReversibleAction(R.string.source_enabled, rollback))
+ }
+ }
+
+ private fun getLocalesImpl(): List {
+ return repository.allMangaSources
+ .mapToSet { it.locale?.let(::Locale) }
+ .sortedWith(LocaleComparator())
+ }
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/model/SourceConfigItem.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/model/SourceConfigItem.kt
index b162c15ef..a68b6581b 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/model/SourceConfigItem.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/model/SourceConfigItem.kt
@@ -2,45 +2,15 @@ package org.koitharu.kotatsu.settings.sources.model
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
-import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaSource
sealed interface SourceConfigItem : ListModel {
- data class Header(
- @StringRes val titleResId: Int,
- ) : SourceConfigItem {
-
- override fun areItemsTheSame(other: ListModel): Boolean {
- return other is Header && other.titleResId == titleResId
- }
- }
-
- data class LocaleGroup(
- val localeId: String?,
- val title: String?,
- val isExpanded: Boolean,
- ) : SourceConfigItem {
-
- override fun areItemsTheSame(other: ListModel): Boolean {
- return other is LocaleGroup && other.localeId == localeId
- }
-
- override fun getChangePayload(previousState: ListModel): Any? {
- return if (previousState is LocaleGroup && previousState.isExpanded != isExpanded) {
- ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED
- } else {
- super.getChangePayload(previousState)
- }
- }
- }
-
data class SourceItem(
val source: MangaSource,
val isEnabled: Boolean,
- val summary: String?,
val isDraggable: Boolean,
val isAvailable: Boolean,
) : SourceConfigItem {
@@ -51,14 +21,6 @@ sealed interface SourceConfigItem : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is SourceItem && other.source == source
}
-
- override fun getChangePayload(previousState: ListModel): Any? {
- return if (previousState is SourceItem && previousState.isEnabled != isEnabled) {
- ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED
- } else {
- super.getChangePayload(previousState)
- }
- }
}
data class Tip(
diff --git a/app/src/main/res/layout/activity_sources_catalog.xml b/app/src/main/res/layout/activity_sources_catalog.xml
new file mode 100644
index 000000000..88e67d6d6
--- /dev/null
+++ b/app/src/main/res/layout/activity_sources_catalog.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_explore_source_list.xml b/app/src/main/res/layout/item_explore_source_list.xml
index 320d853b7..0666e7f8f 100644
--- a/app/src/main/res/layout/item_explore_source_list.xml
+++ b/app/src/main/res/layout/item_explore_source_list.xml
@@ -1,12 +1,15 @@
-
+ android:baselineAligned="false"
+ android:clipChildren="false"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
-
+ android:layout_marginEnd="@dimen/margin_small"
+ android:orientation="vertical">
-
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_search_suggestion_source.xml b/app/src/main/res/layout/item_search_suggestion_source.xml
index 23636ad87..7aad1e997 100644
--- a/app/src/main/res/layout/item_search_suggestion_source.xml
+++ b/app/src/main/res/layout/item_search_suggestion_source.xml
@@ -4,10 +4,12 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
- android:layout_height="?attr/listPreferredItemHeightSmall"
+ android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:gravity="center_vertical"
- android:orientation="horizontal">
+ android:minHeight="?attr/listPreferredItemHeightSmall"
+ android:orientation="horizontal"
+ android:paddingVertical="@dimen/margin_small">
-
+ android:orientation="vertical">
+
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/app/src/main/res/layout/item_source_catalog.xml b/app/src/main/res/layout/item_source_catalog.xml
new file mode 100644
index 000000000..a1a67ebed
--- /dev/null
+++ b/app/src/main/res/layout/item_source_catalog.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_source_config.xml b/app/src/main/res/layout/item_source_config.xml
index 27852b4ce..b1c524539 100644
--- a/app/src/main/res/layout/item_source_config.xml
+++ b/app/src/main/res/layout/item_source_config.xml
@@ -7,6 +7,7 @@
android:layout_height="wrap_content"
android:background="?android:windowBackground"
android:gravity="center_vertical"
+ android:minHeight="?listPreferredItemHeightSmall"
android:orientation="horizontal"
android:paddingVertical="@dimen/margin_small"
android:paddingStart="?listPreferredItemPaddingStart"
@@ -36,7 +37,7 @@
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleLine="true"
- android:textAppearance="?attr/textAppearanceBodyLarge"
+ android:textAppearance="?attr/textAppearanceTitleSmall"
tools:text="@tools:sample/lorem[15]" />
+
+
-
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 489d6c4dc..7fe845d30 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -512,4 +512,13 @@
Last successful backup: %s
x%.1f
Lock screen rotation
+ Manga
+ Hentai
+ Comics
+ Other
+ %1$s, %2$s
+ Sources catalog
+ Source enabled
+ No available sources in this section yet. Stay tuned
+ No available manga sources found by your query