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