Sources catalog improvements

This commit is contained in:
Koitharu
2024-06-06 10:07:14 +03:00
parent 8d7bad97de
commit 173087ee19
19 changed files with 117 additions and 74 deletions

View File

@@ -82,7 +82,7 @@ afterEvaluate {
}
dependencies {
//noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:77a733a062') {
implementation('com.github.KotatsuApp:kotatsu-parsers:0b2bf607f7') {
exclude group: 'org.json', module: 'json'
}

View File

@@ -15,8 +15,6 @@ import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.toLocale
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.toTitleCase
import java.util.Locale
import com.google.android.material.R as materialR
fun MangaSource(name: String): MangaSource {
@@ -39,7 +37,7 @@ val ContentType.titleResId
fun MangaSource.getSummary(context: Context): String {
val type = context.getString(contentType.titleResId)
val locale = locale?.toLocale().getDisplayName(context)
val locale = locale.toLocale().getDisplayName(context)
return context.getString(R.string.source_summary_pattern, type, locale)
}

View File

@@ -52,6 +52,16 @@ class FastScrollRecyclerView @JvmOverloads constructor(
fastScroller.visibility = if (isFastScrollerEnabled) visibility else GONE
}
override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
super.setPadding(left, top, right, bottom)
fastScroller.setPadding(left, top, right, bottom)
}
override fun setPaddingRelative(start: Int, top: Int, end: Int, bottom: Int) {
super.setPaddingRelative(start, top, end, bottom)
fastScroller.setPaddingRelative(start, top, end, bottom)
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
fastScroller.attachRecyclerView(this)

View File

@@ -26,7 +26,9 @@ class ChipsView @JvmOverloads constructor(
onChipClickListener?.onChipClick(it as Chip, it.tag)
}
private val chipOnCloseListener = OnClickListener {
onChipCloseClickListener?.onChipCloseClick(it as Chip, it.tag)
val chip = it as Chip
val data = it.tag
onChipCloseClickListener?.onChipCloseClick(chip, data) ?: onChipClickListener?.onChipClick(chip, data)
}
private val chipStyle: Int
var onChipClickListener: OnChipClickListener? = null

View File

@@ -1,14 +1,27 @@
package org.koitharu.kotatsu.core.util
import androidx.core.os.LocaleListCompat
import org.koitharu.kotatsu.core.util.ext.map
import org.koitharu.kotatsu.core.util.ext.iterator
import java.util.Locale
class LocaleComparator : Comparator<Locale> {
private val deviceLocales = LocaleListCompat.getAdjustedDefault()//LocaleManagerCompat.getSystemLocales(context)
.map { it.language }
.distinct()
private val deviceLocales: List<String>
init {
val localeList = LocaleListCompat.getAdjustedDefault()
deviceLocales = buildList(localeList.size() + 1) {
add("")
val set = HashSet<String>(localeList.size() + 1)
set.add("")
for (locale in localeList) {
val lang = locale.language
if (set.add(lang)) {
add(lang)
}
}
}
}
override fun compare(a: Locale, b: Locale): Int {
val indexA = deviceLocales.indexOf(a.language)

View File

@@ -22,11 +22,10 @@ fun LocaleListCompat.getOrThrow(index: Int) = get(index) ?: throw NoSuchElementE
fun String.toLocale() = Locale(this)
fun Locale?.getDisplayName(context: Context): String {
if (this == null) {
return context.getString(R.string.various_languages)
}
return getDisplayLanguage(this).toTitleCase(this)
fun Locale?.getDisplayName(context: Context): String = when (this) {
null -> context.getString(R.string.all_languages)
Locale.ROOT -> context.getString(R.string.various_languages)
else -> getDisplayLanguage(this).toTitleCase(this)
}
private class LocaleListCompatIterator(private val list: LocaleListCompat) : ListIterator<Locale> {

View File

@@ -67,6 +67,7 @@ class MangaSourcesRepository @Inject constructor(
excludeBroken: Boolean,
types: Set<ContentType>,
query: String?,
locale: String?,
sortOrder: SourcesSortOrder?,
): List<MangaSource> {
assimilateNewSources()
@@ -81,6 +82,9 @@ class MangaSourcesRepository @Inject constructor(
skipNsfwSources = settings.isNsfwContentDisabled,
sortOrder = sortOrder,
)
if (locale != null) {
sources.retainAll { it.locale == locale }
}
if (excludeBroken) {
sources.removeAll { it.isBroken }
}
@@ -175,7 +179,7 @@ class MangaSourcesRepository @Inject constructor(
fun observeHasNewSources(): Flow<Boolean> = observeIsNsfwDisabled().map { skipNsfw ->
val sources = dao.findAllFromVersion(BuildConfig.VERSION_CODE).toSources(skipNsfw, null)
sources.isNotEmpty()
sources.isNotEmpty() && sources.size != remoteSources.size
}.onStart { assimilateNewSources() }
fun observeHasNewSourcesForBadge(): Flow<Boolean> = combine(

View File

@@ -17,6 +17,7 @@ import org.koitharu.kotatsu.core.ui.model.titleRes
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.getDisplayName
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.parentView
import org.koitharu.kotatsu.core.util.ext.showDistinct
@@ -29,7 +30,6 @@ import org.koitharu.kotatsu.parsers.model.ContentRating
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.toTitleCase
import java.util.Locale
import com.google.android.material.R as materialR
@@ -122,10 +122,7 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
b.spinnerLocale.context,
android.R.layout.simple_spinner_dropdown_item,
android.R.id.text1,
value.availableItems.map {
it?.getDisplayLanguage(it)?.toTitleCase(it)
?: b.spinnerLocale.context.getString(R.string.various_languages)
},
value.availableItems.map { it.getDisplayName(b.spinnerLocale.context) },
)
val selectedIndex = value.availableItems.indexOf(selected)
if (selectedIndex >= 0) {

View File

@@ -18,13 +18,13 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.titleResId
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.ext.getDisplayName
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.showDistinct
import org.koitharu.kotatsu.core.util.ext.tryLaunch
import org.koitharu.kotatsu.databinding.SheetWelcomeBinding
import org.koitharu.kotatsu.filter.ui.model.FilterProperty
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.util.toTitleCase
import org.koitharu.kotatsu.settings.backup.RestoreDialogFragment
import java.util.Locale
@@ -58,7 +58,7 @@ class WelcomeSheet : BaseAdaptiveSheet<SheetWelcomeBinding>(), ChipsView.OnChipC
override fun onChipClick(chip: Chip, data: Any?) {
when (data) {
is ContentType -> viewModel.setTypeChecked(data, chip.isChecked)
is Locale? -> viewModel.setLocaleChecked(data, chip.isChecked)
is Locale -> viewModel.setLocaleChecked(data, chip.isChecked)
}
}
@@ -86,12 +86,12 @@ class WelcomeSheet : BaseAdaptiveSheet<SheetWelcomeBinding>(), ChipsView.OnChipC
}
}
private fun onLocalesChanged(value: FilterProperty<Locale?>) {
private fun onLocalesChanged(value: FilterProperty<Locale>) {
val chips = viewBinding?.chipsLocales ?: return
chips.setChips(
value.availableItems.map {
ChipsView.ChipModel(
title = it?.getDisplayLanguage(it)?.toTitleCase(it) ?: getString(R.string.various_languages),
title = it.getDisplayName(chips.context),
isCheckable = true,
isChecked = it in value.selectedItems,
data = it,

View File

@@ -11,6 +11,7 @@ import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.LocaleComparator
import org.koitharu.kotatsu.core.util.ext.sortedWithSafe
import org.koitharu.kotatsu.core.util.ext.toList
import org.koitharu.kotatsu.core.util.ext.toLocale
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.filter.ui.model.FilterProperty
import org.koitharu.kotatsu.parsers.model.ContentType
@@ -27,14 +28,14 @@ class WelcomeViewModel @Inject constructor(
) : BaseViewModel() {
private val allSources = repository.allMangaSources
private val localesGroups by lazy { allSources.groupBy { it.locale?.let { x -> Locale(x) } } }
private val localesGroups by lazy { allSources.groupBy { it.locale.toLocale() } }
private var updateJob: Job
val locales = MutableStateFlow(
FilterProperty<Locale?>(
availableItems = listOf(null),
selectedItems = setOf(null),
FilterProperty<Locale>(
availableItems = listOf(Locale.ROOT),
selectedItems = setOf(Locale.ROOT),
isLoading = true,
error = null,
),
@@ -51,13 +52,14 @@ class WelcomeViewModel @Inject constructor(
init {
updateJob = launchJob(Dispatchers.Default) {
val languages = localesGroups.keys.associateBy { x -> x?.language }
val selectedLocales = HashSet<Locale?>(2)
selectedLocales += ConfigurationCompat.getLocales(context.resources.configuration).toList()
val languages = localesGroups.keys.associateBy { x -> x.language }
val selectedLocales = HashSet<Locale>(2)
ConfigurationCompat.getLocales(context.resources.configuration).toList()
.firstNotNullOfOrNull { lc -> languages[lc.language] }
selectedLocales += null
?.let { selectedLocales += it }
selectedLocales += Locale.ROOT
locales.value = locales.value.copy(
availableItems = localesGroups.keys.sortedWithSafe(nullsFirst(LocaleComparator())),
availableItems = localesGroups.keys.sortedWithSafe(LocaleComparator()),
selectedItems = selectedLocales,
isLoading = false,
)
@@ -66,7 +68,7 @@ class WelcomeViewModel @Inject constructor(
}
}
fun setLocaleChecked(locale: Locale?, isChecked: Boolean) {
fun setLocaleChecked(locale: Locale, isChecked: Boolean) {
val snapshot = locales.value
locales.value = snapshot.copy(
selectedItems = if (isChecked) {
@@ -99,7 +101,7 @@ class WelcomeViewModel @Inject constructor(
}
private suspend fun commit() {
val languages = locales.value.selectedItems.mapToSet { it?.language }
val languages = locales.value.selectedItems.mapToSet { it.language }
val types = types.value.selectedItems
val enabledSources = allSources.filterTo(EnumSet.noneOf(MangaSource::class.java)) { x ->
x.contentType in types && x.locale in languages

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.settings.sources.catalog
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
@@ -15,6 +16,7 @@ import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate
import org.koitharu.kotatsu.core.util.ext.crossfade
import org.koitharu.kotatsu.core.util.ext.drawableStart
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
@@ -22,12 +24,13 @@ import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ItemCatalogPageBinding
import org.koitharu.kotatsu.databinding.ItemEmptyHintBinding
import org.koitharu.kotatsu.databinding.ItemSourceCatalogBinding
import org.koitharu.kotatsu.list.ui.model.ListModel
fun sourceCatalogItemSourceAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
listener: OnListItemClickListener<SourceCatalogItem.Source>
) = adapterDelegateViewBinding<SourceCatalogItem.Source, SourceCatalogItem, ItemSourceCatalogBinding>(
) = adapterDelegateViewBinding<SourceCatalogItem.Source, ListModel, ItemSourceCatalogBinding>(
{ layoutInflater, parent ->
ItemSourceCatalogBinding.inflate(layoutInflater, parent, false)
},
@@ -43,6 +46,11 @@ fun sourceCatalogItemSourceAD(
bind {
binding.textViewTitle.text = item.source.getTitle(context)
binding.textViewDescription.text = item.source.getSummary(context)
binding.textViewDescription.drawableStart = if (item.source.isBroken) {
ContextCompat.getDrawable(context, R.drawable.ic_off_small)
} else {
null
}
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
crossfade(context)
@@ -59,7 +67,7 @@ fun sourceCatalogItemSourceAD(
fun sourceCatalogItemHintAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
) = adapterDelegateViewBinding<SourceCatalogItem.Hint, SourceCatalogItem, ItemEmptyHintBinding>(
) = adapterDelegateViewBinding<SourceCatalogItem.Hint, ListModel, ItemEmptyHintBinding>(
{ inflater, parent -> ItemEmptyHintBinding.inflate(inflater, parent, false) },
) {

View File

@@ -73,6 +73,9 @@ class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
left = insets.left,
right = insets.right,
)
viewBinding.recyclerView.updatePadding(
bottom = insets.bottom,
)
}
override fun onChipClick(chip: Chip, data: Any?) {

View File

@@ -7,16 +7,19 @@ 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
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
import org.koitharu.kotatsu.list.ui.model.ListModel
class SourcesCatalogAdapter(
listener: OnListItemClickListener<SourceCatalogItem.Source>,
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
) : BaseListAdapter<SourceCatalogItem>(), FastScroller.SectionIndexer {
) : BaseListAdapter<ListModel>(), FastScroller.SectionIndexer {
init {
addDelegate(ListItemType.CHAPTER_LIST, sourceCatalogItemSourceAD(coil, lifecycleOwner, listener))
addDelegate(ListItemType.HINT_EMPTY, sourceCatalogItemHintAD(coil, lifecycleOwner))
addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
}
override fun getSectionText(context: Context, position: Int): CharSequence? {

View File

@@ -1,25 +0,0 @@
package org.koitharu.kotatsu.settings.sources.catalog
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import org.koitharu.kotatsu.core.model.titleResId
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
class SourcesCatalogPagerAdapter(
listener: OnListItemClickListener<SourceCatalogItem.Source>,
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
) : BaseListAdapter<SourceCatalogPage>(), TabLayoutMediator.TabConfigurationStrategy {
init {
delegatesManager.addDelegate(sourceCatalogPageAD(listener, coil, lifecycleOwner))
}
override fun onConfigureTab(tab: TabLayout.Tab, position: Int) {
val item = items.getOrNull(position) ?: return
tab.setText(item.type.titleResId)
}
}

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.settings.sources.catalog
import androidx.lifecycle.viewModelScope
import androidx.room.invalidationTrackerFlow
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@@ -10,6 +11,8 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
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.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
@@ -17,9 +20,10 @@ 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.explore.data.SourcesSortOrder
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapToSet
import java.util.EnumSet
import java.util.Locale
import javax.inject.Inject
@@ -27,11 +31,14 @@ import javax.inject.Inject
@HiltViewModel
class SourcesCatalogViewModel @Inject constructor(
private val repository: MangaSourcesRepository,
private val settings: AppSettings,
db: MangaDatabase,
settings: AppSettings,
) : BaseViewModel() {
val onActionDone = MutableEventFlow<ReversibleAction>()
val locales = repository.allMangaSources.mapToSet { it.locale }
val locales: Set<String?> = repository.allMangaSources.mapTo(HashSet<String?>()) { it.locale }.also {
it.add(null)
}
private val searchQuery = MutableStateFlow<String?>(null)
val appliedFilter = MutableStateFlow(
@@ -47,12 +54,13 @@ class SourcesCatalogViewModel @Inject constructor(
val hasNewSources = repository.observeHasNewSources()
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false)
val content: StateFlow<List<SourceCatalogItem>> = combine(
val content: StateFlow<List<ListModel>> = combine(
searchQuery,
appliedFilter,
) { q, f ->
db.invalidationTrackerFlow(TABLE_SOURCES),
) { q, f, _ ->
buildSourcesList(f, q)
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
init {
repository.clearNewSourcesBadge()
@@ -96,8 +104,9 @@ class SourcesCatalogViewModel @Inject constructor(
excludeBroken = false,
types = filter.types,
query = query,
locale = filter.locale,
sortOrder = SourcesSortOrder.ALPHABETIC,
).filter { it.locale == filter.locale }
)
return if (sources.isEmpty()) {
listOf(
if (query == null) {
@@ -115,7 +124,9 @@ class SourcesCatalogViewModel @Inject constructor(
},
)
} else {
sources.map {
sources.sortedBy {
it.isBroken
}.map {
SourceCatalogItem.Source(source = it)
}
}

View File

@@ -0,0 +1,13 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:tint="?colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M22.11 21.46L2.39 1.73L1.11 3L6.25 8.14C6.1 8.41 6 8.7 6 9V14.5L9.5 18V21H14.5V18L15.31 17.2L20.84 22.73L22.11 21.46M13.09 16.59L12.67 17H11.33L10.92 16.59L8 13.67V9.89L13.89 15.78L13.09 16.59M12.2 9L10.2 7H14V3H16V7C17 7 18 8 18 9V14.5L17.85 14.65L16 12.8V9.09C16 9.06 15.95 9 15.92 9H12.2M10 6.8L8 4.8V3H10V6.8Z" />
</vector>

View File

@@ -17,7 +17,8 @@
<com.google.android.material.appbar.MaterialToolbar
android:id="@id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" />
android:layout_height="?attr/actionBarSize"
app:layout_scrollFlags="scroll|enterAlways|snap" />
<HorizontalScrollView
android:id="@+id/scrollView_chips"
@@ -43,11 +44,11 @@
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
<org.koitharu.kotatsu.core.ui.list.fastscroll.FastScrollRecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
android:clipToPadding="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />

View File

@@ -45,9 +45,12 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:drawablePadding="4dp"
android:ellipsize="end"
android:gravity="center_vertical"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceBodySmall"
tools:drawableStart="@drawable/ic_off_small"
tools:text="English" />
</LinearLayout>

View File

@@ -652,4 +652,5 @@
<string name="tracker_debug_info_summary">Debug information about background checks for new chapters</string>
<!-- In plural, used for filter -->
<string name="_new">New</string>
<string name="all_languages">All languages</string>
</resources>