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