Language filter support

This commit is contained in:
Koitharu
2023-12-05 15:15:51 +02:00
parent 357669d8b2
commit 64dc646fc5
11 changed files with 132 additions and 17 deletions

View File

@@ -15,6 +15,7 @@ import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder
import java.lang.ref.WeakReference
import java.util.EnumMap
import java.util.Locale
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.collections.set
@@ -41,6 +42,8 @@ interface MangaRepository {
suspend fun getTags(): Set<MangaTag>
suspend fun getLocales(): Set<Locale>
suspend fun getRelated(seed: Manga): List<Manga>
@Singleton

View File

@@ -32,6 +32,7 @@ import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.domain
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import java.util.Locale
class RemoteMangaRepository(
private val parser: MangaParser,
@@ -104,6 +105,10 @@ class RemoteMangaRepository(
parser.getAvailableTags()
}
override suspend fun getLocales(): Set<Locale> {
return parser.getAvailableLocales()
}
suspend fun getFavicons(): Favicons = mirrorSwitchInterceptor.withMirrorSwitching {
parser.getFavicons()
}

View File

@@ -2,8 +2,10 @@ package org.koitharu.kotatsu.filter.ui
import android.content.Context
import androidx.recyclerview.widget.AsyncListDiffer.ListListener
import org.koitharu.kotatsu.core.model.titleResId
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.core.ui.model.titleRes
import org.koitharu.kotatsu.filter.ui.model.FilterItem
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD
@@ -21,6 +23,7 @@ class FilterAdapter(
addDelegate(ListItemType.FILTER_TAG, filterTagDelegate(listener))
addDelegate(ListItemType.FILTER_TAG_MULTI, filterTagMultipleDelegate(listener))
addDelegate(ListItemType.FILTER_STATE, filterStateDelegate(listener))
addDelegate(ListItemType.FILTER_LANGUAGE, filterLanguageDelegate(listener))
addDelegate(ListItemType.HEADER, listHeaderAD(listener))
addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD())
@@ -31,10 +34,14 @@ class FilterAdapter(
override fun getSectionText(context: Context, position: Int): CharSequence? {
val list = items
for (i in (0..position).reversed()) {
val item = list.getOrNull(i) ?: continue
if (item is FilterItem.Tag) {
return item.tag.title.firstOrNull()?.toString()
}
val item = list.getOrNull(i) as? FilterItem ?: continue
when (item) {
is FilterItem.Error -> null
is FilterItem.Language -> item.getTitle(context.resources)
is FilterItem.Sort -> context.getString(item.order.titleRes)
is FilterItem.State -> context.getString(item.state.titleResId)
is FilterItem.Tag -> item.tag.title
}?.firstOrNull()?.uppercase()
}
return null
}

View File

@@ -44,6 +44,23 @@ fun filterStateDelegate(
}
}
fun filterLanguageDelegate(
listener: OnFilterChangedListener,
) = adapterDelegateViewBinding<FilterItem.Language, ListModel, ItemCheckableSingleBinding>(
{ layoutInflater, parent -> ItemCheckableSingleBinding.inflate(layoutInflater, parent, false) },
) {
itemView.setOnClickListener {
listener.onLanguageItemClick(item)
}
bind { payloads ->
binding.root.text = item.getTitle(context.resources)
binding.root.setChecked(item.isChecked, payloads.isNotEmpty())
}
}
fun filterTagDelegate(
listener: OnFilterChangedListener,
) = adapterDelegateViewBinding<FilterItem.Tag, ListModel, ItemCheckableSingleBinding>(

View File

@@ -62,6 +62,7 @@ class FilterCoordinator @Inject constructor(
dataRepository.findTags(repository.source)
}
private var availableTagsDeferred = loadTagsAsync()
private var availableLocalesDeferred = loadLocalesAsync()
override val filterItems: StateFlow<List<ListModel>> = getItemsFlow()
.stateIn(coroutineScope + Dispatchers.Default, SharingStarted.Lazily, listOf(LoadingState))
@@ -120,12 +121,18 @@ class FilterCoordinator @Inject constructor(
}
}
override fun onLanguageItemClick(item: FilterItem.Language) {
currentState.update { oldValue ->
oldValue.copy(locale = item.locale)
}
}
override fun onListHeaderClick(item: ListHeader, view: View) {
currentState.update { oldValue ->
oldValue.copy(
sortOrder = oldValue.sortOrder,
tags = if (item.payload == R.string.genres) emptySet() else oldValue.tags,
locale = null,
locale = if (item.payload == R.string.language) null else oldValue.locale,
states = if (item.payload == R.string.state) emptySet() else oldValue.states,
)
}
@@ -173,20 +180,31 @@ class FilterCoordinator @Inject constructor(
private fun getItemsFlow() = combine(
getTagsAsFlow(),
getLocalesAsFlow(),
currentState,
searchQuery,
) { tags, state, query ->
buildFilterList(tags, state, query)
) { tags, locales, state, query ->
buildFilterList(tags, locales, state, query)
}
private fun getTagsAsFlow() = flow {
val localTags = localTags.get()
emit(TagsWrapper(localTags, isLoading = true, isError = false))
emit(PendingSet(localTags, isLoading = true, isError = false))
val remoteTags = tryLoadTags()
if (remoteTags == null) {
emit(TagsWrapper(localTags, isLoading = false, isError = true))
emit(PendingSet(localTags, isLoading = false, isError = true))
} else {
emit(TagsWrapper(mergeTags(remoteTags, localTags), isLoading = false, isError = false))
emit(PendingSet(mergeTags(remoteTags, localTags), isLoading = false, isError = false))
}
}
private fun getLocalesAsFlow(): Flow<PendingSet<Locale>> = flow {
emit(PendingSet(emptySet(), isLoading = true, isError = false))
val locales = tryLoadLocales()
if (locales == null) {
emit(PendingSet(emptySet(), isLoading = false, isError = true))
} else {
emit(PendingSet(locales, isLoading = false, isError = false))
}
}
@@ -239,13 +257,14 @@ class FilterCoordinator @Inject constructor(
@WorkerThread
private fun buildFilterList(
allTags: TagsWrapper,
allTags: PendingSet<MangaTag>,
allLocales: PendingSet<Locale>,
state: MangaListFilter.Advanced,
query: String,
): List<ListModel> {
val sortOrders = repository.sortOrders.sortedByOrdinal()
val states = repository.states
val tags = mergeTags(state.tags, allTags.tags).toList()
val tags = mergeTags(state.tags, allTags.items).toList()
val list = ArrayList<ListModel>(tags.size + states.size + sortOrders.size + 4)
val isMultiTag = repository.isMultipleTagsSupported
if (query.isEmpty()) {
@@ -267,6 +286,19 @@ class FilterCoordinator @Inject constructor(
FilterItem.State(it, isChecked = it in state.states)
}
}
if (allLocales.items.isNotEmpty()) {
list.add(
ListHeader(
textRes = R.string.language,
buttonTextRes = if (state.locale == null) 0 else R.string.reset,
payload = R.string.language,
),
)
list.add(FilterItem.Language(null, isChecked = state.locale == null))
allLocales.items.mapTo(list) {
FilterItem.Language(it, isChecked = state.locale == it)
}
}
if (allTags.isLoading || allTags.isError || tags.isNotEmpty()) {
list.add(
ListHeader(
@@ -309,6 +341,16 @@ class FilterCoordinator @Inject constructor(
return result
}
private suspend fun tryLoadLocales(): Set<Locale>? {
val shouldRetryOnError = availableLocalesDeferred.isCompleted
val result = availableLocalesDeferred.await()
if (result == null && shouldRetryOnError) {
availableLocalesDeferred = loadLocalesAsync()
return availableLocalesDeferred.await()
}
return result
}
private fun loadTagsAsync() = coroutineScope.async(Dispatchers.Default, CoroutineStart.LAZY) {
runCatchingCancellable {
repository.getTags()
@@ -317,6 +359,14 @@ class FilterCoordinator @Inject constructor(
}.getOrNull()
}
private fun loadLocalesAsync() = coroutineScope.async(Dispatchers.Default, CoroutineStart.LAZY) {
runCatchingCancellable {
repository.getLocales()
}.onFailure { error ->
error.printStackTraceDebug()
}.getOrNull()
}
private fun mergeTags(primary: Set<MangaTag>, secondary: Set<MangaTag>): Set<MangaTag> {
val result = TreeSet(TagTitleComparator(repository.source.locale))
result.addAll(secondary)
@@ -324,8 +374,8 @@ class FilterCoordinator @Inject constructor(
return result
}
private data class TagsWrapper(
val tags: Set<MangaTag>,
private data class PendingSet<T>(
val items: Set<T>,
val isLoading: Boolean,
val isError: Boolean,
)

View File

@@ -10,4 +10,6 @@ interface OnFilterChangedListener : ListHeaderClickListener {
fun onTagItemClick(item: FilterItem.Tag, isFromChip: Boolean)
fun onStateItemClick(item: FilterItem.State)
fun onLanguageItemClick(item: FilterItem.Language)
}

View File

@@ -1,11 +1,15 @@
package org.koitharu.kotatsu.filter.ui.model
import android.content.res.Resources
import androidx.annotation.StringRes
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
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
sealed interface FilterItem : ListModel {
@@ -64,6 +68,28 @@ sealed interface FilterItem : ListModel {
}
}
data class Language(
val locale: Locale?,
val isChecked: Boolean,
) : FilterItem {
private val displayText = locale?.getDisplayLanguage(locale)?.toTitleCase(locale)
fun getTitle(resources: Resources) = displayText ?: resources.getString(R.string.various_languages)
override fun areItemsTheSame(other: ListModel): Boolean {
return other is Language && other.locale == locale
}
override fun getChangePayload(previousState: ListModel): Any? {
return if (previousState is Language && previousState.isChecked != isChecked) {
ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED
} else {
super.getChangePayload(previousState)
}
}
}
data class Error(
@StringRes val textResId: Int,
) : FilterItem {

View File

@@ -6,6 +6,7 @@ enum class ListItemType {
FILTER_TAG,
FILTER_TAG_MULTI,
FILTER_STATE,
FILTER_LANGUAGE,
HEADER,
MANGA_LIST,
MANGA_LIST_DETAILED,

View File

@@ -31,6 +31,7 @@ class TypedListSpacingDecoration(
ListItemType.FILTER_TAG,
ListItemType.FILTER_TAG_MULTI,
ListItemType.FILTER_STATE,
ListItemType.FILTER_LANGUAGE,
-> outRect.set(0)
ListItemType.HEADER,

View File

@@ -35,6 +35,7 @@ import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import java.io.File
import java.util.EnumSet
import java.util.Locale
import javax.inject.Inject
import javax.inject.Singleton
@@ -160,6 +161,8 @@ class LocalMangaRepository @Inject constructor(
override suspend fun getTags() = emptySet<MangaTag>()
override suspend fun getLocales() = emptySet<Locale>()
override suspend fun getRelated(seed: Manga): List<Manga> = emptyList()
suspend fun getOutputDir(manga: Manga): File? {