Skip to content

Commit

Permalink
Language filter support
Browse files Browse the repository at this point in the history
  • Loading branch information
Koitharu committed Dec 5, 2023
1 parent 357669d commit 64dc646
Show file tree
Hide file tree
Showing 11 changed files with 132 additions and 17 deletions.
6 changes: 3 additions & 3 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ android {
applicationId 'org.koitharu.kotatsu'
minSdk = 21
targetSdk = 34
versionCode = 602
versionName = '6.4.2'
versionCode = 603
versionName = '6.5-b1'
generatedDensities = []
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
ksp {
Expand Down Expand Up @@ -82,7 +82,7 @@ afterEvaluate {
}
dependencies {
//noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:0efd5437f9') {
implementation('com.github.KotatsuApp:kotatsu-parsers:44c2c074a8') {
exclude group: 'org.json', module: 'json'
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -41,6 +42,8 @@ interface MangaRepository {

suspend fun getTags(): Set<MangaTag>

suspend fun getLocales(): Set<Locale>

suspend fun getRelated(seed: Manga): List<Manga>

@Singleton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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())
Expand All @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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,
)
}
Expand Down Expand Up @@ -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))
}
}

Expand Down Expand Up @@ -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()) {
Expand All @@ -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(
Expand Down Expand Up @@ -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()
Expand All @@ -317,15 +359,23 @@ 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)
result.addAll(primary)
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,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ interface OnFilterChangedListener : ListHeaderClickListener {
fun onTagItemClick(item: FilterItem.Tag, isFromChip: Boolean)

fun onStateItemClick(item: FilterItem.State)

fun onLanguageItemClick(item: FilterItem.Language)
}
Original file line number Diff line number Diff line change
@@ -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 {

Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ enum class ListItemType {
FILTER_TAG,
FILTER_TAG_MULTI,
FILTER_STATE,
FILTER_LANGUAGE,
HEADER,
MANGA_LIST,
MANGA_LIST_DETAILED,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class TypedListSpacingDecoration(
ListItemType.FILTER_TAG,
ListItemType.FILTER_TAG_MULTI,
ListItemType.FILTER_STATE,
ListItemType.FILTER_LANGUAGE,
-> outRect.set(0)

ListItemType.HEADER,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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? {
Expand Down

0 comments on commit 64dc646

Please sign in to comment.