New filter sheet draft implementation
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package org.koitharu.kotatsu.core.model
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import org.koitharu.kotatsu.R
|
||||
@@ -43,6 +44,15 @@ val MangaState.titleResId: Int
|
||||
MangaState.PAUSED -> R.string.state_paused
|
||||
}
|
||||
|
||||
@get:DrawableRes
|
||||
val MangaState.iconResId: Int
|
||||
get() = when (this) {
|
||||
MangaState.ONGOING -> R.drawable.ic_state_ongoing
|
||||
MangaState.FINISHED -> R.drawable.ic_state_finished
|
||||
MangaState.ABANDONED -> R.drawable.ic_state_abandoned
|
||||
MangaState.PAUSED -> R.drawable.ic_action_pause
|
||||
}
|
||||
|
||||
fun Manga.findChapter(id: Long): MangaChapter? {
|
||||
return chapters?.findById(id)
|
||||
}
|
||||
|
||||
@@ -54,6 +54,14 @@ class ChipsView @JvmOverloads constructor(
|
||||
defaultChipTextColor = a.getColorStateListOrThrow(materialR.styleable.Chip_android_textColor)
|
||||
defaultChipIconTint = a.getColorStateListOrThrow(materialR.styleable.Chip_chipIconTint)
|
||||
a.recycle()
|
||||
|
||||
if (isInEditMode) {
|
||||
setChips(
|
||||
List(5) {
|
||||
ChipModel(0, "Chip $it", 0, false, false)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun requestLayout() {
|
||||
|
||||
@@ -19,6 +19,11 @@ import org.koitharu.kotatsu.core.exceptions.SyncApiException
|
||||
import org.koitharu.kotatsu.core.exceptions.TooManyRequestExceptions
|
||||
import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException
|
||||
import org.koitharu.kotatsu.core.exceptions.WrongPasswordException
|
||||
import org.koitharu.kotatsu.parsers.ErrorMessages.FILTER_BOTH_LOCALE_GENRES_NOT_SUPPORTED
|
||||
import org.koitharu.kotatsu.parsers.ErrorMessages.FILTER_BOTH_STATES_GENRES_NOT_SUPPORTED
|
||||
import org.koitharu.kotatsu.parsers.ErrorMessages.FILTER_MULTIPLE_GENRES_NOT_SUPPORTED
|
||||
import org.koitharu.kotatsu.parsers.ErrorMessages.FILTER_MULTIPLE_STATES_NOT_SUPPORTED
|
||||
import org.koitharu.kotatsu.parsers.ErrorMessages.SEARCH_NOT_SUPPORTED
|
||||
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
|
||||
import org.koitharu.kotatsu.parsers.exception.ContentUnavailableException
|
||||
import org.koitharu.kotatsu.parsers.exception.NotFoundException
|
||||
@@ -28,9 +33,6 @@ import java.net.UnknownHostException
|
||||
|
||||
private const val MSG_NO_SPACE_LEFT = "No space left on device"
|
||||
private const val IMAGE_FORMAT_NOT_SUPPORTED = "Image format not supported"
|
||||
private const val MULTIPLE_GENRES_NOT_SUPPORTED = "Multiple genres are not supported by this source"
|
||||
private const val MULTIPLE_STATES_NOT_SUPPORTED = "Multiple states are not supported by this source"
|
||||
private const val SEARCH_NOT_SUPPORTED = "Search is not supported by this source"
|
||||
|
||||
fun Throwable.getDisplayMessage(resources: Resources): String = when (this) {
|
||||
is AuthRequiredException -> resources.getString(R.string.auth_required)
|
||||
@@ -85,9 +87,11 @@ private fun getDisplayMessage(msg: String?, resources: Resources): String? = whe
|
||||
msg.isNullOrEmpty() -> null
|
||||
msg.contains(MSG_NO_SPACE_LEFT) -> resources.getString(R.string.error_no_space_left)
|
||||
msg.contains(IMAGE_FORMAT_NOT_SUPPORTED) -> resources.getString(R.string.error_corrupted_file)
|
||||
msg == MULTIPLE_GENRES_NOT_SUPPORTED -> resources.getString(R.string.error_multiple_genres_not_supported)
|
||||
msg == MULTIPLE_STATES_NOT_SUPPORTED -> resources.getString(R.string.error_multiple_states_not_supported)
|
||||
msg == FILTER_MULTIPLE_GENRES_NOT_SUPPORTED -> resources.getString(R.string.error_multiple_genres_not_supported)
|
||||
msg == FILTER_MULTIPLE_STATES_NOT_SUPPORTED -> resources.getString(R.string.error_multiple_states_not_supported)
|
||||
msg == SEARCH_NOT_SUPPORTED -> resources.getString(R.string.error_search_not_supported)
|
||||
msg == FILTER_BOTH_LOCALE_GENRES_NOT_SUPPORTED -> resources.getString(R.string.error_filter_locale_genre_not_supported)
|
||||
msg == FILTER_BOTH_STATES_GENRES_NOT_SUPPORTED -> resources.getString(R.string.error_filter_states_genre_not_supported)
|
||||
else -> null
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ fun filterSortDelegate(
|
||||
) {
|
||||
|
||||
itemView.setOnClickListener {
|
||||
listener.onSortItemClick(item)
|
||||
listener.setSortOrder(item.order)
|
||||
}
|
||||
|
||||
bind { payloads ->
|
||||
@@ -35,7 +35,7 @@ fun filterStateDelegate(
|
||||
) {
|
||||
|
||||
itemView.setOnClickListener {
|
||||
listener.onStateItemClick(item)
|
||||
listener.setState(item.state, !item.isChecked)
|
||||
}
|
||||
|
||||
bind { payloads ->
|
||||
@@ -52,7 +52,7 @@ fun filterLanguageDelegate(
|
||||
) {
|
||||
|
||||
itemView.setOnClickListener {
|
||||
listener.onLanguageItemClick(item)
|
||||
listener.setLanguage(item.locale)
|
||||
}
|
||||
|
||||
bind { payloads ->
|
||||
@@ -69,7 +69,7 @@ fun filterTagDelegate(
|
||||
) {
|
||||
|
||||
itemView.setOnClickListener {
|
||||
listener.onTagItemClick(item, isFromChip = false)
|
||||
listener.setTag(item.tag, !item.isChecked)
|
||||
}
|
||||
|
||||
bind { payloads ->
|
||||
@@ -86,7 +86,7 @@ fun filterTagMultipleDelegate(
|
||||
) {
|
||||
|
||||
itemView.setOnClickListener {
|
||||
listener.onTagItemClick(item, isFromChip = false)
|
||||
listener.setTag(item.tag, !item.isChecked)
|
||||
}
|
||||
|
||||
bind { payloads ->
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.koitharu.kotatsu.filter.ui
|
||||
|
||||
import android.view.View
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import dagger.hilt.android.ViewModelLifecycle
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
@@ -14,7 +13,9 @@ import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.plus
|
||||
@@ -22,18 +23,17 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.core.util.LocaleComparator
|
||||
import org.koitharu.kotatsu.core.util.ext.lifecycleScope
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.require
|
||||
import org.koitharu.kotatsu.core.util.ext.sortedByOrdinal
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterItem
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterProperty
|
||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||
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.SuspendLazy
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
|
||||
@@ -55,17 +55,75 @@ class FilterCoordinator @Inject constructor(
|
||||
|
||||
private val coroutineScope = lifecycle.lifecycleScope
|
||||
private val repository = mangaRepositoryFactory.create(savedStateHandle.require(RemoteListFragment.ARG_SOURCE))
|
||||
private val currentState =
|
||||
MutableStateFlow(MangaListFilter.Advanced(repository.defaultSortOrder, emptySet(), null, emptySet()))
|
||||
private var searchQuery = MutableStateFlow("")
|
||||
private val currentState = MutableStateFlow(
|
||||
MangaListFilter.Advanced(repository.defaultSortOrder, emptySet(), null, emptySet()),
|
||||
)
|
||||
private val localTags = SuspendLazy {
|
||||
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))
|
||||
override val filterTags: StateFlow<FilterProperty<MangaTag>> = combine(
|
||||
currentState.distinctUntilChangedBy { it.tags },
|
||||
getTagsAsFlow(),
|
||||
) { state, tags ->
|
||||
FilterProperty(
|
||||
availableItems = tags.items.sortedBy { it.title },
|
||||
selectedItems = state.tags,
|
||||
isLoading = tags.isLoading,
|
||||
error = tags.error,
|
||||
)
|
||||
}.stateIn(coroutineScope + Dispatchers.Default, SharingStarted.Lazily, loadingProperty())
|
||||
|
||||
override val filterSortOrder: StateFlow<FilterProperty<SortOrder>> = combine(
|
||||
currentState.distinctUntilChangedBy { it.sortOrder },
|
||||
flowOf(repository.sortOrders),
|
||||
) { state, orders ->
|
||||
FilterProperty(
|
||||
availableItems = orders.sortedBy { it.ordinal },
|
||||
selectedItems = setOf(state.sortOrder),
|
||||
isLoading = false,
|
||||
error = null,
|
||||
)
|
||||
}.stateIn(coroutineScope + Dispatchers.Default, SharingStarted.Lazily, loadingProperty())
|
||||
|
||||
override val filterState: StateFlow<FilterProperty<MangaState>> = combine(
|
||||
currentState.distinctUntilChangedBy { it.states },
|
||||
flowOf(repository.states),
|
||||
) { state, states ->
|
||||
FilterProperty(
|
||||
availableItems = states.sortedBy { it.ordinal },
|
||||
selectedItems = state.states,
|
||||
isLoading = false,
|
||||
error = null,
|
||||
)
|
||||
}.stateIn(coroutineScope + Dispatchers.Default, SharingStarted.Lazily, loadingProperty())
|
||||
|
||||
override val filterLocale: StateFlow<FilterProperty<Locale?>> = combine(
|
||||
currentState.distinctUntilChangedBy { it.locale },
|
||||
getLocalesAsFlow(),
|
||||
) { state, locales ->
|
||||
val list = if (locales.items.isNotEmpty()) {
|
||||
val l = ArrayList<Locale?>(locales.items.size + 1)
|
||||
l.add(null)
|
||||
l.addAll(locales.items)
|
||||
try {
|
||||
l.sortWith(nullsFirst(LocaleComparator()))
|
||||
} catch (e: IllegalArgumentException) {
|
||||
e.printStackTraceDebug()
|
||||
}
|
||||
l
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
FilterProperty(
|
||||
availableItems = list,
|
||||
selectedItems = setOf(state.locale),
|
||||
isLoading = locales.isLoading,
|
||||
error = locales.error,
|
||||
)
|
||||
}.stateIn(coroutineScope + Dispatchers.Default, SharingStarted.Lazily, loadingProperty())
|
||||
|
||||
override val header: StateFlow<FilterHeaderModel> = getHeaderFlow().stateIn(
|
||||
scope = coroutineScope + Dispatchers.Default,
|
||||
@@ -78,55 +136,53 @@ class FilterCoordinator @Inject constructor(
|
||||
),
|
||||
)
|
||||
|
||||
init {
|
||||
observeState()
|
||||
}
|
||||
|
||||
override fun applyFilter(tags: Set<MangaTag>) {
|
||||
setTags(tags)
|
||||
}
|
||||
|
||||
override fun onSortItemClick(item: FilterItem.Sort) {
|
||||
override fun setSortOrder(value: SortOrder) {
|
||||
currentState.update { oldValue ->
|
||||
oldValue.copy(sortOrder = item.order)
|
||||
oldValue.copy(sortOrder = value)
|
||||
}
|
||||
repository.defaultSortOrder = item.order
|
||||
repository.defaultSortOrder = value
|
||||
}
|
||||
|
||||
override fun onTagItemClick(item: FilterItem.Tag, isFromChip: Boolean) {
|
||||
override fun setLanguage(value: Locale?) {
|
||||
currentState.update { oldValue ->
|
||||
val newTags = if (!item.isMultiple) {
|
||||
if (isFromChip && item.isChecked) {
|
||||
emptySet()
|
||||
oldValue.copy(locale = value)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setTag(value: MangaTag, addOrRemove: Boolean) {
|
||||
currentState.update { oldValue ->
|
||||
val newTags = if (repository.isMultipleTagsSupported) {
|
||||
if (addOrRemove) {
|
||||
oldValue.tags + value
|
||||
} else {
|
||||
setOf(item.tag)
|
||||
oldValue.tags - value
|
||||
}
|
||||
} else if (item.isChecked) {
|
||||
oldValue.tags - item.tag
|
||||
} else {
|
||||
oldValue.tags + item.tag
|
||||
if (addOrRemove) {
|
||||
setOf(value)
|
||||
} else {
|
||||
emptySet()
|
||||
}
|
||||
}
|
||||
oldValue.copy(tags = newTags)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStateItemClick(item: FilterItem.State) {
|
||||
override fun setState(value: MangaState, addOrRemove: Boolean) {
|
||||
currentState.update { oldValue ->
|
||||
val newStates = if (item.isChecked) {
|
||||
oldValue.states - item.state
|
||||
val newStates = if (addOrRemove) {
|
||||
oldValue.states + value
|
||||
} else {
|
||||
oldValue.states + item.state
|
||||
oldValue.states - value
|
||||
}
|
||||
oldValue.copy(states = newStates)
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
@@ -142,7 +198,7 @@ class FilterCoordinator @Inject constructor(
|
||||
if (!availableTagsDeferred.isCompleted) {
|
||||
emit(emptySet())
|
||||
}
|
||||
emit(availableTagsDeferred.await())
|
||||
emit(availableTagsDeferred.await().getOrNull())
|
||||
}
|
||||
|
||||
fun observeState() = currentState.asStateFlow()
|
||||
@@ -161,10 +217,6 @@ class FilterCoordinator @Inject constructor(
|
||||
|
||||
fun snapshot() = currentState.value
|
||||
|
||||
fun performSearch(query: String) {
|
||||
searchQuery.value = query
|
||||
}
|
||||
|
||||
private fun getHeaderFlow() = combine(
|
||||
observeState(),
|
||||
observeAvailableTags(),
|
||||
@@ -178,34 +230,25 @@ class FilterCoordinator @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private fun getItemsFlow() = combine(
|
||||
getTagsAsFlow(),
|
||||
getLocalesAsFlow(),
|
||||
currentState,
|
||||
searchQuery,
|
||||
) { tags, locales, state, query ->
|
||||
buildFilterList(tags, locales, state, query)
|
||||
}
|
||||
|
||||
private fun getTagsAsFlow() = flow {
|
||||
val localTags = localTags.get()
|
||||
emit(PendingSet(localTags, isLoading = true, isError = false))
|
||||
val remoteTags = tryLoadTags()
|
||||
if (remoteTags == null) {
|
||||
emit(PendingSet(localTags, isLoading = false, isError = true))
|
||||
} else {
|
||||
emit(PendingSet(mergeTags(remoteTags, localTags), isLoading = false, isError = false))
|
||||
}
|
||||
emit(PendingSet(localTags, isLoading = true, error = null))
|
||||
tryLoadTags()
|
||||
.onSuccess { remoteTags ->
|
||||
emit(PendingSet(mergeTags(remoteTags, localTags), isLoading = false, error = null))
|
||||
}.onFailure {
|
||||
emit(PendingSet(localTags, isLoading = false, error = it))
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
emit(PendingSet(emptySet(), isLoading = true, error = null))
|
||||
tryLoadLocales()
|
||||
.onSuccess { locales ->
|
||||
emit(PendingSet(locales, isLoading = false, error = null))
|
||||
}.onFailure {
|
||||
emit(PendingSet(emptySet(), isLoading = false, error = it))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createChipsList(
|
||||
@@ -255,96 +298,20 @@ class FilterCoordinator @Inject constructor(
|
||||
return result
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun buildFilterList(
|
||||
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.items).toList()
|
||||
val list = ArrayList<ListModel>(tags.size + states.size + sortOrders.size + 4)
|
||||
val isMultiTag = repository.isMultipleTagsSupported
|
||||
if (query.isEmpty()) {
|
||||
if (sortOrders.isNotEmpty()) {
|
||||
list.add(ListHeader(R.string.sort_order))
|
||||
sortOrders.mapTo(list) {
|
||||
FilterItem.Sort(it, isSelected = it == state.sortOrder)
|
||||
}
|
||||
}
|
||||
if (states.isNotEmpty()) {
|
||||
list.add(
|
||||
ListHeader(
|
||||
textRes = R.string.state,
|
||||
buttonTextRes = if (state.states.isEmpty()) 0 else R.string.reset,
|
||||
payload = R.string.state,
|
||||
),
|
||||
)
|
||||
states.mapTo(list) {
|
||||
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(
|
||||
textRes = R.string.genres,
|
||||
buttonTextRes = if (state.tags.isEmpty()) 0 else R.string.reset,
|
||||
payload = R.string.genres,
|
||||
),
|
||||
)
|
||||
tags.mapTo(list) {
|
||||
FilterItem.Tag(it, isMultiple = isMultiTag, isChecked = it in state.tags)
|
||||
}
|
||||
}
|
||||
if (allTags.isError) {
|
||||
list.add(FilterItem.Error(R.string.filter_load_error))
|
||||
} else if (allTags.isLoading) {
|
||||
list.add(LoadingFooter())
|
||||
}
|
||||
} else {
|
||||
tags.mapNotNullTo(list) {
|
||||
if (it.title.contains(query, ignoreCase = true)) {
|
||||
FilterItem.Tag(it, isMultiple = isMultiTag, isChecked = it in state.tags)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
if (list.isEmpty()) {
|
||||
list.add(FilterItem.Error(R.string.nothing_found))
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
private suspend fun tryLoadTags(): Set<MangaTag>? {
|
||||
private suspend fun tryLoadTags(): Result<Set<MangaTag>> {
|
||||
val shouldRetryOnError = availableTagsDeferred.isCompleted
|
||||
val result = availableTagsDeferred.await()
|
||||
if (result == null && shouldRetryOnError) {
|
||||
if (result.isFailure && shouldRetryOnError) {
|
||||
availableTagsDeferred = loadTagsAsync()
|
||||
return availableTagsDeferred.await()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private suspend fun tryLoadLocales(): Set<Locale>? {
|
||||
private suspend fun tryLoadLocales(): Result<Set<Locale>> {
|
||||
val shouldRetryOnError = availableLocalesDeferred.isCompleted
|
||||
val result = availableLocalesDeferred.await()
|
||||
if (result == null && shouldRetryOnError) {
|
||||
if (result.isFailure && shouldRetryOnError) {
|
||||
availableLocalesDeferred = loadLocalesAsync()
|
||||
return availableLocalesDeferred.await()
|
||||
}
|
||||
@@ -356,7 +323,7 @@ class FilterCoordinator @Inject constructor(
|
||||
repository.getTags()
|
||||
}.onFailure { error ->
|
||||
error.printStackTraceDebug()
|
||||
}.getOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadLocalesAsync() = coroutineScope.async(Dispatchers.Default, CoroutineStart.LAZY) {
|
||||
@@ -364,7 +331,7 @@ class FilterCoordinator @Inject constructor(
|
||||
repository.getLocales()
|
||||
}.onFailure { error ->
|
||||
error.printStackTraceDebug()
|
||||
}.getOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
private fun mergeTags(primary: Set<MangaTag>, secondary: Set<MangaTag>): Set<MangaTag> {
|
||||
@@ -377,9 +344,11 @@ class FilterCoordinator @Inject constructor(
|
||||
private data class PendingSet<T>(
|
||||
val items: Set<T>,
|
||||
val isLoading: Boolean,
|
||||
val isError: Boolean,
|
||||
val error: Throwable?,
|
||||
)
|
||||
|
||||
private fun <T> loadingProperty() = FilterProperty<T>(emptyList(), emptySet(), true, null)
|
||||
|
||||
private class TagTitleComparator(lc: String?) : Comparator<MangaTag> {
|
||||
|
||||
private val collator = lc?.let { Collator.getInstance(Locale(it)) }
|
||||
|
||||
@@ -13,7 +13,7 @@ import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.databinding.FragmentFilterHeaderBinding
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterItem
|
||||
import org.koitharu.kotatsu.filter.ui.sheet.FilterSheetFragment
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
@@ -39,8 +39,7 @@ class FilterHeaderFragment : BaseFragment<FragmentFilterHeaderBinding>(), ChipsV
|
||||
if (tag == null) {
|
||||
FilterSheetFragment.show(parentFragmentManager)
|
||||
} else {
|
||||
val filterItem = FilterItem.Tag(tag, filter.header.value.allowMultipleTags, !chip.isChecked)
|
||||
filter.onTagItemClick(filterItem, isFromChip = true)
|
||||
filter.setTag(tag, chip.isChecked)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
package org.koitharu.kotatsu.filter.ui
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.recyclerview.widget.AsyncListDiffer
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetBehavior
|
||||
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetCallback
|
||||
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.showDistinct
|
||||
import org.koitharu.kotatsu.databinding.SheetFilterBinding
|
||||
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
class FilterSheetFragment :
|
||||
BaseAdaptiveSheet<SheetFilterBinding>(),
|
||||
AdaptiveSheetCallback,
|
||||
AsyncListDiffer.ListListener<ListModel> {
|
||||
|
||||
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetFilterBinding {
|
||||
return SheetFilterBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewBindingCreated(binding: SheetFilterBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
val filter = (requireActivity() as FilterOwner).filter
|
||||
addSheetCallback(this)
|
||||
val adapter = FilterAdapter(filter, this)
|
||||
binding.recyclerView.adapter = adapter
|
||||
filter.filterItems.observe(viewLifecycleOwner, adapter)
|
||||
binding.recyclerView.addItemDecoration(TypedListSpacingDecoration(binding.root.context, true))
|
||||
|
||||
if (dialog == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
binding.recyclerView.scrollIndicators = 0
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCurrentListChanged(previousList: MutableList<ListModel>, currentList: MutableList<ListModel>) {
|
||||
if (currentList.size > previousList.size && view != null) {
|
||||
(requireViewBinding().recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStateChanged(sheet: View, newState: Int) {
|
||||
viewBinding?.recyclerView?.isFastScrollerEnabled = newState == AdaptiveSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "FilterBottomSheet"
|
||||
|
||||
fun show(fm: FragmentManager) = FilterSheetFragment().showDistinct(fm, TAG)
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,21 @@ package org.koitharu.kotatsu.filter.ui
|
||||
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterProperty
|
||||
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import java.util.Locale
|
||||
|
||||
interface MangaFilter : OnFilterChangedListener {
|
||||
|
||||
val filterItems: StateFlow<List<ListModel>>
|
||||
val filterTags: StateFlow<FilterProperty<MangaTag>>
|
||||
|
||||
val filterSortOrder: StateFlow<FilterProperty<SortOrder>>
|
||||
|
||||
val filterState: StateFlow<FilterProperty<MangaState>>
|
||||
|
||||
val filterLocale: StateFlow<FilterProperty<Locale?>>
|
||||
|
||||
val header: StateFlow<FilterHeaderModel>
|
||||
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
package org.koitharu.kotatsu.filter.ui
|
||||
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterItem
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListHeaderClickListener
|
||||
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import java.util.Locale
|
||||
|
||||
interface OnFilterChangedListener : ListHeaderClickListener {
|
||||
|
||||
fun onSortItemClick(item: FilterItem.Sort)
|
||||
fun setSortOrder(value: SortOrder)
|
||||
|
||||
fun onTagItemClick(item: FilterItem.Tag, isFromChip: Boolean)
|
||||
fun setLanguage(value: Locale?)
|
||||
|
||||
fun onStateItemClick(item: FilterItem.State)
|
||||
fun setTag(value: MangaTag, addOrRemove: Boolean)
|
||||
|
||||
fun onLanguageItemClick(item: FilterItem.Language)
|
||||
fun setState(value: MangaState, addOrRemove: Boolean)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.koitharu.kotatsu.filter.ui.model
|
||||
|
||||
data class FilterProperty<T>(
|
||||
val availableItems: List<T>,
|
||||
val selectedItems: Set<T>,
|
||||
val isLoading: Boolean,
|
||||
val error: Throwable?,
|
||||
) {
|
||||
|
||||
fun isEmpty(): Boolean = availableItems.isEmpty()
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package org.koitharu.kotatsu.filter.ui.sheet
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.core.view.isGone
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import com.google.android.material.chip.Chip
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.titleResId
|
||||
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.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.showDistinct
|
||||
import org.koitharu.kotatsu.databinding.SheetFilter2Binding
|
||||
import org.koitharu.kotatsu.filter.ui.FilterOwner
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterProperty
|
||||
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
|
||||
|
||||
class FilterSheetFragment :
|
||||
BaseAdaptiveSheet<SheetFilter2Binding>(), AdapterView.OnItemSelectedListener, ChipsView.OnChipClickListener,
|
||||
ChipsView.OnChipCloseClickListener {
|
||||
|
||||
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetFilter2Binding {
|
||||
return SheetFilter2Binding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewBindingCreated(binding: SheetFilter2Binding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
if (dialog == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
binding.scrollView.scrollIndicators = 0
|
||||
}
|
||||
val filter = requireFilter()
|
||||
filter.filterSortOrder.observe(viewLifecycleOwner, this::onSortOrderChanged)
|
||||
filter.filterLocale.observe(viewLifecycleOwner, this::onLocaleChanged)
|
||||
filter.filterTags.observe(viewLifecycleOwner, this::onTagsChanged)
|
||||
filter.filterState.observe(viewLifecycleOwner, this::onStateChanged)
|
||||
|
||||
binding.spinnerLocale.onItemSelectedListener = this
|
||||
binding.spinnerOrder.onItemSelectedListener = this
|
||||
binding.chipsState.onChipClickListener = this
|
||||
binding.chipsGenres.onChipClickListener = this
|
||||
binding.chipsGenres.onChipCloseClickListener = this
|
||||
}
|
||||
|
||||
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
|
||||
val filter = requireFilter()
|
||||
when (parent.id) {
|
||||
R.id.spinner_order -> filter.setSortOrder(filter.filterSortOrder.value.availableItems[position])
|
||||
R.id.spinner_locale -> filter.setLanguage(filter.filterLocale.value.availableItems[position])
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
|
||||
|
||||
override fun onChipClick(chip: Chip, data: Any?) {
|
||||
val filter = requireFilter()
|
||||
when (data) {
|
||||
is MangaState -> filter.setState(data, chip.isChecked)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onChipCloseClick(chip: Chip, data: Any?) {
|
||||
val tag = data as? MangaTag ?: return
|
||||
requireFilter().setTag(tag, false)
|
||||
}
|
||||
|
||||
private fun onSortOrderChanged(value: FilterProperty<SortOrder>) {
|
||||
val b = viewBinding ?: return
|
||||
b.textViewOrderTitle.isGone = value.isEmpty()
|
||||
b.cardOrder.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val selected = value.selectedItems.single()
|
||||
b.spinnerOrder.adapter = ArrayAdapter(
|
||||
b.spinnerOrder.context,
|
||||
android.R.layout.simple_spinner_dropdown_item,
|
||||
android.R.id.text1,
|
||||
value.availableItems.map { b.spinnerOrder.context.getString(it.titleRes) },
|
||||
)
|
||||
val selectedIndex = value.availableItems.indexOf(selected)
|
||||
if (selectedIndex >= 0) {
|
||||
b.spinnerOrder.setSelection(selectedIndex, false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onLocaleChanged(value: FilterProperty<Locale?>) {
|
||||
val b = viewBinding ?: return
|
||||
b.textViewLocaleTitle.isGone = value.isEmpty()
|
||||
b.cardLocale.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val selected = value.selectedItems.singleOrNull()
|
||||
b.spinnerLocale.adapter = ArrayAdapter(
|
||||
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)
|
||||
},
|
||||
)
|
||||
val selectedIndex = value.availableItems.indexOf(selected)
|
||||
if (selectedIndex >= 0) {
|
||||
b.spinnerLocale.setSelection(selectedIndex, false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onTagsChanged(value: FilterProperty<MangaTag>) {
|
||||
val b = viewBinding ?: return
|
||||
b.textViewGenresTitle.isGone = value.isEmpty()
|
||||
b.chipsGenres.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val chips = ArrayList<ChipsView.ChipModel>(value.selectedItems.size + 1)
|
||||
value.selectedItems.mapTo(chips) { tag ->
|
||||
ChipsView.ChipModel(
|
||||
tint = 0,
|
||||
title = tag.title,
|
||||
icon = 0,
|
||||
isCheckable = false,
|
||||
isChecked = false,
|
||||
data = tag,
|
||||
)
|
||||
}
|
||||
chips.add(
|
||||
ChipsView.ChipModel(
|
||||
tint = 0,
|
||||
title = getString(R.string.more),
|
||||
icon = materialR.drawable.abc_ic_menu_overflow_material,
|
||||
isCheckable = false,
|
||||
isChecked = false,
|
||||
data = null,
|
||||
),
|
||||
)
|
||||
b.chipsGenres.setChips(chips)
|
||||
}
|
||||
|
||||
private fun onStateChanged(value: FilterProperty<MangaState>) {
|
||||
val b = viewBinding ?: return
|
||||
b.textViewStateTitle.isGone = value.isEmpty()
|
||||
b.chipsState.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val chips = value.availableItems.map { state ->
|
||||
ChipsView.ChipModel(
|
||||
tint = 0,
|
||||
title = getString(state.titleResId),
|
||||
icon = 0,
|
||||
isCheckable = true,
|
||||
isChecked = state in value.selectedItems,
|
||||
data = state,
|
||||
)
|
||||
}
|
||||
b.chipsState.setChips(chips)
|
||||
}
|
||||
|
||||
private fun requireFilter() = (requireActivity() as FilterOwner).filter
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "FilterSheet"
|
||||
|
||||
fun show(fm: FragmentManager) = FilterSheetFragment().showDistinct(fm, TAG)
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,6 @@ import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
||||
import org.koitharu.kotatsu.databinding.FragmentPreviewBinding
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.filter.ui.FilterOwner
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterItem
|
||||
import org.koitharu.kotatsu.image.ui.ImageActivity
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
@@ -98,8 +97,7 @@ class PreviewFragment : BaseFragment<FragmentPreviewBinding>(), View.OnClickList
|
||||
if (filter == null) {
|
||||
startActivity(MangaListActivity.newIntent(requireContext(), setOf(tag)))
|
||||
} else {
|
||||
val filterItem = FilterItem.Tag(tag, filter.header.value.allowMultipleTags, false)
|
||||
filter.onTagItemClick(filterItem, isFromChip = false)
|
||||
filter.setTag(tag, true)
|
||||
closeSelf()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.core.util.ext.withArgs
|
||||
import org.koitharu.kotatsu.databinding.FragmentListBinding
|
||||
import org.koitharu.kotatsu.filter.ui.FilterOwner
|
||||
import org.koitharu.kotatsu.filter.ui.FilterSheetFragment
|
||||
import org.koitharu.kotatsu.filter.ui.MangaFilter
|
||||
import org.koitharu.kotatsu.filter.ui.sheet.FilterSheetFragment
|
||||
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
|
||||
@@ -94,7 +94,7 @@ class LocalListFragment : MangaListFragment(), FilterOwner {
|
||||
Snackbar.make(
|
||||
requireViewBinding().recyclerView,
|
||||
R.string.removal_completed,
|
||||
Snackbar.LENGTH_SHORT
|
||||
Snackbar.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ class LocalListFragment : MangaListFragment(), FilterOwner {
|
||||
fun newInstance() = LocalListFragment().withArgs(1) {
|
||||
putSerializable(
|
||||
RemoteListFragment.ARG_SOURCE,
|
||||
MangaSource.LOCAL
|
||||
MangaSource.LOCAL,
|
||||
) // required by FilterCoordinator
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ import org.koitharu.kotatsu.core.util.ext.withArgs
|
||||
import org.koitharu.kotatsu.databinding.FragmentListBinding
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.filter.ui.FilterOwner
|
||||
import org.koitharu.kotatsu.filter.ui.FilterSheetFragment
|
||||
import org.koitharu.kotatsu.filter.ui.MangaFilter
|
||||
import org.koitharu.kotatsu.filter.ui.sheet.FilterSheetFragment
|
||||
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
||||
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
|
||||
@@ -32,8 +32,8 @@ import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
|
||||
import org.koitharu.kotatsu.databinding.ActivityMangaListBinding
|
||||
import org.koitharu.kotatsu.filter.ui.FilterHeaderFragment
|
||||
import org.koitharu.kotatsu.filter.ui.FilterOwner
|
||||
import org.koitharu.kotatsu.filter.ui.FilterSheetFragment
|
||||
import org.koitharu.kotatsu.filter.ui.MangaFilter
|
||||
import org.koitharu.kotatsu.filter.ui.sheet.FilterSheetFragment
|
||||
import org.koitharu.kotatsu.list.ui.preview.PreviewFragment
|
||||
import org.koitharu.kotatsu.local.ui.LocalListFragment
|
||||
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||
|
||||
143
app/src/main/res/layout/sheet_filter2.xml
Normal file
143
app/src/main/res/layout/sheet_filter2.xml
Normal file
@@ -0,0 +1,143 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetHeaderBar
|
||||
android:id="@+id/headerBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:title="@string/filter" />
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/scrollView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scrollIndicators="top">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="@dimen/margin_normal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_order_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_normal"
|
||||
android:singleLine="true"
|
||||
android:text="@string/sort_order"
|
||||
android:textAppearance="?textAppearanceTitleSmall"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/card_order"
|
||||
style="?materialCardViewOutlinedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginTop="@dimen/margin_normal"
|
||||
android:visibility="gone"
|
||||
app:cardBackgroundColor="?m3ColorBackground"
|
||||
app:shapeAppearance="?shapeAppearanceCornerMedium"
|
||||
app:strokeColor="@color/m3_button_outline_color_selector"
|
||||
app:strokeWidth="@dimen/m3_comp_outlined_button_outline_width"
|
||||
tools:visibility="visible">
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinner_order"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="?listPreferredItemHeightSmall" />
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_locale_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_normal"
|
||||
android:layout_marginTop="@dimen/margin_normal"
|
||||
android:singleLine="true"
|
||||
android:text="@string/language"
|
||||
android:textAppearance="?textAppearanceTitleSmall"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/card_locale"
|
||||
style="?materialCardViewOutlinedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginTop="@dimen/margin_normal"
|
||||
android:visibility="gone"
|
||||
app:cardBackgroundColor="?m3ColorBackground"
|
||||
app:shapeAppearance="?shapeAppearanceCornerMedium"
|
||||
app:strokeColor="@color/m3_button_outline_color_selector"
|
||||
app:strokeWidth="@dimen/m3_comp_outlined_button_outline_width"
|
||||
tools:visibility="visible">
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinner_locale"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="?listPreferredItemHeightSmall" />
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_genres_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_normal"
|
||||
android:layout_marginTop="@dimen/margin_normal"
|
||||
android:singleLine="true"
|
||||
android:text="@string/genres"
|
||||
android:textAppearance="?textAppearanceTitleSmall"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_genres"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_normal"
|
||||
android:paddingHorizontal="@dimen/margin_normal"
|
||||
android:visibility="gone"
|
||||
app:chipSpacingHorizontal="6dp"
|
||||
app:chipSpacingVertical="6dp"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_state_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_normal"
|
||||
android:layout_marginTop="@dimen/margin_normal"
|
||||
android:singleLine="true"
|
||||
android:text="@string/state"
|
||||
android:textAppearance="?textAppearanceTitleSmall"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_state"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_normal"
|
||||
android:paddingHorizontal="@dimen/margin_normal"
|
||||
android:visibility="gone"
|
||||
app:chipSpacingHorizontal="6dp"
|
||||
app:chipSpacingVertical="6dp"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</LinearLayout>
|
||||
@@ -119,6 +119,7 @@
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginTop="@dimen/margin_normal"
|
||||
android:visibility="gone"
|
||||
app:cardBackgroundColor="?m3ColorBackground"
|
||||
app:shapeAppearance="?shapeAppearanceCornerMedium"
|
||||
app:strokeColor="@color/m3_button_outline_color_selector"
|
||||
app:strokeWidth="@dimen/m3_comp_outlined_button_outline_width"
|
||||
|
||||
@@ -540,4 +540,6 @@
|
||||
<string name="this_manga">This manga</string>
|
||||
<string name="color_correction_apply_text">These settings can be applied globally or only to the current manga. If applied globally, individual settings will not be overridden.</string>
|
||||
<string name="apply">Apply</string>
|
||||
<string name="error_filter_locale_genre_not_supported">Filtering by both genres and locale is not supported by this source</string>
|
||||
<string name="error_filter_states_genre_not_supported">Filtering by both genres and states is not supported by this source</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user