Improve saved filters
This commit is contained in:
@@ -4,7 +4,7 @@ root = true
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = tab
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
max_line_length = 120
|
||||
tab_width = 4
|
||||
|
||||
74
.idea/codeStyles/Project.xml
generated
74
.idea/codeStyles/Project.xml
generated
@@ -1,9 +1,7 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<option name="OTHER_INDENT_OPTIONS">
|
||||
<value>
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</value>
|
||||
<value />
|
||||
</option>
|
||||
<AndroidXmlCodeStyleSettings>
|
||||
<option name="LAYOUT_SETTINGS">
|
||||
@@ -22,40 +20,46 @@
|
||||
</value>
|
||||
</option>
|
||||
</AndroidXmlCodeStyleSettings>
|
||||
<JavaCodeStyleSettings>
|
||||
<option name="IMPORT_LAYOUT_TABLE">
|
||||
<value>
|
||||
<package name="android" withSubpackages="true" static="true" />
|
||||
<package name="androidx" withSubpackages="true" static="true" />
|
||||
<package name="com" withSubpackages="true" static="true" />
|
||||
<package name="junit" withSubpackages="true" static="true" />
|
||||
<package name="net" withSubpackages="true" static="true" />
|
||||
<package name="org" withSubpackages="true" static="true" />
|
||||
<package name="java" withSubpackages="true" static="true" />
|
||||
<package name="javax" withSubpackages="true" static="true" />
|
||||
<package name="" withSubpackages="true" static="true" />
|
||||
<emptyLine />
|
||||
<package name="android" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="androidx" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="com" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="junit" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="net" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="org" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="java" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="javax" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
</value>
|
||||
</option>
|
||||
</JavaCodeStyleSettings>
|
||||
<JetCodeStyleSettings>
|
||||
<option name="ALLOW_TRAILING_COMMA" value="true" />
|
||||
<option name="ALLOW_TRAILING_COMMA_COLLECTION_LITERAL_EXPRESSION" value="true" />
|
||||
<option name="ALLOW_TRAILING_COMMA_VALUE_ARGUMENT_LIST" value="true" />
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="CMake">
|
||||
<indentOptions>
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="Groovy">
|
||||
<indentOptions>
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="HTML">
|
||||
<indentOptions>
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="JAVA">
|
||||
<indentOptions>
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="JSON">
|
||||
<indentOptions>
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="ObjectiveC">
|
||||
<indentOptions>
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="Shell Script">
|
||||
<indentOptions>
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
@@ -64,7 +68,6 @@
|
||||
<codeStyleSettings language="XML">
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
<arrangement>
|
||||
<rules>
|
||||
@@ -179,9 +182,6 @@
|
||||
<option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" />
|
||||
<option name="BLOCK_COMMENT_AT_FIRST_COLUMN" value="false" />
|
||||
<option name="LINE_COMMENT_ADD_SPACE" value="true" />
|
||||
<indentOptions>
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.koitharu.kotatsu.core.model
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.descriptors.serialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
|
||||
object MangaSourceSerializer : KSerializer<MangaSource> {
|
||||
|
||||
override val descriptor: SerialDescriptor = serialDescriptor<String>()
|
||||
|
||||
override fun serialize(
|
||||
encoder: Encoder,
|
||||
value: MangaSource
|
||||
) = encoder.encodeString(value.name)
|
||||
|
||||
override fun deserialize(decoder: Decoder): MangaSource = MangaSource(decoder.decodeString())
|
||||
}
|
||||
@@ -2,10 +2,16 @@ package org.koitharu.kotatsu.core.ui.dialog
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.CompoundButton.OnCheckedChangeListener
|
||||
import android.widget.EditText
|
||||
import android.widget.FrameLayout
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.annotation.UiContext
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.AppCompatEditText
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@@ -18,51 +24,75 @@ import org.koitharu.kotatsu.databinding.DialogCheckboxBinding
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
inline fun buildAlertDialog(
|
||||
@UiContext context: Context,
|
||||
isCentered: Boolean = false,
|
||||
block: MaterialAlertDialogBuilder.() -> Unit,
|
||||
@UiContext context: Context,
|
||||
isCentered: Boolean = false,
|
||||
block: MaterialAlertDialogBuilder.() -> Unit,
|
||||
): AlertDialog = MaterialAlertDialogBuilder(
|
||||
context,
|
||||
if (isCentered) materialR.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered else 0,
|
||||
context,
|
||||
if (isCentered) materialR.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered else 0,
|
||||
).apply(block).create()
|
||||
|
||||
fun <B : AlertDialog.Builder> B.setCheckbox(
|
||||
@StringRes textResId: Int,
|
||||
isChecked: Boolean,
|
||||
onCheckedChangeListener: OnCheckedChangeListener
|
||||
@StringRes textResId: Int,
|
||||
isChecked: Boolean,
|
||||
onCheckedChangeListener: OnCheckedChangeListener
|
||||
) = apply {
|
||||
val binding = DialogCheckboxBinding.inflate(LayoutInflater.from(context))
|
||||
binding.checkbox.setText(textResId)
|
||||
binding.checkbox.isChecked = isChecked
|
||||
binding.checkbox.setOnCheckedChangeListener(onCheckedChangeListener)
|
||||
setView(binding.root)
|
||||
val binding = DialogCheckboxBinding.inflate(LayoutInflater.from(context))
|
||||
binding.checkbox.setText(textResId)
|
||||
binding.checkbox.isChecked = isChecked
|
||||
binding.checkbox.setOnCheckedChangeListener(onCheckedChangeListener)
|
||||
setView(binding.root)
|
||||
}
|
||||
|
||||
fun <B : AlertDialog.Builder, T> B.setRecyclerViewList(
|
||||
list: List<T>,
|
||||
delegate: AdapterDelegate<List<T>>,
|
||||
list: List<T>,
|
||||
delegate: AdapterDelegate<List<T>>,
|
||||
) = apply {
|
||||
val delegatesManager = AdapterDelegatesManager<List<T>>()
|
||||
delegatesManager.addDelegate(delegate)
|
||||
setRecyclerViewList(ListDelegationAdapter(delegatesManager).also { it.items = list })
|
||||
val delegatesManager = AdapterDelegatesManager<List<T>>()
|
||||
delegatesManager.addDelegate(delegate)
|
||||
setRecyclerViewList(ListDelegationAdapter(delegatesManager).also { it.items = list })
|
||||
}
|
||||
|
||||
fun <B : AlertDialog.Builder, T> B.setRecyclerViewList(
|
||||
list: List<T>,
|
||||
vararg delegates: AdapterDelegate<List<T>>,
|
||||
list: List<T>,
|
||||
vararg delegates: AdapterDelegate<List<T>>,
|
||||
) = apply {
|
||||
val delegatesManager = AdapterDelegatesManager<List<T>>()
|
||||
delegates.forEach { delegatesManager.addDelegate(it) }
|
||||
setRecyclerViewList(ListDelegationAdapter(delegatesManager).also { it.items = list })
|
||||
val delegatesManager = AdapterDelegatesManager<List<T>>()
|
||||
delegates.forEach { delegatesManager.addDelegate(it) }
|
||||
setRecyclerViewList(ListDelegationAdapter(delegatesManager).also { it.items = list })
|
||||
}
|
||||
|
||||
fun <B : AlertDialog.Builder> B.setRecyclerViewList(adapter: RecyclerView.Adapter<*>) = apply {
|
||||
val recyclerView = RecyclerView(context)
|
||||
recyclerView.layoutManager = LinearLayoutManager(context)
|
||||
recyclerView.updatePadding(
|
||||
top = context.resources.getDimensionPixelOffset(R.dimen.list_spacing),
|
||||
)
|
||||
recyclerView.clipToPadding = false
|
||||
recyclerView.adapter = adapter
|
||||
setView(recyclerView)
|
||||
val recyclerView = RecyclerView(context)
|
||||
recyclerView.layoutManager = LinearLayoutManager(context)
|
||||
recyclerView.updatePadding(
|
||||
top = context.resources.getDimensionPixelOffset(R.dimen.list_spacing),
|
||||
)
|
||||
recyclerView.clipToPadding = false
|
||||
recyclerView.adapter = adapter
|
||||
setView(recyclerView)
|
||||
}
|
||||
|
||||
fun <B : AlertDialog.Builder> B.setEditText(
|
||||
inputType: Int,
|
||||
singleLine: Boolean,
|
||||
): EditText {
|
||||
val editText = AppCompatEditText(context)
|
||||
editText.inputType = inputType
|
||||
if (singleLine) {
|
||||
editText.setSingleLine()
|
||||
editText.imeOptions = EditorInfo.IME_ACTION_DONE
|
||||
}
|
||||
val layout = FrameLayout(context)
|
||||
val lp = FrameLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
||||
val horizontalMargin = context.resources.getDimensionPixelOffset(R.dimen.screen_padding)
|
||||
lp.setMargins(
|
||||
horizontalMargin,
|
||||
context.resources.getDimensionPixelOffset(R.dimen.margin_small),
|
||||
horizontalMargin,
|
||||
0,
|
||||
)
|
||||
layout.addView(editText, lp)
|
||||
setView(layout)
|
||||
return editText
|
||||
}
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
package org.koitharu.kotatsu.filter.data
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.builtins.SetSerializer
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.element
|
||||
import kotlinx.serialization.encoding.CompositeDecoder
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.encoding.decodeStructure
|
||||
import kotlinx.serialization.encoding.encodeStructure
|
||||
import kotlinx.serialization.serializer
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.util.ext.toLocaleOrNull
|
||||
import org.koitharu.kotatsu.parsers.model.ContentRating
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.model.Demographic
|
||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import java.util.Locale
|
||||
|
||||
object MangaListFilterSerializer : KSerializer<MangaListFilter> {
|
||||
|
||||
override val descriptor: SerialDescriptor =
|
||||
buildClassSerialDescriptor(MangaListFilter::class.java.name) {
|
||||
element<String?>("query", isOptional = true)
|
||||
element(
|
||||
elementName = "tags",
|
||||
descriptor = SetSerializer(MangaTagSerializer).descriptor,
|
||||
isOptional = true,
|
||||
)
|
||||
element(
|
||||
elementName = "tagsExclude",
|
||||
descriptor = SetSerializer(MangaTagSerializer).descriptor,
|
||||
isOptional = true,
|
||||
)
|
||||
element<String?>("locale", isOptional = true)
|
||||
element<String?>("originalLocale", isOptional = true)
|
||||
element<Set<MangaState>>("states", isOptional = true)
|
||||
element<Set<ContentRating>>("contentRating", isOptional = true)
|
||||
element<Set<ContentType>>("types", isOptional = true)
|
||||
element<Set<Demographic>>("demographics", isOptional = true)
|
||||
element<Int>("year", isOptional = true)
|
||||
element<Int>("yearFrom", isOptional = true)
|
||||
element<Int>("yearTo", isOptional = true)
|
||||
element<String?>("author", isOptional = true)
|
||||
}
|
||||
|
||||
override fun serialize(
|
||||
encoder: Encoder,
|
||||
value: MangaListFilter
|
||||
) = encoder.encodeStructure(descriptor) {
|
||||
encodeNullableSerializableElement(descriptor, 0, String.serializer(), value.query)
|
||||
encodeSerializableElement(descriptor, 1, SetSerializer(MangaTagSerializer), value.tags)
|
||||
encodeSerializableElement(descriptor, 2, SetSerializer(MangaTagSerializer), value.tagsExclude)
|
||||
encodeNullableSerializableElement(descriptor, 3, String.serializer(), value.locale?.toLanguageTag())
|
||||
encodeNullableSerializableElement(descriptor, 4, String.serializer(), value.originalLocale?.toLanguageTag())
|
||||
encodeSerializableElement(descriptor, 5, SetSerializer(serializer()), value.states)
|
||||
encodeSerializableElement(descriptor, 6, SetSerializer(serializer()), value.contentRating)
|
||||
encodeSerializableElement(descriptor, 7, SetSerializer(serializer()), value.types)
|
||||
encodeSerializableElement(descriptor, 8, SetSerializer(serializer()), value.demographics)
|
||||
encodeIntElement(descriptor, 9, value.year)
|
||||
encodeIntElement(descriptor, 10, value.yearFrom)
|
||||
encodeIntElement(descriptor, 11, value.yearTo)
|
||||
encodeNullableSerializableElement(descriptor, 12, String.serializer(), value.author)
|
||||
}
|
||||
|
||||
override fun deserialize(
|
||||
decoder: Decoder
|
||||
): MangaListFilter = decoder.decodeStructure(descriptor) {
|
||||
var query: String? = MangaListFilter.EMPTY.query
|
||||
var tags: Set<MangaTag> = MangaListFilter.EMPTY.tags
|
||||
var tagsExclude: Set<MangaTag> = MangaListFilter.EMPTY.tagsExclude
|
||||
var locale: Locale? = MangaListFilter.EMPTY.locale
|
||||
var originalLocale: Locale? = MangaListFilter.EMPTY.originalLocale
|
||||
var states: Set<MangaState> = MangaListFilter.EMPTY.states
|
||||
var contentRating: Set<ContentRating> = MangaListFilter.EMPTY.contentRating
|
||||
var types: Set<ContentType> = MangaListFilter.EMPTY.types
|
||||
var demographics: Set<Demographic> = MangaListFilter.EMPTY.demographics
|
||||
var year: Int = MangaListFilter.EMPTY.year
|
||||
var yearFrom: Int = MangaListFilter.EMPTY.yearFrom
|
||||
var yearTo: Int = MangaListFilter.EMPTY.yearTo
|
||||
var author: String? = MangaListFilter.EMPTY.author
|
||||
|
||||
while (true) {
|
||||
when (decodeElementIndex(descriptor)) {
|
||||
0 -> query = decodeNullableSerializableElement(descriptor, 0, serializer<String>())
|
||||
1 -> tags = decodeSerializableElement(descriptor, 1, SetSerializer(MangaTagSerializer))
|
||||
2 -> tagsExclude = decodeSerializableElement(descriptor, 2, SetSerializer(MangaTagSerializer))
|
||||
3 -> locale = decodeNullableSerializableElement(descriptor, 3, serializer<String>())?.toLocaleOrNull()
|
||||
4 -> originalLocale =
|
||||
decodeNullableSerializableElement(descriptor, 4, serializer<String>())?.toLocaleOrNull()
|
||||
|
||||
5 -> states = decodeSerializableElement(descriptor, 5, SetSerializer(serializer()))
|
||||
6 -> contentRating = decodeSerializableElement(descriptor, 6, SetSerializer(serializer()))
|
||||
7 -> types = decodeSerializableElement(descriptor, 7, SetSerializer(serializer()))
|
||||
8 -> demographics = decodeSerializableElement(descriptor, 8, SetSerializer(serializer()))
|
||||
9 -> year = decodeIntElement(descriptor, 9)
|
||||
10 -> yearFrom = decodeIntElement(descriptor, 10)
|
||||
11 -> yearTo = decodeIntElement(descriptor, 11)
|
||||
12 -> author = decodeNullableSerializableElement(descriptor, 12, serializer<String>())
|
||||
CompositeDecoder.DECODE_DONE -> break
|
||||
}
|
||||
}
|
||||
|
||||
MangaListFilter(
|
||||
query = query,
|
||||
tags = tags,
|
||||
tagsExclude = tagsExclude,
|
||||
locale = locale,
|
||||
originalLocale = originalLocale,
|
||||
states = states,
|
||||
contentRating = contentRating,
|
||||
types = types,
|
||||
demographics = demographics,
|
||||
year = year,
|
||||
yearFrom = yearFrom,
|
||||
yearTo = yearTo,
|
||||
author = author,
|
||||
)
|
||||
}
|
||||
|
||||
private object MangaTagSerializer : KSerializer<MangaTag> {
|
||||
|
||||
override val descriptor: SerialDescriptor = buildClassSerialDescriptor(MangaTag::class.java.name) {
|
||||
element<String>("title")
|
||||
element<String>("key")
|
||||
element<String>("source")
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: MangaTag) = encoder.encodeStructure(descriptor) {
|
||||
encodeStringElement(descriptor, 0, value.title)
|
||||
encodeStringElement(descriptor, 1, value.key)
|
||||
encodeStringElement(descriptor, 2, value.source.name)
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): MangaTag = decoder.decodeStructure(descriptor) {
|
||||
var title: String? = null
|
||||
var key: String? = null
|
||||
var source: String? = null
|
||||
|
||||
while (true) {
|
||||
when (decodeElementIndex(descriptor)) {
|
||||
0 -> title = decodeStringElement(descriptor, 0)
|
||||
1 -> key = decodeStringElement(descriptor, 1)
|
||||
2 -> source = decodeStringElement(descriptor, 2)
|
||||
CompositeDecoder.DECODE_DONE -> break
|
||||
}
|
||||
}
|
||||
|
||||
MangaTag(
|
||||
title = title ?: error("Missing 'title' field"),
|
||||
key = key ?: error("Missing 'key' field"),
|
||||
source = MangaSource(source),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.koitharu.kotatsu.filter.data
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonIgnoreUnknownKeys
|
||||
import org.koitharu.kotatsu.core.model.MangaSourceSerializer
|
||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
|
||||
@Serializable
|
||||
@JsonIgnoreUnknownKeys
|
||||
data class PersistableFilter(
|
||||
@SerialName("name")
|
||||
val name: String,
|
||||
@Serializable(with = MangaSourceSerializer::class)
|
||||
@SerialName("source")
|
||||
val source: MangaSource,
|
||||
@Serializable(with = MangaListFilterSerializer::class)
|
||||
@SerialName("filter")
|
||||
val filter: MangaListFilter,
|
||||
) {
|
||||
|
||||
val id: Int
|
||||
get() = filter.hashCode()
|
||||
|
||||
companion object {
|
||||
|
||||
const val MAX_TITLE_LENGTH = 18
|
||||
}
|
||||
}
|
||||
@@ -1,152 +1,99 @@
|
||||
package org.koitharu.kotatsu.filter.data
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import org.koitharu.kotatsu.parsers.model.ContentRating
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.model.Demographic
|
||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import android.content.Context
|
||||
import androidx.core.content.edit
|
||||
import dagger.Reusable
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.koitharu.kotatsu.core.util.ext.observeChanges
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import javax.inject.Inject
|
||||
|
||||
@Singleton
|
||||
@Reusable
|
||||
class SavedFiltersRepository @Inject constructor(
|
||||
@ApplicationContext context: Context,
|
||||
@ApplicationContext private val context: Context,
|
||||
) {
|
||||
private val prefs: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
private val scope = CoroutineScope(Dispatchers.Default)
|
||||
|
||||
private val keyRoot = "saved_filters_v1"
|
||||
fun observeAll(source: MangaSource): Flow<List<PersistableFilter>> = getPrefs(source).observeChanges()
|
||||
.onStart { emit(null) }
|
||||
.map {
|
||||
getAll(source)
|
||||
}.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.Default)
|
||||
|
||||
private val state = MutableStateFlow<Map<String, List<Preset>>>(emptyMap())
|
||||
|
||||
init {
|
||||
scope.launch { loadAll() }
|
||||
}
|
||||
|
||||
data class Preset(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val source: String,
|
||||
val payload: JSONObject,
|
||||
)
|
||||
|
||||
fun observe(source: String): StateFlow<List<Preset>> = MutableStateFlow(state.value[source].orEmpty()).also { out ->
|
||||
scope.launch {
|
||||
state.collect { all -> out.value = all[source].orEmpty() }
|
||||
suspend fun getAll(source: MangaSource): List<PersistableFilter> = withContext(Dispatchers.Default) {
|
||||
val prefs = getPrefs(source)
|
||||
val keys = prefs.all.keys.filter { it.startsWith(FILTER_PREFIX) }
|
||||
keys.mapNotNull { key ->
|
||||
val value = prefs.getString(key, null) ?: return@mapNotNull null
|
||||
try {
|
||||
Json.decodeFromString(value)
|
||||
} catch (e: SerializationException) {
|
||||
e.printStackTraceDebug()
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun list(source: String): List<Preset> = state.value[source].orEmpty()
|
||||
|
||||
fun save(source: String, name: String, filter: MangaListFilter): Preset {
|
||||
val nowId = System.currentTimeMillis()
|
||||
val preset = Preset(
|
||||
id = nowId,
|
||||
suspend fun save(
|
||||
source: MangaSource,
|
||||
name: String,
|
||||
filter: MangaListFilter,
|
||||
): PersistableFilter = withContext(Dispatchers.Default) {
|
||||
val persistableFilter = PersistableFilter(
|
||||
name = name,
|
||||
source = source,
|
||||
payload = serializeFilter(filter),
|
||||
filter = filter,
|
||||
)
|
||||
val list = list(source) + preset
|
||||
persist(source, list)
|
||||
return preset
|
||||
persist(source, persistableFilter)
|
||||
persistableFilter
|
||||
}
|
||||
|
||||
fun rename(source: String, id: Long, newName: String) {
|
||||
val list = list(source).map { if (it.id == id) it.copy(name = newName) else it }
|
||||
persist(source, list)
|
||||
suspend fun rename(source: MangaSource, id: Int, newName: String) = withContext(Dispatchers.Default) {
|
||||
val filter = load(source, id) ?: return@withContext
|
||||
persist(source, filter.copy(name = newName))
|
||||
}
|
||||
|
||||
fun delete(source: String, id: Long) {
|
||||
val list = list(source).filterNot { it.id == id }
|
||||
persist(source, list)
|
||||
}
|
||||
|
||||
private fun persist(source: String, list: List<Preset>) {
|
||||
val root = JSONObject(prefs.getString(keyRoot, "{}"))
|
||||
root.put(source, JSONArray(list.map { presetToJson(it) }))
|
||||
prefs.edit { putString(keyRoot, root.toString()) }
|
||||
state.value = state.value.toMutableMap().also { it[source] = list }
|
||||
}
|
||||
|
||||
private fun loadAll() {
|
||||
val root = JSONObject(prefs.getString(keyRoot, "{}"))
|
||||
val map = mutableMapOf<String, List<Preset>>()
|
||||
for (key in root.keys()) {
|
||||
val arr = root.optJSONArray(key) ?: continue
|
||||
map[key] = (0 until arr.length()).mapNotNull { i -> jsonToPreset(arr.optJSONObject(i), key) }
|
||||
suspend fun delete(source: MangaSource, id: Int) = withContext(Dispatchers.Default) {
|
||||
val prefs = getPrefs(source)
|
||||
prefs.edit(commit = true) {
|
||||
remove(FILTER_PREFIX + id)
|
||||
}
|
||||
state.value = map
|
||||
}
|
||||
|
||||
private fun presetToJson(p: Preset): JSONObject = JSONObject().apply {
|
||||
put("id", p.id)
|
||||
put("name", p.name)
|
||||
put("payload", p.payload)
|
||||
private fun persist(source: MangaSource, persistableFilter: PersistableFilter) {
|
||||
val prefs = getPrefs(source)
|
||||
val json = Json.encodeToString(persistableFilter)
|
||||
prefs.edit(commit = true) {
|
||||
putString(FILTER_PREFIX + persistableFilter.id, json)
|
||||
}
|
||||
}
|
||||
|
||||
private fun jsonToPreset(obj: JSONObject?, source: String): Preset? {
|
||||
obj ?: return null
|
||||
val id = obj.optLong("id", 0L)
|
||||
val name = obj.optString("name", null) ?: return null
|
||||
val payload = obj.optJSONObject("payload") ?: return null
|
||||
return Preset(id, name, source, payload)
|
||||
private fun load(source: MangaSource, id: Int): PersistableFilter? {
|
||||
val prefs = getPrefs(source)
|
||||
val json = prefs.getString(FILTER_PREFIX + id, null) ?: return null
|
||||
return try {
|
||||
Json.decodeFromString<PersistableFilter>(json)
|
||||
} catch (e: SerializationException) {
|
||||
e.printStackTraceDebug()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun serializeFilter(f: MangaListFilter): JSONObject = JSONObject().apply {
|
||||
put("query", f.query)
|
||||
put("author", f.author)
|
||||
put("locale", f.locale?.toLanguageTag())
|
||||
put("originalLocale", f.originalLocale?.toLanguageTag())
|
||||
put("states", JSONArray(f.states.map { it.name }))
|
||||
put("contentRating", JSONArray(f.contentRating.map { it.name }))
|
||||
put("types", JSONArray(f.types.map { it.name }))
|
||||
put("demographics", JSONArray(f.demographics.map { it.name }))
|
||||
put("tags", JSONArray(f.tags.map { it.key }))
|
||||
put("tagsExclude", JSONArray(f.tagsExclude.map { it.key }))
|
||||
put("year", f.year)
|
||||
put("yearFrom", f.yearFrom)
|
||||
put("yearTo", f.yearTo)
|
||||
}
|
||||
private fun getPrefs(source: MangaSource) = context.getSharedPreferences(source.name, Context.MODE_PRIVATE)
|
||||
|
||||
fun deserializeFilter(
|
||||
obj: JSONObject,
|
||||
resolveTags: (Set<String>) -> Set<MangaTag>,
|
||||
): MangaListFilter {
|
||||
return MangaListFilter(
|
||||
query = obj.optString("query").takeIf { it.isNotEmpty() },
|
||||
author = obj.optString("author").takeIf { it.isNotEmpty() },
|
||||
locale = obj.optString("locale").takeIf { it.isNotEmpty() }?.let { Locale.forLanguageTag(it) },
|
||||
originalLocale = obj.optString("originalLocale").takeIf { it.isNotEmpty() }?.let { Locale.forLanguageTag(it) },
|
||||
states = obj.optJSONArray("states")?.toStringSet()?.mapNotNull { runCatching { MangaState.valueOf(it) }.getOrNull() }?.toSet().orEmpty(),
|
||||
contentRating = obj.optJSONArray("contentRating")?.toStringSet()?.mapNotNull { runCatching { ContentRating.valueOf(it) }.getOrNull() }?.toSet().orEmpty(),
|
||||
types = obj.optJSONArray("types")?.toStringSet()?.mapNotNull { runCatching { ContentType.valueOf(it) }.getOrNull() }?.toSet().orEmpty(),
|
||||
demographics = obj.optJSONArray("demographics")?.toStringSet()?.mapNotNull { runCatching { Demographic.valueOf(it) }.getOrNull() }?.toSet().orEmpty(),
|
||||
tags = resolveTags(obj.optJSONArray("tags")?.toStringSet().orEmpty()).toSet(),
|
||||
tagsExclude = resolveTags(obj.optJSONArray("tagsExclude")?.toStringSet().orEmpty()).toSet(),
|
||||
year = obj.optInt("year"),
|
||||
yearFrom = obj.optInt("yearFrom"),
|
||||
yearTo = obj.optInt("yearTo"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun JSONArray.toStringSet(): Set<String> = buildSet {
|
||||
for (i in 0 until length()) {
|
||||
val v = optString(i)
|
||||
if (!v.isNullOrEmpty()) add(v)
|
||||
private companion object {
|
||||
|
||||
const val FILTER_PREFIX = "__pf_"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,19 +15,18 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.model.unwrap
|
||||
import org.koitharu.kotatsu.core.util.LocaleComparator
|
||||
import org.koitharu.kotatsu.core.util.ext.asFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.lifecycleScope
|
||||
import org.koitharu.kotatsu.core.util.ext.sortedByOrdinal
|
||||
import org.koitharu.kotatsu.core.util.ext.sortedWithSafe
|
||||
import org.koitharu.kotatsu.filter.data.PersistableFilter
|
||||
import org.koitharu.kotatsu.filter.data.SavedFiltersRepository
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterProperty
|
||||
import org.koitharu.kotatsu.filter.ui.tags.TagTitleComparator
|
||||
@@ -46,7 +45,6 @@ import org.koitharu.kotatsu.parsers.util.nullIfEmpty
|
||||
import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy
|
||||
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
|
||||
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
|
||||
import org.json.JSONObject
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
@@ -66,27 +64,10 @@ class FilterCoordinator @Inject constructor(
|
||||
|
||||
private val currentListFilter = MutableStateFlow(MangaListFilter.EMPTY)
|
||||
private val currentSortOrder = MutableStateFlow(repository.defaultSortOrder)
|
||||
private val currentPresetId = MutableStateFlow<Long?>(null)
|
||||
private var lastAppliedPayload: JSONObject? = null
|
||||
|
||||
private val availableSortOrders = repository.sortOrders
|
||||
private val filterOptions = suspendLazy { repository.getFilterOptions() }
|
||||
|
||||
init {
|
||||
coroutineScope.launch {
|
||||
currentListFilter.collect { lf ->
|
||||
val applied = lastAppliedPayload
|
||||
if (applied != null) {
|
||||
val cur = savedFiltersRepository.serializeFilter(lf)
|
||||
if (cur.toString() != applied.toString()) {
|
||||
currentPresetId.value = null
|
||||
lastAppliedPayload = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val capabilities = repository.filterCapabilities
|
||||
|
||||
val mangaSource: MangaSource
|
||||
@@ -273,11 +254,15 @@ class FilterCoordinator @Inject constructor(
|
||||
MutableStateFlow(FilterProperty.EMPTY)
|
||||
}
|
||||
|
||||
val savedPresets: StateFlow<List<SavedFiltersRepository.Preset>> =
|
||||
savedFiltersRepository.observe(repository.source.unwrap().name)
|
||||
.stateIn(coroutineScope, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
val selectedPresetId: StateFlow<Long?> = currentPresetId
|
||||
val savedFilters: StateFlow<FilterProperty<PersistableFilter>> = combine(
|
||||
savedFiltersRepository.observeAll(repository.source),
|
||||
currentListFilter,
|
||||
) { available, applied ->
|
||||
FilterProperty(
|
||||
availableItems = available,
|
||||
selectedItems = setOfNotNull(available.find { it.filter == applied }),
|
||||
)
|
||||
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.EMPTY)
|
||||
|
||||
fun reset() {
|
||||
currentListFilter.value = MangaListFilter.EMPTY
|
||||
@@ -313,36 +298,16 @@ class FilterCoordinator @Inject constructor(
|
||||
set(newFilter)
|
||||
}
|
||||
|
||||
fun saveCurrentPreset(name: String) {
|
||||
val preset = savedFiltersRepository.save(repository.source.unwrap().name, name, currentListFilter.value)
|
||||
currentPresetId.value = preset.id
|
||||
lastAppliedPayload = preset.payload
|
||||
fun saveCurrentFilter(name: String) = coroutineScope.launch {
|
||||
savedFiltersRepository.save(repository.source, name, currentListFilter.value)
|
||||
}
|
||||
|
||||
fun applyPreset(preset: SavedFiltersRepository.Preset) {
|
||||
coroutineScope.launch {
|
||||
val available = filterOptions.asFlow().map { it.getOrNull()?.availableTags.orEmpty() }.first()
|
||||
val byKey: (Set<String>) -> Set<MangaTag> = { keys ->
|
||||
val all = available.associateBy { it.key }
|
||||
keys.mapNotNull { all[it] }.toSet()
|
||||
}
|
||||
val filter = savedFiltersRepository.deserializeFilter(preset.payload, byKey)
|
||||
setAdjusted(filter)
|
||||
currentPresetId.value = preset.id
|
||||
lastAppliedPayload = preset.payload
|
||||
}
|
||||
fun renameSavedFilter(id: Int, newName: String) = coroutineScope.launch {
|
||||
savedFiltersRepository.rename(repository.source, id, newName)
|
||||
}
|
||||
|
||||
fun renamePreset(id: Long, newName: String) {
|
||||
savedFiltersRepository.rename(repository.source.unwrap().name, id, newName)
|
||||
}
|
||||
|
||||
fun deletePreset(id: Long) {
|
||||
savedFiltersRepository.delete(repository.source.unwrap().name, id)
|
||||
if (currentPresetId.value == id) {
|
||||
currentPresetId.value = null
|
||||
lastAppliedPayload = null
|
||||
}
|
||||
fun deleteSavedFilter(id: Int) = coroutineScope.launch {
|
||||
savedFiltersRepository.delete(repository.source, id)
|
||||
}
|
||||
|
||||
fun setQuery(value: String?) {
|
||||
@@ -517,57 +482,57 @@ class FilterCoordinator @Inject constructor(
|
||||
emit(Result.failure(it))
|
||||
}
|
||||
|
||||
private fun <T> List<T>.addFirstDistinct(other: Collection<T>): List<T> {
|
||||
val result = ArrayDeque<T>(this.size + other.size)
|
||||
result.addAll(this)
|
||||
for (item in other) {
|
||||
if (item !in result) {
|
||||
result.addFirst(item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
private fun <T> List<T>.addFirstDistinct(other: Collection<T>): List<T> {
|
||||
val result = ArrayDeque<T>(this.size + other.size)
|
||||
result.addAll(this)
|
||||
for (item in other) {
|
||||
if (item !in result) {
|
||||
result.addFirst(item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun <T> List<T>.addFirstDistinct(item: T): List<T> {
|
||||
val result = ArrayDeque<T>(this.size + 1)
|
||||
result.addAll(this)
|
||||
if (item !in result) {
|
||||
result.addFirst(item)
|
||||
}
|
||||
return result
|
||||
}
|
||||
private fun <T> List<T>.addFirstDistinct(item: T): List<T> {
|
||||
val result = ArrayDeque<T>(this.size + 1)
|
||||
result.addAll(this)
|
||||
if (item !in result) {
|
||||
result.addFirst(item)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
data class Snapshot(
|
||||
val sortOrder: SortOrder,
|
||||
val listFilter: MangaListFilter,
|
||||
)
|
||||
data class Snapshot(
|
||||
val sortOrder: SortOrder,
|
||||
val listFilter: MangaListFilter,
|
||||
)
|
||||
|
||||
interface Owner {
|
||||
interface Owner {
|
||||
|
||||
val filterCoordinator: FilterCoordinator
|
||||
}
|
||||
val filterCoordinator: FilterCoordinator
|
||||
}
|
||||
|
||||
companion object {
|
||||
companion object {
|
||||
|
||||
private const val TAGS_LIMIT = 12
|
||||
private val MAX_YEAR = Calendar.getInstance()[Calendar.YEAR] + 1
|
||||
private const val TAGS_LIMIT = 12
|
||||
private val MAX_YEAR = Calendar.getInstance()[Calendar.YEAR] + 1
|
||||
|
||||
fun find(fragment: Fragment): FilterCoordinator? {
|
||||
(fragment.activity as? Owner)?.let {
|
||||
return it.filterCoordinator
|
||||
}
|
||||
var f = fragment
|
||||
while (true) {
|
||||
(f as? Owner)?.let {
|
||||
return it.filterCoordinator
|
||||
}
|
||||
f = f.parentFragment ?: break
|
||||
}
|
||||
return null
|
||||
}
|
||||
fun find(fragment: Fragment): FilterCoordinator? {
|
||||
(fragment.activity as? Owner)?.let {
|
||||
return it.filterCoordinator
|
||||
}
|
||||
var f = fragment
|
||||
while (true) {
|
||||
(f as? Owner)?.let {
|
||||
return it.filterCoordinator
|
||||
}
|
||||
f = f.parentFragment ?: break
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun require(fragment: Fragment): FilterCoordinator {
|
||||
return find(fragment) ?: throw IllegalStateException("FilterCoordinator cannot be found")
|
||||
}
|
||||
}
|
||||
fun require(fragment: Fragment): FilterCoordinator {
|
||||
return find(fragment) ?: throw IllegalStateException("FilterCoordinator cannot be found")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
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.data.PersistableFilter
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel
|
||||
import org.koitharu.kotatsu.parsers.model.ContentRating
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
@@ -28,69 +29,75 @@ import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class FilterHeaderFragment : BaseFragment<FragmentFilterHeaderBinding>(), ChipsView.OnChipClickListener,
|
||||
ChipsView.OnChipCloseClickListener {
|
||||
ChipsView.OnChipCloseClickListener {
|
||||
|
||||
@Inject
|
||||
lateinit var filterHeaderProducer: FilterHeaderProducer
|
||||
@Inject
|
||||
lateinit var filterHeaderProducer: FilterHeaderProducer
|
||||
|
||||
private val filter: FilterCoordinator
|
||||
get() = (requireActivity() as FilterCoordinator.Owner).filterCoordinator
|
||||
private val filter: FilterCoordinator
|
||||
get() = (requireActivity() as FilterCoordinator.Owner).filterCoordinator
|
||||
|
||||
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFilterHeaderBinding {
|
||||
return FragmentFilterHeaderBinding.inflate(inflater, container, false)
|
||||
}
|
||||
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFilterHeaderBinding {
|
||||
return FragmentFilterHeaderBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewBindingCreated(binding: FragmentFilterHeaderBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
binding.chipsTags.onChipClickListener = this
|
||||
binding.chipsTags.onChipCloseClickListener = this
|
||||
filterHeaderProducer.observeHeader(filter)
|
||||
.flowOn(Dispatchers.Default)
|
||||
.observe(viewLifecycleOwner, ::onDataChanged)
|
||||
}
|
||||
override fun onViewBindingCreated(binding: FragmentFilterHeaderBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
binding.chipsTags.onChipClickListener = this
|
||||
binding.chipsTags.onChipCloseClickListener = this
|
||||
filterHeaderProducer.observeHeader(filter)
|
||||
.flowOn(Dispatchers.Default)
|
||||
.observe(viewLifecycleOwner, ::onDataChanged)
|
||||
}
|
||||
|
||||
override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat = insets
|
||||
override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat = insets
|
||||
|
||||
override fun onChipClick(chip: Chip, data: Any?) {
|
||||
when (data) {
|
||||
is MangaTag -> filter.toggleTag(data, !chip.isChecked)
|
||||
is String -> Unit
|
||||
null -> router.showTagsCatalogSheet(excludeMode = false)
|
||||
}
|
||||
}
|
||||
override fun onChipClick(chip: Chip, data: Any?) {
|
||||
when (data) {
|
||||
is MangaTag -> filter.toggleTag(data, !chip.isChecked)
|
||||
is PersistableFilter -> if (chip.isChecked) {
|
||||
filter.reset()
|
||||
} else {
|
||||
filter.setAdjusted(data.filter)
|
||||
}
|
||||
|
||||
override fun onChipCloseClick(chip: Chip, data: Any?) {
|
||||
when (data) {
|
||||
is String -> if (data == filter.snapshot().listFilter.author) {
|
||||
filter.setAuthor(null)
|
||||
} else {
|
||||
filter.setQuery(null)
|
||||
}
|
||||
is String -> Unit
|
||||
null -> router.showTagsCatalogSheet(excludeMode = false)
|
||||
}
|
||||
}
|
||||
|
||||
is ContentRating -> filter.toggleContentRating(data, false)
|
||||
is Demographic -> filter.toggleDemographic(data, false)
|
||||
is ContentType -> filter.toggleContentType(data, false)
|
||||
is MangaState -> filter.toggleState(data, false)
|
||||
is Locale -> filter.setLocale(null)
|
||||
is Int -> filter.setYear(YEAR_UNKNOWN)
|
||||
is IntRange -> filter.setYearRange(YEAR_UNKNOWN, YEAR_UNKNOWN)
|
||||
}
|
||||
}
|
||||
override fun onChipCloseClick(chip: Chip, data: Any?) {
|
||||
when (data) {
|
||||
is String -> if (data == filter.snapshot().listFilter.author) {
|
||||
filter.setAuthor(null)
|
||||
} else {
|
||||
filter.setQuery(null)
|
||||
}
|
||||
|
||||
private fun onDataChanged(header: FilterHeaderModel) {
|
||||
val binding = viewBinding ?: return
|
||||
val chips = header.chips
|
||||
if (chips.isEmpty()) {
|
||||
binding.chipsTags.setChips(emptyList())
|
||||
binding.root.isVisible = false
|
||||
return
|
||||
}
|
||||
binding.chipsTags.setChips(header.chips)
|
||||
binding.root.isVisible = true
|
||||
if (binding.root.context.isAnimationsEnabled) {
|
||||
binding.scrollView.smoothScrollTo(0, 0)
|
||||
} else {
|
||||
binding.scrollView.scrollTo(0, 0)
|
||||
}
|
||||
}
|
||||
is ContentRating -> filter.toggleContentRating(data, false)
|
||||
is Demographic -> filter.toggleDemographic(data, false)
|
||||
is ContentType -> filter.toggleContentType(data, false)
|
||||
is MangaState -> filter.toggleState(data, false)
|
||||
is Locale -> filter.setLocale(null)
|
||||
is Int -> filter.setYear(YEAR_UNKNOWN)
|
||||
is IntRange -> filter.setYearRange(YEAR_UNKNOWN, YEAR_UNKNOWN)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onDataChanged(header: FilterHeaderModel) {
|
||||
val binding = viewBinding ?: return
|
||||
val chips = header.chips
|
||||
if (chips.isEmpty()) {
|
||||
binding.chipsTags.setChips(emptyList())
|
||||
binding.root.isVisible = false
|
||||
return
|
||||
}
|
||||
binding.chipsTags.setChips(header.chips)
|
||||
binding.root.isVisible = true
|
||||
if (binding.root.context.isAnimationsEnabled) {
|
||||
binding.scrollView.smoothScrollTo(0, 0)
|
||||
} else {
|
||||
binding.scrollView.scrollTo(0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import kotlinx.coroutines.flow.combine
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.titleResId
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.filter.data.PersistableFilter
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterProperty
|
||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||
@@ -17,143 +18,161 @@ import javax.inject.Inject
|
||||
import androidx.appcompat.R as appcompatR
|
||||
|
||||
class FilterHeaderProducer @Inject constructor(
|
||||
private val searchRepository: MangaSearchRepository,
|
||||
private val searchRepository: MangaSearchRepository,
|
||||
) {
|
||||
|
||||
fun observeHeader(filterCoordinator: FilterCoordinator): Flow<FilterHeaderModel> {
|
||||
return combine(filterCoordinator.tags, filterCoordinator.observe()) { tags, snapshot ->
|
||||
val chipList = createChipsList(
|
||||
source = filterCoordinator.mangaSource,
|
||||
capabilities = filterCoordinator.capabilities,
|
||||
tagsProperty = tags,
|
||||
snapshot = snapshot.listFilter,
|
||||
limit = 12,
|
||||
)
|
||||
FilterHeaderModel(
|
||||
chips = chipList,
|
||||
sortOrder = snapshot.sortOrder,
|
||||
isFilterApplied = !snapshot.listFilter.isEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
fun observeHeader(filterCoordinator: FilterCoordinator): Flow<FilterHeaderModel> {
|
||||
return combine(
|
||||
filterCoordinator.savedFilters,
|
||||
filterCoordinator.tags,
|
||||
filterCoordinator.observe(),
|
||||
) { saved, tags, snapshot ->
|
||||
val chipList = createChipsList(
|
||||
source = filterCoordinator.mangaSource,
|
||||
capabilities = filterCoordinator.capabilities,
|
||||
savedFilters = saved,
|
||||
tagsProperty = tags,
|
||||
snapshot = snapshot.listFilter,
|
||||
limit = 12,
|
||||
)
|
||||
FilterHeaderModel(
|
||||
chips = chipList,
|
||||
sortOrder = snapshot.sortOrder,
|
||||
isFilterApplied = !snapshot.listFilter.isEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createChipsList(
|
||||
source: MangaSource,
|
||||
capabilities: MangaListFilterCapabilities,
|
||||
tagsProperty: FilterProperty<MangaTag>,
|
||||
snapshot: MangaListFilter,
|
||||
limit: Int,
|
||||
): List<ChipsView.ChipModel> {
|
||||
val result = ArrayDeque<ChipsView.ChipModel>(limit + 3)
|
||||
if (snapshot.query.isNullOrEmpty() || capabilities.isSearchWithFiltersSupported) {
|
||||
val selectedTags = tagsProperty.selectedItems.toMutableSet()
|
||||
var tags = if (selectedTags.isEmpty()) {
|
||||
searchRepository.getTagsSuggestion("", limit, source)
|
||||
} else {
|
||||
searchRepository.getTagsSuggestion(selectedTags).take(limit)
|
||||
}
|
||||
if (tags.size < limit) {
|
||||
tags = tags + tagsProperty.availableItems.take(limit - tags.size)
|
||||
}
|
||||
if (tags.isEmpty() && selectedTags.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
for (tag in tags) {
|
||||
val model = ChipsView.ChipModel(
|
||||
title = tag.title,
|
||||
isChecked = selectedTags.remove(tag),
|
||||
data = tag,
|
||||
)
|
||||
if (model.isChecked) {
|
||||
result.addFirst(model)
|
||||
} else {
|
||||
result.addLast(model)
|
||||
}
|
||||
}
|
||||
for (tag in selectedTags) {
|
||||
val model = ChipsView.ChipModel(
|
||||
title = tag.title,
|
||||
isChecked = true,
|
||||
data = tag,
|
||||
)
|
||||
result.addFirst(model)
|
||||
}
|
||||
}
|
||||
snapshot.locale?.let {
|
||||
result.addFirst(
|
||||
ChipsView.ChipModel(
|
||||
title = it.getDisplayName(it).toTitleCase(it),
|
||||
icon = R.drawable.ic_language,
|
||||
isCloseable = true,
|
||||
data = it,
|
||||
),
|
||||
)
|
||||
}
|
||||
snapshot.types.forEach {
|
||||
result.addFirst(
|
||||
ChipsView.ChipModel(
|
||||
titleResId = it.titleResId,
|
||||
isCloseable = true,
|
||||
data = it,
|
||||
),
|
||||
)
|
||||
}
|
||||
snapshot.demographics.forEach {
|
||||
result.addFirst(
|
||||
ChipsView.ChipModel(
|
||||
titleResId = it.titleResId,
|
||||
isCloseable = true,
|
||||
data = it,
|
||||
),
|
||||
)
|
||||
}
|
||||
snapshot.contentRating.forEach {
|
||||
result.addFirst(
|
||||
ChipsView.ChipModel(
|
||||
titleResId = it.titleResId,
|
||||
isCloseable = true,
|
||||
data = it,
|
||||
),
|
||||
)
|
||||
}
|
||||
snapshot.states.forEach {
|
||||
result.addFirst(
|
||||
ChipsView.ChipModel(
|
||||
titleResId = it.titleResId,
|
||||
isCloseable = true,
|
||||
data = it,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (!snapshot.query.isNullOrEmpty()) {
|
||||
result.addFirst(
|
||||
ChipsView.ChipModel(
|
||||
title = snapshot.query,
|
||||
icon = appcompatR.drawable.abc_ic_search_api_material,
|
||||
isCloseable = true,
|
||||
data = snapshot.query,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (!snapshot.author.isNullOrEmpty()) {
|
||||
result.addFirst(
|
||||
ChipsView.ChipModel(
|
||||
title = snapshot.author,
|
||||
icon = R.drawable.ic_user,
|
||||
isCloseable = true,
|
||||
data = snapshot.author,
|
||||
),
|
||||
)
|
||||
}
|
||||
val hasTags = result.any { it.data is MangaTag }
|
||||
if (hasTags) {
|
||||
result.addFirst(moreTagsChip())
|
||||
}
|
||||
return result
|
||||
}
|
||||
private suspend fun createChipsList(
|
||||
source: MangaSource,
|
||||
capabilities: MangaListFilterCapabilities,
|
||||
savedFilters: FilterProperty<PersistableFilter>,
|
||||
tagsProperty: FilterProperty<MangaTag>,
|
||||
snapshot: MangaListFilter,
|
||||
limit: Int,
|
||||
): List<ChipsView.ChipModel> {
|
||||
val result = ArrayDeque<ChipsView.ChipModel>(savedFilters.availableItems.size + limit + 3)
|
||||
if (snapshot.query.isNullOrEmpty() || capabilities.isSearchWithFiltersSupported) {
|
||||
val selectedTags = tagsProperty.selectedItems.toMutableSet()
|
||||
var tags = if (selectedTags.isEmpty()) {
|
||||
searchRepository.getTagsSuggestion("", limit, source)
|
||||
} else {
|
||||
searchRepository.getTagsSuggestion(selectedTags).take(limit)
|
||||
}
|
||||
if (tags.size < limit) {
|
||||
tags = tags + tagsProperty.availableItems.take(limit - tags.size)
|
||||
}
|
||||
if (tags.isEmpty() && selectedTags.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
for (saved in savedFilters.availableItems) {
|
||||
val model = ChipsView.ChipModel(
|
||||
title = saved.name,
|
||||
isChecked = saved in savedFilters.selectedItems,
|
||||
data = saved,
|
||||
)
|
||||
if (model.isChecked) {
|
||||
result.addFirst(model)
|
||||
} else {
|
||||
result.addLast(model)
|
||||
}
|
||||
}
|
||||
for (tag in tags) {
|
||||
val model = ChipsView.ChipModel(
|
||||
title = tag.title,
|
||||
isChecked = selectedTags.remove(tag),
|
||||
data = tag,
|
||||
)
|
||||
if (model.isChecked) {
|
||||
result.addFirst(model)
|
||||
} else {
|
||||
result.addLast(model)
|
||||
}
|
||||
}
|
||||
for (tag in selectedTags) {
|
||||
val model = ChipsView.ChipModel(
|
||||
title = tag.title,
|
||||
isChecked = true,
|
||||
data = tag,
|
||||
)
|
||||
result.addFirst(model)
|
||||
}
|
||||
}
|
||||
snapshot.locale?.let {
|
||||
result.addFirst(
|
||||
ChipsView.ChipModel(
|
||||
title = it.getDisplayName(it).toTitleCase(it),
|
||||
icon = R.drawable.ic_language,
|
||||
isCloseable = true,
|
||||
data = it,
|
||||
),
|
||||
)
|
||||
}
|
||||
snapshot.types.forEach {
|
||||
result.addFirst(
|
||||
ChipsView.ChipModel(
|
||||
titleResId = it.titleResId,
|
||||
isCloseable = true,
|
||||
data = it,
|
||||
),
|
||||
)
|
||||
}
|
||||
snapshot.demographics.forEach {
|
||||
result.addFirst(
|
||||
ChipsView.ChipModel(
|
||||
titleResId = it.titleResId,
|
||||
isCloseable = true,
|
||||
data = it,
|
||||
),
|
||||
)
|
||||
}
|
||||
snapshot.contentRating.forEach {
|
||||
result.addFirst(
|
||||
ChipsView.ChipModel(
|
||||
titleResId = it.titleResId,
|
||||
isCloseable = true,
|
||||
data = it,
|
||||
),
|
||||
)
|
||||
}
|
||||
snapshot.states.forEach {
|
||||
result.addFirst(
|
||||
ChipsView.ChipModel(
|
||||
titleResId = it.titleResId,
|
||||
isCloseable = true,
|
||||
data = it,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (!snapshot.query.isNullOrEmpty()) {
|
||||
result.addFirst(
|
||||
ChipsView.ChipModel(
|
||||
title = snapshot.query,
|
||||
icon = appcompatR.drawable.abc_ic_search_api_material,
|
||||
isCloseable = true,
|
||||
data = snapshot.query,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (!snapshot.author.isNullOrEmpty()) {
|
||||
result.addFirst(
|
||||
ChipsView.ChipModel(
|
||||
title = snapshot.author,
|
||||
icon = R.drawable.ic_user,
|
||||
isCloseable = true,
|
||||
data = snapshot.author,
|
||||
),
|
||||
)
|
||||
}
|
||||
val hasTags = result.any { it.data is MangaTag }
|
||||
if (hasTags) {
|
||||
result.addFirst(moreTagsChip())
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun moreTagsChip() = ChipsView.ChipModel(
|
||||
titleResId = R.string.genres,
|
||||
icon = R.drawable.ic_drawer_menu_open,
|
||||
)
|
||||
private fun moreTagsChip() = ChipsView.ChipModel(
|
||||
titleResId = R.string.genres,
|
||||
icon = R.drawable.ic_drawer_menu_open,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,20 +1,32 @@
|
||||
package org.koitharu.kotatsu.filter.ui.sheet
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.InputFilter
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updatePadding
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.slider.RangeSlider
|
||||
import com.google.android.material.slider.Slider
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.titleResId
|
||||
import org.koitharu.kotatsu.core.nav.router
|
||||
import org.koitharu.kotatsu.core.ui.dialog.buildAlertDialog
|
||||
import org.koitharu.kotatsu.core.ui.dialog.setEditText
|
||||
import org.koitharu.kotatsu.core.ui.model.titleRes
|
||||
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
@@ -26,8 +38,9 @@ import org.koitharu.kotatsu.core.util.ext.parentView
|
||||
import org.koitharu.kotatsu.core.util.ext.setValueRounded
|
||||
import org.koitharu.kotatsu.core.util.ext.setValuesRounded
|
||||
import org.koitharu.kotatsu.databinding.SheetFilterBinding
|
||||
import org.koitharu.kotatsu.filter.data.PersistableFilter
|
||||
import org.koitharu.kotatsu.filter.data.PersistableFilter.Companion.MAX_TITLE_LENGTH
|
||||
import org.koitharu.kotatsu.filter.ui.FilterCoordinator
|
||||
import org.koitharu.kotatsu.filter.data.SavedFiltersRepository
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterProperty
|
||||
import org.koitharu.kotatsu.parsers.model.ContentRating
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
@@ -38,417 +51,451 @@ import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.parsers.model.YEAR_UNKNOWN
|
||||
import org.koitharu.kotatsu.parsers.util.toIntUp
|
||||
import java.util.Locale
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import android.widget.EditText
|
||||
|
||||
class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
|
||||
AdapterView.OnItemSelectedListener,
|
||||
ChipsView.OnChipClickListener {
|
||||
AdapterView.OnItemSelectedListener,
|
||||
View.OnClickListener,
|
||||
ChipsView.OnChipClickListener,
|
||||
ChipsView.OnChipLongClickListener,
|
||||
ChipsView.OnChipCloseClickListener {
|
||||
|
||||
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetFilterBinding {
|
||||
return SheetFilterBinding.inflate(inflater, container, false)
|
||||
}
|
||||
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetFilterBinding {
|
||||
return SheetFilterBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
private fun onSavedPresetsChanged(list: List<SavedFiltersRepository.Preset>, selectedId: Long?) {
|
||||
val b = viewBinding ?: return
|
||||
if (list.isEmpty()) {
|
||||
b.layoutSavedFilters.isGone = true
|
||||
b.chipsSavedFilters.setChips(emptyList())
|
||||
return
|
||||
}
|
||||
b.layoutSavedFilters.isGone = false
|
||||
val chips = list.map { p ->
|
||||
ChipsView.ChipModel(
|
||||
title = p.name,
|
||||
isChecked = p.id == selectedId,
|
||||
data = p,
|
||||
)
|
||||
}
|
||||
b.chipsSavedFilters.setChips(chips)
|
||||
}
|
||||
override fun onViewBindingCreated(binding: SheetFilterBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
if (dialog == null) {
|
||||
binding.layoutBody.updatePadding(top = binding.layoutBody.paddingBottom)
|
||||
binding.scrollView.scrollIndicators = 0
|
||||
}
|
||||
val filter = FilterCoordinator.require(this)
|
||||
filter.sortOrder.observe(viewLifecycleOwner, this::onSortOrderChanged)
|
||||
filter.locale.observe(viewLifecycleOwner, this::onLocaleChanged)
|
||||
filter.originalLocale.observe(viewLifecycleOwner, this::onOriginalLocaleChanged)
|
||||
filter.tags.observe(viewLifecycleOwner, this::onTagsChanged)
|
||||
filter.tagsExcluded.observe(viewLifecycleOwner, this::onTagsExcludedChanged)
|
||||
filter.states.observe(viewLifecycleOwner, this::onStateChanged)
|
||||
filter.contentTypes.observe(viewLifecycleOwner, this::onContentTypesChanged)
|
||||
filter.contentRating.observe(viewLifecycleOwner, this::onContentRatingChanged)
|
||||
filter.demographics.observe(viewLifecycleOwner, this::onDemographicsChanged)
|
||||
filter.year.observe(viewLifecycleOwner, this::onYearChanged)
|
||||
filter.yearRange.observe(viewLifecycleOwner, this::onYearRangeChanged)
|
||||
filter.savedFilters.observe(viewLifecycleOwner, ::onSavedPresetsChanged)
|
||||
|
||||
private fun promptPresetName(onSubmit: (String) -> Unit) {
|
||||
val ctx = requireContext()
|
||||
val input = EditText(ctx)
|
||||
MaterialAlertDialogBuilder(ctx)
|
||||
.setTitle(R.string.enter_name)
|
||||
.setView(input)
|
||||
.setPositiveButton(R.string.save) { d, _ ->
|
||||
val text = input.text?.toString()?.trim()
|
||||
if (!text.isNullOrEmpty()) onSubmit(text)
|
||||
d.dismiss()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
|
||||
.show()
|
||||
}
|
||||
binding.layoutGenres.setTitle(
|
||||
if (filter.capabilities.isMultipleTagsSupported) {
|
||||
R.string.genres
|
||||
} else {
|
||||
R.string.genre
|
||||
},
|
||||
)
|
||||
binding.spinnerLocale.onItemSelectedListener = this
|
||||
binding.spinnerOriginalLocale.onItemSelectedListener = this
|
||||
binding.spinnerOrder.onItemSelectedListener = this
|
||||
binding.chipsSavedFilters.onChipClickListener = this
|
||||
binding.chipsState.onChipClickListener = this
|
||||
binding.chipsTypes.onChipClickListener = this
|
||||
binding.chipsContentRating.onChipClickListener = this
|
||||
binding.chipsDemographics.onChipClickListener = this
|
||||
binding.chipsGenres.onChipClickListener = this
|
||||
binding.chipsGenresExclude.onChipClickListener = this
|
||||
binding.chipsSavedFilters.onChipLongClickListener = this
|
||||
binding.chipsSavedFilters.onChipCloseClickListener = this
|
||||
binding.sliderYear.addOnChangeListener(this::onSliderValueChange)
|
||||
binding.sliderYearsRange.addOnChangeListener(this::onRangeSliderValueChange)
|
||||
binding.layoutGenres.setOnMoreButtonClickListener {
|
||||
router.showTagsCatalogSheet(excludeMode = false)
|
||||
}
|
||||
binding.layoutGenresExclude.setOnMoreButtonClickListener {
|
||||
router.showTagsCatalogSheet(excludeMode = true)
|
||||
}
|
||||
filter.observe().observe(viewLifecycleOwner) {
|
||||
binding.buttonReset.isEnabled = it.listFilter.isNotEmpty()
|
||||
}
|
||||
combine(
|
||||
filter.observe().map { it.listFilter.isNotEmpty() }.distinctUntilChanged(),
|
||||
filter.savedFilters.map { it.selectedItems.isEmpty() }.distinctUntilChanged(),
|
||||
Boolean::and,
|
||||
).flowOn(Dispatchers.Default)
|
||||
.observe(viewLifecycleOwner) {
|
||||
binding.buttonSave.isEnabled = it
|
||||
}
|
||||
binding.buttonSave.setOnClickListener(this)
|
||||
binding.buttonReset.setOnClickListener(this)
|
||||
}
|
||||
|
||||
private fun showPresetOptions(filter: FilterCoordinator, preset: SavedFiltersRepository.Preset) {
|
||||
val ctx = requireContext()
|
||||
val items = arrayOf(getString(R.string.edit), getString(R.string.delete))
|
||||
MaterialAlertDialogBuilder(ctx)
|
||||
.setItems(items) { d, which ->
|
||||
when (which) {
|
||||
0 -> promptRename(filter, preset)
|
||||
1 -> filter.deletePreset(preset.id)
|
||||
}
|
||||
d.dismiss()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
|
||||
val typeMask = WindowInsetsCompat.Type.systemBars()
|
||||
viewBinding?.layoutBottom?.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = insets.getInsets(typeMask).bottom
|
||||
}
|
||||
return insets.consume(v, typeMask, bottom = true)
|
||||
}
|
||||
|
||||
private fun promptRename(filter: FilterCoordinator, preset: SavedFiltersRepository.Preset) {
|
||||
val ctx = requireContext()
|
||||
val input = EditText(ctx)
|
||||
input.setText(preset.name)
|
||||
MaterialAlertDialogBuilder(ctx)
|
||||
.setTitle(R.string.edit)
|
||||
.setView(input)
|
||||
.setPositiveButton(R.string.save) { d, _ ->
|
||||
val text = input.text?.toString()?.trim()
|
||||
if (!text.isNullOrEmpty()) filter.renamePreset(preset.id, text)
|
||||
d.dismiss()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
|
||||
.show()
|
||||
}
|
||||
override fun onClick(v: View) {
|
||||
when (v.id) {
|
||||
R.id.button_reset -> FilterCoordinator.require(this).reset()
|
||||
R.id.button_save -> onSaveFilterClick()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewBindingCreated(binding: SheetFilterBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
if (dialog == null) {
|
||||
binding.layoutBody.updatePadding(top = binding.layoutBody.paddingBottom)
|
||||
binding.scrollView.scrollIndicators = 0
|
||||
}
|
||||
val filter = FilterCoordinator.require(this)
|
||||
filter.sortOrder.observe(viewLifecycleOwner, this::onSortOrderChanged)
|
||||
filter.locale.observe(viewLifecycleOwner, this::onLocaleChanged)
|
||||
filter.originalLocale.observe(viewLifecycleOwner, this::onOriginalLocaleChanged)
|
||||
filter.tags.observe(viewLifecycleOwner, this::onTagsChanged)
|
||||
filter.tagsExcluded.observe(viewLifecycleOwner, this::onTagsExcludedChanged)
|
||||
filter.states.observe(viewLifecycleOwner, this::onStateChanged)
|
||||
filter.contentTypes.observe(viewLifecycleOwner, this::onContentTypesChanged)
|
||||
filter.contentRating.observe(viewLifecycleOwner, this::onContentRatingChanged)
|
||||
filter.demographics.observe(viewLifecycleOwner, this::onDemographicsChanged)
|
||||
filter.year.observe(viewLifecycleOwner, this::onYearChanged)
|
||||
filter.yearRange.observe(viewLifecycleOwner, this::onYearRangeChanged)
|
||||
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
|
||||
val filter = FilterCoordinator.require(this)
|
||||
when (parent.id) {
|
||||
R.id.spinner_order -> filter.setSortOrder(filter.sortOrder.value.availableItems[position])
|
||||
R.id.spinner_locale -> filter.setLocale(filter.locale.value.availableItems[position])
|
||||
R.id.spinner_original_locale -> filter.setOriginalLocale(filter.originalLocale.value.availableItems[position])
|
||||
}
|
||||
}
|
||||
|
||||
binding.layoutGenres.setTitle(
|
||||
if (filter.capabilities.isMultipleTagsSupported) {
|
||||
R.string.genres
|
||||
} else {
|
||||
R.string.genre
|
||||
},
|
||||
)
|
||||
binding.spinnerLocale.onItemSelectedListener = this
|
||||
binding.spinnerOriginalLocale.onItemSelectedListener = this
|
||||
binding.spinnerOrder.onItemSelectedListener = this
|
||||
binding.chipsState.onChipClickListener = this
|
||||
binding.chipsTypes.onChipClickListener = this
|
||||
binding.chipsContentRating.onChipClickListener = this
|
||||
binding.chipsDemographics.onChipClickListener = this
|
||||
binding.chipsGenres.onChipClickListener = this
|
||||
binding.chipsGenresExclude.onChipClickListener = this
|
||||
binding.sliderYear.addOnChangeListener(this::onSliderValueChange)
|
||||
binding.sliderYearsRange.addOnChangeListener(this::onRangeSliderValueChange)
|
||||
binding.layoutGenres.setOnMoreButtonClickListener {
|
||||
router.showTagsCatalogSheet(excludeMode = false)
|
||||
}
|
||||
binding.layoutGenresExclude.setOnMoreButtonClickListener {
|
||||
router.showTagsCatalogSheet(excludeMode = true)
|
||||
}
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
|
||||
|
||||
binding.chipsSavedFilters.onChipClickListener = ChipsView.OnChipClickListener { chip, data ->
|
||||
when (data) {
|
||||
is SavedFiltersRepository.Preset -> filter.applyPreset(data)
|
||||
}
|
||||
}
|
||||
binding.chipsSavedFilters.onChipLongClickListener = ChipsView.OnChipLongClickListener { chip, data ->
|
||||
when (data) {
|
||||
is SavedFiltersRepository.Preset -> {
|
||||
showPresetOptions(filter, data)
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
private fun onSliderValueChange(slider: Slider, value: Float, fromUser: Boolean) {
|
||||
if (!fromUser) {
|
||||
return
|
||||
}
|
||||
val intValue = value.toInt()
|
||||
val filter = FilterCoordinator.require(this)
|
||||
when (slider.id) {
|
||||
R.id.slider_year -> filter.setYear(
|
||||
if (intValue <= slider.valueFrom.toIntUp()) {
|
||||
YEAR_UNKNOWN
|
||||
} else {
|
||||
intValue
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
filter.savedPresets.observe(viewLifecycleOwner) { list ->
|
||||
val selectedId = filter.selectedPresetId.value
|
||||
onSavedPresetsChanged(list, selectedId)
|
||||
}
|
||||
filter.selectedPresetId.observe(viewLifecycleOwner) { selectedId ->
|
||||
onSavedPresetsChanged(filter.savedPresets.value, selectedId)
|
||||
}
|
||||
private fun onRangeSliderValueChange(slider: RangeSlider, value: Float, fromUser: Boolean) {
|
||||
if (!fromUser) {
|
||||
return
|
||||
}
|
||||
val filter = FilterCoordinator.require(this)
|
||||
when (slider.id) {
|
||||
R.id.slider_yearsRange -> filter.setYearRange(
|
||||
valueFrom = slider.values.firstOrNull()?.let {
|
||||
if (it <= slider.valueFrom) YEAR_UNKNOWN else it.toInt()
|
||||
} ?: YEAR_UNKNOWN,
|
||||
valueTo = slider.values.lastOrNull()?.let {
|
||||
if (it >= slider.valueTo) YEAR_UNKNOWN else it.toInt()
|
||||
} ?: YEAR_UNKNOWN,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
filter.observe().observe(viewLifecycleOwner) {
|
||||
binding.buttonSaveFilter.isEnabled = filter.isFilterApplied
|
||||
}
|
||||
binding.buttonSaveFilter.setOnClickListener {
|
||||
promptPresetName { name ->
|
||||
filter.saveCurrentPreset(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun onChipClick(chip: Chip, data: Any?) {
|
||||
val filter = FilterCoordinator.require(this)
|
||||
when (data) {
|
||||
is MangaState -> filter.toggleState(data, !chip.isChecked)
|
||||
is MangaTag -> if (chip.parentView?.id == R.id.chips_genresExclude) {
|
||||
filter.toggleTagExclude(data, !chip.isChecked)
|
||||
} else {
|
||||
filter.toggleTag(data, !chip.isChecked)
|
||||
}
|
||||
|
||||
override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
|
||||
val typeMask = WindowInsetsCompat.Type.systemBars()
|
||||
viewBinding?.scrollView?.updatePadding(
|
||||
bottom = insets.getInsets(typeMask).bottom,
|
||||
)
|
||||
return insets.consume(v, typeMask, bottom = true)
|
||||
}
|
||||
is ContentType -> filter.toggleContentType(data, !chip.isChecked)
|
||||
is ContentRating -> filter.toggleContentRating(data, !chip.isChecked)
|
||||
is Demographic -> filter.toggleDemographic(data, !chip.isChecked)
|
||||
is PersistableFilter -> filter.setAdjusted(data.filter)
|
||||
null -> router.showTagsCatalogSheet(excludeMode = chip.parentView?.id == R.id.chips_genresExclude)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
|
||||
val filter = FilterCoordinator.require(this)
|
||||
when (parent.id) {
|
||||
R.id.spinner_order -> filter.setSortOrder(filter.sortOrder.value.availableItems[position])
|
||||
R.id.spinner_locale -> filter.setLocale(filter.locale.value.availableItems[position])
|
||||
R.id.spinner_original_locale -> filter.setOriginalLocale(filter.originalLocale.value.availableItems[position])
|
||||
}
|
||||
}
|
||||
override fun onChipLongClick(chip: Chip, data: Any?): Boolean {
|
||||
return when (data) {
|
||||
is PersistableFilter -> {
|
||||
showSavedFilterMenu(chip, data)
|
||||
true
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSliderValueChange(slider: Slider, value: Float, fromUser: Boolean) {
|
||||
if (!fromUser) {
|
||||
return
|
||||
}
|
||||
val intValue = value.toInt()
|
||||
val filter = FilterCoordinator.require(this)
|
||||
when (slider.id) {
|
||||
R.id.slider_year -> filter.setYear(
|
||||
if (intValue <= slider.valueFrom.toIntUp()) {
|
||||
YEAR_UNKNOWN
|
||||
} else {
|
||||
intValue
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
override fun onChipCloseClick(chip: Chip, data: Any?) {
|
||||
when (data) {
|
||||
is PersistableFilter -> {
|
||||
showSavedFilterMenu(chip, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRangeSliderValueChange(slider: RangeSlider, value: Float, fromUser: Boolean) {
|
||||
if (!fromUser) {
|
||||
return
|
||||
}
|
||||
val filter = FilterCoordinator.require(this)
|
||||
when (slider.id) {
|
||||
R.id.slider_yearsRange -> filter.setYearRange(
|
||||
valueFrom = slider.values.firstOrNull()?.let {
|
||||
if (it <= slider.valueFrom) YEAR_UNKNOWN else it.toInt()
|
||||
} ?: YEAR_UNKNOWN,
|
||||
valueTo = slider.values.lastOrNull()?.let {
|
||||
if (it >= slider.valueTo) YEAR_UNKNOWN else it.toInt()
|
||||
} ?: YEAR_UNKNOWN,
|
||||
)
|
||||
}
|
||||
}
|
||||
private fun onSortOrderChanged(value: FilterProperty<SortOrder>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutOrder.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)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onChipClick(chip: Chip, data: Any?) {
|
||||
val filter = FilterCoordinator.require(this)
|
||||
when (data) {
|
||||
is MangaState -> filter.toggleState(data, !chip.isChecked)
|
||||
is MangaTag -> if (chip.parentView?.id == R.id.chips_genresExclude) {
|
||||
filter.toggleTagExclude(data, !chip.isChecked)
|
||||
} else {
|
||||
filter.toggleTag(data, !chip.isChecked)
|
||||
}
|
||||
private fun onLocaleChanged(value: FilterProperty<Locale?>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutLocale.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.getDisplayName(b.spinnerLocale.context) },
|
||||
)
|
||||
val selectedIndex = value.availableItems.indexOf(selected)
|
||||
if (selectedIndex >= 0) {
|
||||
b.spinnerLocale.setSelection(selectedIndex, false)
|
||||
}
|
||||
}
|
||||
|
||||
is ContentType -> filter.toggleContentType(data, !chip.isChecked)
|
||||
is ContentRating -> filter.toggleContentRating(data, !chip.isChecked)
|
||||
is Demographic -> filter.toggleDemographic(data, !chip.isChecked)
|
||||
null -> router.showTagsCatalogSheet(excludeMode = chip.parentView?.id == R.id.chips_genresExclude)
|
||||
}
|
||||
}
|
||||
private fun onOriginalLocaleChanged(value: FilterProperty<Locale?>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutOriginalLocale.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val selected = value.selectedItems.singleOrNull()
|
||||
b.spinnerOriginalLocale.adapter = ArrayAdapter(
|
||||
b.spinnerOriginalLocale.context,
|
||||
android.R.layout.simple_spinner_dropdown_item,
|
||||
android.R.id.text1,
|
||||
value.availableItems.map { it.getDisplayName(b.spinnerOriginalLocale.context) },
|
||||
)
|
||||
val selectedIndex = value.availableItems.indexOf(selected)
|
||||
if (selectedIndex >= 0) {
|
||||
b.spinnerOriginalLocale.setSelection(selectedIndex, false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSortOrderChanged(value: FilterProperty<SortOrder>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutOrder.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 onTagsChanged(value: FilterProperty<MangaTag>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutGenres.isGone = value.isEmptyAndSuccess()
|
||||
b.layoutGenres.setError(value.error?.getDisplayMessage(resources))
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val chips = value.availableItems.map { tag ->
|
||||
ChipsView.ChipModel(
|
||||
title = tag.title,
|
||||
isChecked = tag in value.selectedItems,
|
||||
data = tag,
|
||||
)
|
||||
}
|
||||
b.chipsGenres.setChips(chips)
|
||||
}
|
||||
|
||||
private fun onLocaleChanged(value: FilterProperty<Locale?>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutLocale.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.getDisplayName(b.spinnerLocale.context) },
|
||||
)
|
||||
val selectedIndex = value.availableItems.indexOf(selected)
|
||||
if (selectedIndex >= 0) {
|
||||
b.spinnerLocale.setSelection(selectedIndex, false)
|
||||
}
|
||||
}
|
||||
private fun onTagsExcludedChanged(value: FilterProperty<MangaTag>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutGenresExclude.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val chips = value.availableItems.map { tag ->
|
||||
ChipsView.ChipModel(
|
||||
title = tag.title,
|
||||
isChecked = tag in value.selectedItems,
|
||||
data = tag,
|
||||
)
|
||||
}
|
||||
b.chipsGenresExclude.setChips(chips)
|
||||
}
|
||||
|
||||
private fun onOriginalLocaleChanged(value: FilterProperty<Locale?>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutOriginalLocale.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val selected = value.selectedItems.singleOrNull()
|
||||
b.spinnerOriginalLocale.adapter = ArrayAdapter(
|
||||
b.spinnerOriginalLocale.context,
|
||||
android.R.layout.simple_spinner_dropdown_item,
|
||||
android.R.id.text1,
|
||||
value.availableItems.map { it.getDisplayName(b.spinnerOriginalLocale.context) },
|
||||
)
|
||||
val selectedIndex = value.availableItems.indexOf(selected)
|
||||
if (selectedIndex >= 0) {
|
||||
b.spinnerOriginalLocale.setSelection(selectedIndex, false)
|
||||
}
|
||||
}
|
||||
private fun onStateChanged(value: FilterProperty<MangaState>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutState.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val chips = value.availableItems.map { state ->
|
||||
ChipsView.ChipModel(
|
||||
title = getString(state.titleResId),
|
||||
isChecked = state in value.selectedItems,
|
||||
data = state,
|
||||
)
|
||||
}
|
||||
b.chipsState.setChips(chips)
|
||||
}
|
||||
|
||||
private fun onTagsChanged(value: FilterProperty<MangaTag>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutGenres.isGone = value.isEmptyAndSuccess()
|
||||
b.layoutGenres.setError(value.error?.getDisplayMessage(resources))
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val chips = value.availableItems.map { tag ->
|
||||
ChipsView.ChipModel(
|
||||
title = tag.title,
|
||||
isChecked = tag in value.selectedItems,
|
||||
data = tag,
|
||||
)
|
||||
}
|
||||
b.chipsGenres.setChips(chips)
|
||||
}
|
||||
private fun onContentTypesChanged(value: FilterProperty<ContentType>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutTypes.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val chips = value.availableItems.map { type ->
|
||||
ChipsView.ChipModel(
|
||||
title = getString(type.titleResId),
|
||||
isChecked = type in value.selectedItems,
|
||||
data = type,
|
||||
)
|
||||
}
|
||||
b.chipsTypes.setChips(chips)
|
||||
}
|
||||
|
||||
private fun onTagsExcludedChanged(value: FilterProperty<MangaTag>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutGenresExclude.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val chips = value.availableItems.map { tag ->
|
||||
ChipsView.ChipModel(
|
||||
title = tag.title,
|
||||
isChecked = tag in value.selectedItems,
|
||||
data = tag,
|
||||
)
|
||||
}
|
||||
b.chipsGenresExclude.setChips(chips)
|
||||
}
|
||||
private fun onContentRatingChanged(value: FilterProperty<ContentRating>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutContentRating.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val chips = value.availableItems.map { contentRating ->
|
||||
ChipsView.ChipModel(
|
||||
title = getString(contentRating.titleResId),
|
||||
isChecked = contentRating in value.selectedItems,
|
||||
data = contentRating,
|
||||
)
|
||||
}
|
||||
b.chipsContentRating.setChips(chips)
|
||||
}
|
||||
|
||||
private fun onStateChanged(value: FilterProperty<MangaState>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutState.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val chips = value.availableItems.map { state ->
|
||||
ChipsView.ChipModel(
|
||||
title = getString(state.titleResId),
|
||||
isChecked = state in value.selectedItems,
|
||||
data = state,
|
||||
)
|
||||
}
|
||||
b.chipsState.setChips(chips)
|
||||
}
|
||||
private fun onDemographicsChanged(value: FilterProperty<Demographic>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutDemographics.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val chips = value.availableItems.map { demographic ->
|
||||
ChipsView.ChipModel(
|
||||
title = getString(demographic.titleResId),
|
||||
isChecked = demographic in value.selectedItems,
|
||||
data = demographic,
|
||||
)
|
||||
}
|
||||
b.chipsDemographics.setChips(chips)
|
||||
}
|
||||
|
||||
private fun onContentTypesChanged(value: FilterProperty<ContentType>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutTypes.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val chips = value.availableItems.map { type ->
|
||||
ChipsView.ChipModel(
|
||||
title = getString(type.titleResId),
|
||||
isChecked = type in value.selectedItems,
|
||||
data = type,
|
||||
)
|
||||
}
|
||||
b.chipsTypes.setChips(chips)
|
||||
}
|
||||
private fun onYearChanged(value: FilterProperty<Int>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutYear.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val currentValue = value.selectedItems.singleOrNull() ?: YEAR_UNKNOWN
|
||||
b.layoutYear.setValueText(
|
||||
if (currentValue == YEAR_UNKNOWN) {
|
||||
getString(R.string.any)
|
||||
} else {
|
||||
currentValue.toString()
|
||||
},
|
||||
)
|
||||
b.sliderYear.valueFrom = value.availableItems.first().toFloat()
|
||||
b.sliderYear.valueTo = value.availableItems.last().toFloat()
|
||||
b.sliderYear.setValueRounded(currentValue.toFloat())
|
||||
}
|
||||
|
||||
private fun onContentRatingChanged(value: FilterProperty<ContentRating>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutContentRating.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val chips = value.availableItems.map { contentRating ->
|
||||
ChipsView.ChipModel(
|
||||
title = getString(contentRating.titleResId),
|
||||
isChecked = contentRating in value.selectedItems,
|
||||
data = contentRating,
|
||||
)
|
||||
}
|
||||
b.chipsContentRating.setChips(chips)
|
||||
}
|
||||
private fun onYearRangeChanged(value: FilterProperty<Int>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutYearsRange.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
b.sliderYearsRange.valueFrom = value.availableItems.first().toFloat()
|
||||
b.sliderYearsRange.valueTo = value.availableItems.last().toFloat()
|
||||
val currentValueFrom = value.selectedItems.firstOrNull()?.toFloat() ?: b.sliderYearsRange.valueFrom
|
||||
val currentValueTo = value.selectedItems.lastOrNull()?.toFloat() ?: b.sliderYearsRange.valueTo
|
||||
b.layoutYearsRange.setValueText(
|
||||
getString(
|
||||
R.string.memory_usage_pattern,
|
||||
currentValueFrom.toInt().toString(),
|
||||
currentValueTo.toInt().toString(),
|
||||
),
|
||||
)
|
||||
b.sliderYearsRange.setValuesRounded(currentValueFrom, currentValueTo)
|
||||
}
|
||||
|
||||
private fun onDemographicsChanged(value: FilterProperty<Demographic>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutDemographics.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val chips = value.availableItems.map { demographic ->
|
||||
ChipsView.ChipModel(
|
||||
title = getString(demographic.titleResId),
|
||||
isChecked = demographic in value.selectedItems,
|
||||
data = demographic,
|
||||
)
|
||||
}
|
||||
b.chipsDemographics.setChips(chips)
|
||||
}
|
||||
private fun onSavedPresetsChanged(value: FilterProperty<PersistableFilter>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutSavedFilters.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val chips = value.availableItems.map { f ->
|
||||
ChipsView.ChipModel(
|
||||
title = f.name,
|
||||
isChecked = f in value.selectedItems,
|
||||
data = f,
|
||||
isDropdown = true,
|
||||
)
|
||||
}
|
||||
b.chipsSavedFilters.setChips(chips)
|
||||
}
|
||||
|
||||
private fun onYearChanged(value: FilterProperty<Int>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutYear.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val currentValue = value.selectedItems.singleOrNull() ?: YEAR_UNKNOWN
|
||||
b.layoutYear.setValueText(
|
||||
if (currentValue == YEAR_UNKNOWN) {
|
||||
getString(R.string.any)
|
||||
} else {
|
||||
currentValue.toString()
|
||||
},
|
||||
)
|
||||
b.sliderYear.valueFrom = value.availableItems.first().toFloat()
|
||||
b.sliderYear.valueTo = value.availableItems.last().toFloat()
|
||||
b.sliderYear.setValueRounded(currentValue.toFloat())
|
||||
}
|
||||
private fun showSavedFilterMenu(anchor: View, preset: PersistableFilter) {
|
||||
val menu = PopupMenu(context ?: return, anchor)
|
||||
val filter = FilterCoordinator.require(this)
|
||||
menu.inflate(R.menu.popup_saved_filter)
|
||||
menu.setOnMenuItemClickListener { menuItem ->
|
||||
when (menuItem.itemId) {
|
||||
R.id.action_delete -> filter.deleteSavedFilter(preset.id)
|
||||
R.id.action_rename -> onRenameFilterClick(preset)
|
||||
}
|
||||
true
|
||||
}
|
||||
menu.show()
|
||||
}
|
||||
|
||||
private fun onYearRangeChanged(value: FilterProperty<Int>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutYearsRange.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
b.sliderYearsRange.valueFrom = value.availableItems.first().toFloat()
|
||||
b.sliderYearsRange.valueTo = value.availableItems.last().toFloat()
|
||||
val currentValueFrom = value.selectedItems.firstOrNull()?.toFloat() ?: b.sliderYearsRange.valueFrom
|
||||
val currentValueTo = value.selectedItems.lastOrNull()?.toFloat() ?: b.sliderYearsRange.valueTo
|
||||
b.layoutYearsRange.setValueText(
|
||||
getString(
|
||||
R.string.memory_usage_pattern,
|
||||
currentValueFrom.toInt().toString(),
|
||||
currentValueTo.toInt().toString(),
|
||||
),
|
||||
)
|
||||
b.sliderYearsRange.setValuesRounded(currentValueFrom, currentValueTo)
|
||||
}
|
||||
private fun onSaveFilterClick() {
|
||||
val filter = FilterCoordinator.require(this)
|
||||
buildAlertDialog(context ?: return) {
|
||||
val input = setEditText(
|
||||
inputType = EditorInfo.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES,
|
||||
singleLine = true,
|
||||
)
|
||||
input.setHint(R.string.enter_name)
|
||||
input.filters += InputFilter.LengthFilter(MAX_TITLE_LENGTH)
|
||||
setTitle(R.string.save_filter)
|
||||
setPositiveButton(R.string.save) { d, _ ->
|
||||
val text = input.text?.toString()?.trim()
|
||||
if (!text.isNullOrEmpty()) {
|
||||
filter.saveCurrentFilter(text)
|
||||
} else {
|
||||
Snackbar.make(
|
||||
viewBinding?.scrollView ?: return@setPositiveButton,
|
||||
R.string.invalid_value_message,
|
||||
Snackbar.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
setNegativeButton(android.R.string.cancel, null)
|
||||
}.show()
|
||||
}
|
||||
|
||||
private fun onRenameFilterClick(preset: PersistableFilter) {
|
||||
val filter = FilterCoordinator.require(this)
|
||||
buildAlertDialog(context ?: return) {
|
||||
val input = setEditText(
|
||||
inputType = EditorInfo.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES,
|
||||
singleLine = true,
|
||||
)
|
||||
input.filters += InputFilter.LengthFilter(MAX_TITLE_LENGTH)
|
||||
input.setHint(R.string.enter_name)
|
||||
input.setText(preset.name)
|
||||
setTitle(R.string.rename)
|
||||
setPositiveButton(R.string.save) { _, _ ->
|
||||
val text = input.text?.toString()?.trim()
|
||||
if (!text.isNullOrEmpty()) {
|
||||
filter.renameSavedFilter(preset.id, text)
|
||||
} else {
|
||||
Snackbar.make(
|
||||
viewBinding?.scrollView ?: return@setPositiveButton,
|
||||
R.string.invalid_value_message,
|
||||
Snackbar.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
setNegativeButton(android.R.string.cancel, null)
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,287 +1,313 @@
|
||||
<?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">
|
||||
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" />
|
||||
<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="0dp"
|
||||
android:layout_weight="1"
|
||||
android:clipToPadding="false"
|
||||
android:scrollIndicators="top|bottom"
|
||||
tools:ignore="UnusedAttribute">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_body"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="@dimen/margin_small"
|
||||
android:paddingBottom="@dimen/margin_normal">
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/scrollView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:scrollIndicators="top"
|
||||
tools:ignore="UnusedAttribute">
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_order"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:title="@string/sort_order">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_body"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="@dimen/margin_small"
|
||||
android:paddingBottom="@dimen/margin_normal">
|
||||
<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="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small">
|
||||
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_saved_filters"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:title="@string/saved_filters">
|
||||
<Spinner
|
||||
android:id="@+id/spinner_order"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/spinner_height"
|
||||
android:minHeight="?listPreferredItemHeightSmall"
|
||||
android:paddingHorizontal="8dp" />
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_saved_filters"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter" />
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_order"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:title="@string/sort_order">
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_saved_filters"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:title="@string/saved_filters">
|
||||
|
||||
<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="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small">
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_saved_filters"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinner_order"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/spinner_height"
|
||||
android:minHeight="?listPreferredItemHeightSmall"
|
||||
android:paddingHorizontal="8dp" />
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_locale"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:title="@string/language">
|
||||
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
<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="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small">
|
||||
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_locale"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:title="@string/language">
|
||||
<Spinner
|
||||
android:id="@+id/spinner_locale"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/spinner_height"
|
||||
android:minHeight="?listPreferredItemHeightSmall"
|
||||
android:paddingHorizontal="8dp"
|
||||
android:popupBackground="@drawable/m3_spinner_popup_background" />
|
||||
|
||||
<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="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small">
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinner_locale"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/spinner_height"
|
||||
android:minHeight="?listPreferredItemHeightSmall"
|
||||
android:paddingHorizontal="8dp"
|
||||
android:popupBackground="@drawable/m3_spinner_popup_background" />
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_original_locale"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:title="@string/original_language">
|
||||
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/card_original_locale"
|
||||
style="?materialCardViewOutlinedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small">
|
||||
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_original_locale"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:title="@string/original_language">
|
||||
<Spinner
|
||||
android:id="@+id/spinner_original_locale"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/spinner_height"
|
||||
android:minHeight="?listPreferredItemHeightSmall"
|
||||
android:paddingHorizontal="8dp"
|
||||
android:popupBackground="@drawable/m3_spinner_popup_background" />
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/card_original_locale"
|
||||
style="?materialCardViewOutlinedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small">
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinner_original_locale"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/spinner_height"
|
||||
android:minHeight="?listPreferredItemHeightSmall"
|
||||
android:paddingHorizontal="8dp"
|
||||
android:popupBackground="@drawable/m3_spinner_popup_background" />
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_genres"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:showMoreButton="true"
|
||||
app:title="@string/genres">
|
||||
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_genres"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter" />
|
||||
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_genres"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:showMoreButton="true"
|
||||
app:title="@string/genres">
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_genres"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter" />
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_genresExclude"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:showMoreButton="true"
|
||||
app:title="@string/genres_exclude">
|
||||
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_genresExclude"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter" />
|
||||
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_genresExclude"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:showMoreButton="true"
|
||||
app:title="@string/genres_exclude">
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_genresExclude"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter" />
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_types"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:title="@string/type">
|
||||
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_types"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter" />
|
||||
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_types"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:title="@string/type">
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_types"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter" />
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_state"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:title="@string/state">
|
||||
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_state"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter" />
|
||||
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_state"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:title="@string/state">
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_state"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter" />
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_contentRating"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:title="@string/content_rating">
|
||||
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_contentRating"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter" />
|
||||
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_contentRating"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:title="@string/content_rating">
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_contentRating"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter" />
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_demographics"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:title="@string/demographics">
|
||||
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_demographics"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter" />
|
||||
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_demographics"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:title="@string/demographics">
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_demographics"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter" />
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_year"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:title="@string/year">
|
||||
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
<com.google.android.material.slider.Slider
|
||||
android:id="@+id/slider_year"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:stepSize="1"
|
||||
app:labelBehavior="gone"
|
||||
app:tickVisible="true"
|
||||
tools:value="2020"
|
||||
tools:valueFrom="1900"
|
||||
tools:valueTo="2090" />
|
||||
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_year"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:title="@string/year">
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
|
||||
<com.google.android.material.slider.Slider
|
||||
android:id="@+id/slider_year"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:stepSize="1"
|
||||
app:labelBehavior="gone"
|
||||
app:tickVisible="true"
|
||||
tools:value="2020"
|
||||
tools:valueFrom="1900"
|
||||
tools:valueTo="2090" />
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_yearsRange"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:title="@string/years">
|
||||
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
<com.google.android.material.slider.RangeSlider
|
||||
android:id="@+id/slider_yearsRange"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_normal"
|
||||
android:stepSize="1"
|
||||
app:labelBehavior="gone"
|
||||
app:tickVisible="true"
|
||||
tools:valueFrom="1900"
|
||||
tools:valueTo="2090" />
|
||||
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_yearsRange"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:title="@string/years">
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
|
||||
<com.google.android.material.slider.RangeSlider
|
||||
android:id="@+id/slider_yearsRange"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_normal"
|
||||
android:stepSize="1"
|
||||
app:labelBehavior="gone"
|
||||
app:tickVisible="true"
|
||||
tools:valueFrom="1900"
|
||||
tools:valueTo="2090" />
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
<com.google.android.material.dockedtoolbar.DockedToolbarLayout
|
||||
android:id="@+id/docked_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="false">
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_bottom"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/m3_comp_toolbar_docked_container_height"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_reset"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/margin_small"
|
||||
android:layout_weight="1"
|
||||
android:enabled="false"
|
||||
android:text="@string/reset"
|
||||
tools:enabled="true" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_save"
|
||||
style="?materialButtonTonalStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/margin_small"
|
||||
android:layout_weight="1"
|
||||
android:enabled="false"
|
||||
android:text="@string/save"
|
||||
tools:enabled="true"
|
||||
tools:ignore="ButtonStyle" />
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.dockedtoolbar.DockedToolbarLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_save_filter"
|
||||
style="@style/Widget.Material3.Button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
android:layout_marginBottom="@dimen/margin_normal"
|
||||
android:text="@string/save"
|
||||
android:enabled="false" />
|
||||
</LinearLayout>
|
||||
|
||||
12
app/src/main/res/menu/popup_saved_filter.xml
Normal file
12
app/src/main/res/menu/popup_saved_filter.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_rename"
|
||||
android:title="@string/rename" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_delete"
|
||||
android:title="@string/delete" />
|
||||
</menu>
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user