Language filter support
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -10,4 +10,6 @@ interface OnFilterChangedListener : ListHeaderClickListener {
|
||||
fun onTagItemClick(item: FilterItem.Tag, isFromChip: Boolean)
|
||||
|
||||
fun onStateItemClick(item: FilterItem.State)
|
||||
|
||||
fun onLanguageItemClick(item: FilterItem.Language)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -6,6 +6,7 @@ enum class ListItemType {
|
||||
FILTER_TAG,
|
||||
FILTER_TAG_MULTI,
|
||||
FILTER_STATE,
|
||||
FILTER_LANGUAGE,
|
||||
HEADER,
|
||||
MANGA_LIST,
|
||||
MANGA_LIST_DETAILED,
|
||||
|
||||
@@ -31,6 +31,7 @@ class TypedListSpacingDecoration(
|
||||
ListItemType.FILTER_TAG,
|
||||
ListItemType.FILTER_TAG_MULTI,
|
||||
ListItemType.FILTER_STATE,
|
||||
ListItemType.FILTER_LANGUAGE,
|
||||
-> outRect.set(0)
|
||||
|
||||
ListItemType.HEADER,
|
||||
|
||||
@@ -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? {
|
||||
|
||||
Reference in New Issue
Block a user