Use TachiWeb filter serializer for saving filters
This commit is contained in:
parent
4e2c9dc083
commit
57d83e3d1b
@ -263,11 +263,6 @@ dependencies {
|
||||
// Debug network interceptor (EH)
|
||||
devImplementation "com.squareup.okhttp3:logging-interceptor:3.10.0"
|
||||
|
||||
// Serialization
|
||||
implementation ("com.fasterxml.jackson.module:jackson-module-kotlin:2.9.+") {
|
||||
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-reflect'
|
||||
}
|
||||
|
||||
// Firebase (EH)
|
||||
implementation 'com.google.firebase:firebase-perf:16.0.0'
|
||||
implementation 'com.crashlytics.sdk.android:crashlytics:2.9.4'
|
||||
|
1
app/proguard-rules.pro
vendored
1
app/proguard-rules.pro
vendored
@ -97,6 +97,7 @@
|
||||
-dontwarn com.fasterxml.jackson.module.kotlin.KotlinNamesAnnotationIntrospector$hasCreatorAnnotation$1
|
||||
-dontwarn com.fasterxml.jackson.module.kotlin.KotlinValueInstantiator
|
||||
-dontwarn exh.metadata.MetadataUtilKt$joinTagsToGenreString$2
|
||||
-keep class xyz.nulldev.** { *; }
|
||||
|
||||
# Realm
|
||||
-dontnote rx.internal.util.PlatformDependent
|
||||
|
@ -143,7 +143,7 @@ open class BrowseCatalogueController(bundle: Bundle) :
|
||||
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.END)
|
||||
|
||||
// EXH -->
|
||||
navView.setSavedSearches(presenter.loadSearches().map { it.second })
|
||||
navView.setSavedSearches(presenter.source.id, presenter.loadSearches())
|
||||
navView.onSaveClicked = {
|
||||
MaterialDialog.Builder(navView.context)
|
||||
.title("Save current search query?")
|
||||
@ -151,13 +151,13 @@ open class BrowseCatalogueController(bundle: Bundle) :
|
||||
val oldSavedSearches = presenter.loadSearches()
|
||||
if(searchName.isNotBlank()
|
||||
&& oldSavedSearches.size < CatalogueNavigationView.MAX_SAVED_SEARCHES) {
|
||||
val newSearches = oldSavedSearches + (presenter.source.id to EXHSavedSearch(
|
||||
val newSearches = oldSavedSearches + EXHSavedSearch(
|
||||
searchName.toString().trim(),
|
||||
presenter.query,
|
||||
presenter.sourceFilters.toList()
|
||||
))
|
||||
presenter.sourceFilters
|
||||
)
|
||||
presenter.saveSearches(newSearches)
|
||||
navView.setSavedSearches(newSearches.map { it.second })
|
||||
navView.setSavedSearches(presenter.source.id, newSearches)
|
||||
}
|
||||
}
|
||||
.positiveText("Save")
|
||||
@ -182,23 +182,23 @@ open class BrowseCatalogueController(bundle: Bundle) :
|
||||
return@cb
|
||||
}
|
||||
|
||||
presenter.sourceFilters = FilterList(search.second.filterList)
|
||||
presenter.sourceFilters = FilterList(search.filterList)
|
||||
navView.setFilters(presenter.filterItems)
|
||||
val allDefault = presenter.sourceFilters == presenter.source.getFilterList()
|
||||
|
||||
showProgressBar()
|
||||
adapter?.clear()
|
||||
drawer.closeDrawer(Gravity.END)
|
||||
presenter.restartPager(search.second.query, if (allDefault) FilterList() else presenter.sourceFilters)
|
||||
presenter.restartPager(search.query, if (allDefault) FilterList() else presenter.sourceFilters)
|
||||
activity?.invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
navView.onSavedSearchDeleteClicked = cb@{ indexToDelete ->
|
||||
navView.onSavedSearchDeleteClicked = cb@{ indexToDelete, name ->
|
||||
val savedSearches = presenter.loadSearches()
|
||||
|
||||
val search = savedSearches.getOrNull(indexToDelete)
|
||||
|
||||
if(search == null) {
|
||||
if(search == null || search.name != name) {
|
||||
MaterialDialog.Builder(navView.context)
|
||||
.title("Failed to delete saved search!")
|
||||
.content("An error occurred while deleting the search.")
|
||||
@ -210,7 +210,7 @@ open class BrowseCatalogueController(bundle: Bundle) :
|
||||
|
||||
MaterialDialog.Builder(navView.context)
|
||||
.title("Delete saved search query?")
|
||||
.content("Are you sure you wish to delete your saved search query: '${search.second.name}'?")
|
||||
.content("Are you sure you wish to delete your saved search query: '${search.name}'?")
|
||||
.positiveText("Cancel")
|
||||
.negativeText("Confirm")
|
||||
.onNegative { _, _ ->
|
||||
@ -218,7 +218,7 @@ open class BrowseCatalogueController(bundle: Bundle) :
|
||||
index != indexToDelete
|
||||
}
|
||||
presenter.saveSearches(newSearches)
|
||||
navView.setSavedSearches(newSearches.map { it.second })
|
||||
navView.setSavedSearches(presenter.source.id, newSearches)
|
||||
}
|
||||
.cancelable(true)
|
||||
.canceledOnTouchOutside(true)
|
||||
|
@ -1,14 +1,9 @@
|
||||
package eu.kanade.tachiyomi.ui.catalogue.browse
|
||||
|
||||
import android.os.Bundle
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect
|
||||
import com.fasterxml.jackson.annotation.PropertyAccessor
|
||||
import com.fasterxml.jackson.core.JsonProcessingException
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.type.TypeFactory
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.github.salomonbrys.kotson.*
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.flexibleadapter.items.ISectionable
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
@ -35,6 +30,8 @@ import timber.log.Timber
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import xyz.nulldev.ts.api.http.serializer.FilterSerializer
|
||||
import java.lang.RuntimeException
|
||||
|
||||
/**
|
||||
* Presenter of [BrowseCatalogueController].
|
||||
@ -386,31 +383,35 @@ open class BrowseCataloguePresenter(
|
||||
}
|
||||
|
||||
// EXH -->
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
private fun mapper() = jacksonObjectMapper().enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL)
|
||||
.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE)
|
||||
.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||
fun saveSearches(searches: List<Pair<Long, EXHSavedSearch>>) {
|
||||
val m = mapper()
|
||||
val serialized = searches.map {
|
||||
"${it.first}:" + m.writeValueAsString(it.second)
|
||||
}.toSet()
|
||||
prefs.eh_savedSearches().set(serialized)
|
||||
private val jsonParser = JsonParser()
|
||||
private val filterSerializer = FilterSerializer()
|
||||
fun saveSearches(searches: List<EXHSavedSearch>) {
|
||||
val otherSerialized = prefs.eh_savedSearches().getOrDefault().filter {
|
||||
!it.startsWith("${source.id}:")
|
||||
}
|
||||
val newSerialized = searches.map {
|
||||
"${source.id}:" + jsonObject(
|
||||
"name" to it.name,
|
||||
"query" to it.query,
|
||||
"filters" to filterSerializer.serialize(it.filterList)
|
||||
).toString()
|
||||
}
|
||||
prefs.eh_savedSearches().set((otherSerialized + newSerialized).toSet())
|
||||
}
|
||||
|
||||
fun loadSearches(): List<Pair<Long, EXHSavedSearch>> {
|
||||
fun loadSearches(): List<EXHSavedSearch> {
|
||||
val loaded = prefs.eh_savedSearches().getOrDefault()
|
||||
return loaded.map {
|
||||
try {
|
||||
val id = it.substringBefore(':').toLong()
|
||||
val content = it.substringAfter(':')
|
||||
val newMapper = mapper()
|
||||
.setTypeFactory(TypeFactory.defaultInstance()
|
||||
.withClassLoader(sourceManager.getOrStub(id).javaClass.classLoader))
|
||||
id to newMapper.readValue<EXHSavedSearch>(content)
|
||||
|
||||
} catch(t: JsonProcessingException) {
|
||||
if(id != source.id) return@map null
|
||||
val content = jsonParser.parse(it.substringAfter(':')).obj
|
||||
val originalFilters = source.getFilterList()
|
||||
filterSerializer.deserialize(originalFilters, content["filters"].array)
|
||||
EXHSavedSearch(content["name"].string,
|
||||
content["query"].string,
|
||||
originalFilters)
|
||||
} catch(t: RuntimeException) {
|
||||
// Load failed
|
||||
Timber.e(t, "Failed to load saved search!")
|
||||
t.printStackTrace()
|
||||
|
@ -38,7 +38,7 @@ class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs:
|
||||
// EXH <--
|
||||
|
||||
// EXH -->
|
||||
var onSavedSearchDeleteClicked: (Int) -> Unit = {}
|
||||
var onSavedSearchDeleteClicked: (Int, String) -> Unit = { index, name -> }
|
||||
// EXH <--
|
||||
|
||||
init {
|
||||
@ -58,7 +58,7 @@ class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs:
|
||||
}
|
||||
|
||||
// EXH -->
|
||||
fun setSavedSearches(searches: List<EXHSavedSearch>) {
|
||||
fun setSavedSearches(id: Long, searches: List<EXHSavedSearch>) {
|
||||
saved_searches.removeAllViews()
|
||||
|
||||
val outValue = TypedValue()
|
||||
@ -76,7 +76,7 @@ class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs:
|
||||
restoreBtn.setBackgroundResource(outValue.resourceId)
|
||||
restoreBtn.setPadding(8.dpToPx, 8.dpToPx, 8.dpToPx, 8.dpToPx)
|
||||
restoreBtn.setOnClickListener { onSavedSearchClicked(index) }
|
||||
restoreBtn.setOnLongClickListener { onSavedSearchDeleteClicked(index); true }
|
||||
restoreBtn.setOnLongClickListener { onSavedSearchDeleteClicked(index, search.name); true }
|
||||
saved_searches.addView(restoreBtn)
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
package exh
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
|
||||
data class EXHSavedSearch(val name: String,
|
||||
val query: String,
|
||||
val filterList: List<Filter<*>>)
|
||||
val filterList: FilterList)
|
@ -0,0 +1,95 @@
|
||||
package xyz.nulldev.ts.api.http.serializer
|
||||
|
||||
import com.github.salomonbrys.kotson.*
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonObject
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import kotlin.reflect.KMutableProperty1
|
||||
import kotlin.reflect.full.isSubclassOf
|
||||
|
||||
class FilterSerializer {
|
||||
val serializers = listOf<Serializer<*>>(
|
||||
HeaderSerializer(this),
|
||||
SeparatorSerializer(this),
|
||||
SelectSerializer(this),
|
||||
TextSerializer(this),
|
||||
CheckboxSerializer(this),
|
||||
TriStateSerializer(this),
|
||||
GroupSerializer(this),
|
||||
SortSerializer(this)
|
||||
)
|
||||
|
||||
fun serialize(filters: FilterList) = JsonArray().apply {
|
||||
filters.forEach {
|
||||
add(serialize(it as Filter<Any?>))
|
||||
}
|
||||
}
|
||||
|
||||
fun serialize(filter: Filter<Any?>): JsonObject {
|
||||
val out = JsonObject()
|
||||
for(serializer in serializers) {
|
||||
if(filter::class.isSubclassOf(serializer.clazz)) {
|
||||
//TODO Not sure how to deal with the mess of types here
|
||||
serializer as Serializer<Filter<Any?>>
|
||||
|
||||
serializer.serialize(out, filter)
|
||||
|
||||
out[CLASS_MAPPINGS] = JsonObject()
|
||||
|
||||
serializer.mappings().forEach {
|
||||
val res = it.second.get(filter)
|
||||
out[it.first] = res
|
||||
out[CLASS_MAPPINGS][it.first] = res?.javaClass?.name ?: "null"
|
||||
}
|
||||
|
||||
out[TYPE] = serializer.type
|
||||
|
||||
return out
|
||||
}
|
||||
}
|
||||
throw IllegalArgumentException("Cannot serialize this Filter object!")
|
||||
}
|
||||
|
||||
fun deserialize(filters: FilterList, json: JsonArray) {
|
||||
filters.zip(json).forEach { (filter, obj) ->
|
||||
deserialize(filter as Filter<Any?>, obj.obj)
|
||||
}
|
||||
}
|
||||
|
||||
fun deserialize(filter: Filter<Any?>, json: JsonObject) {
|
||||
val serializer = serializers.find {
|
||||
it.type == json[TYPE].string
|
||||
} ?: throw IllegalArgumentException("Cannot deserialize this type!")
|
||||
|
||||
//TODO Not sure how to deal with the mess of types here
|
||||
serializer as Serializer<Filter<Any?>>
|
||||
|
||||
serializer.deserialize(json, filter)
|
||||
|
||||
serializer.mappings().forEach {
|
||||
if(it.second is KMutableProperty1) {
|
||||
val obj = json[it.first]
|
||||
val res: Any? = when(json[CLASS_MAPPINGS][it.first].string) {
|
||||
java.lang.Integer::class.java.name -> obj.int
|
||||
java.lang.Long::class.java.name -> obj.long
|
||||
java.lang.Float::class.java.name -> obj.float
|
||||
java.lang.Double::class.java.name -> obj.double
|
||||
java.lang.String::class.java.name -> obj.string
|
||||
java.lang.Boolean::class.java.name -> obj.bool
|
||||
java.lang.Byte::class.java.name -> obj.byte
|
||||
java.lang.Short::class.java.name -> obj.short
|
||||
java.lang.Character::class.java.name -> obj.char
|
||||
"null" -> null
|
||||
else -> throw IllegalArgumentException("Cannot deserialize this type!")
|
||||
}
|
||||
(it.second as KMutableProperty1<in Filter<Any?>, in Any?>).set(filter, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TYPE = "_type"
|
||||
const val CLASS_MAPPINGS = "_cmaps"
|
||||
}
|
||||
}
|
@ -0,0 +1,196 @@
|
||||
package xyz.nulldev.ts.api.http.serializer
|
||||
|
||||
import com.github.salomonbrys.kotson.bool
|
||||
import com.github.salomonbrys.kotson.int
|
||||
import com.github.salomonbrys.kotson.nullObj
|
||||
import com.github.salomonbrys.kotson.set
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonNull
|
||||
import com.google.gson.JsonObject
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KProperty1
|
||||
|
||||
interface Serializer<in T : Filter<out Any?>> {
|
||||
fun serialize(json: JsonObject, filter: T) {}
|
||||
fun deserialize(json: JsonObject, filter: T) {}
|
||||
|
||||
/**
|
||||
* Automatic two-way mappings between fields and JSON
|
||||
*/
|
||||
fun mappings(): List<Pair<String, KProperty1<in T, *>>> = emptyList()
|
||||
|
||||
val serializer: FilterSerializer
|
||||
val type: String
|
||||
val clazz: KClass<in T>
|
||||
}
|
||||
|
||||
class HeaderSerializer(override val serializer: FilterSerializer) : Serializer<Filter.Header> {
|
||||
override val type = "HEADER"
|
||||
override val clazz = Filter.Header::class
|
||||
|
||||
override fun mappings() = listOf(
|
||||
Pair(NAME, Filter.Header::name)
|
||||
)
|
||||
|
||||
companion object {
|
||||
const val NAME = "name"
|
||||
}
|
||||
}
|
||||
|
||||
class SeparatorSerializer(override val serializer: FilterSerializer) : Serializer<Filter.Separator> {
|
||||
override val type = "SEPARATOR"
|
||||
override val clazz = Filter.Separator::class
|
||||
|
||||
override fun mappings() = listOf(
|
||||
Pair(NAME, Filter.Separator::name)
|
||||
)
|
||||
|
||||
companion object {
|
||||
const val NAME = "name"
|
||||
}
|
||||
}
|
||||
|
||||
class SelectSerializer(override val serializer: FilterSerializer) : Serializer<Filter.Select<Any>> {
|
||||
override val type = "SELECT"
|
||||
override val clazz = Filter.Select::class
|
||||
|
||||
override fun serialize(json: JsonObject, filter: Filter.Select<Any>) {
|
||||
//Serialize values to JSON
|
||||
json[VALUES] = JsonArray().apply {
|
||||
filter.values.map {
|
||||
it.toString()
|
||||
}.forEach { add(it) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun mappings() = listOf(
|
||||
Pair(NAME, Filter.Select<Any>::name),
|
||||
Pair(STATE, Filter.Select<Any>::state)
|
||||
)
|
||||
|
||||
companion object {
|
||||
const val NAME = "name"
|
||||
const val VALUES = "values"
|
||||
const val STATE = "state"
|
||||
}
|
||||
}
|
||||
|
||||
class TextSerializer(override val serializer: FilterSerializer) : Serializer<Filter.Text> {
|
||||
override val type = "TEXT"
|
||||
override val clazz = Filter.Text::class
|
||||
|
||||
override fun mappings() = listOf(
|
||||
Pair(NAME, Filter.Text::name),
|
||||
Pair(STATE, Filter.Text::state)
|
||||
)
|
||||
|
||||
companion object {
|
||||
const val NAME = "name"
|
||||
const val STATE = "state"
|
||||
}
|
||||
}
|
||||
|
||||
class CheckboxSerializer(override val serializer: FilterSerializer) : Serializer<Filter.CheckBox> {
|
||||
override val type = "CHECKBOX"
|
||||
override val clazz = Filter.CheckBox::class
|
||||
|
||||
override fun mappings() = listOf(
|
||||
Pair(NAME, Filter.CheckBox::name),
|
||||
Pair(STATE, Filter.CheckBox::state)
|
||||
)
|
||||
|
||||
companion object {
|
||||
const val NAME = "name"
|
||||
const val STATE = "state"
|
||||
}
|
||||
}
|
||||
|
||||
class TriStateSerializer(override val serializer: FilterSerializer) : Serializer<Filter.TriState> {
|
||||
override val type = "TRISTATE"
|
||||
override val clazz = Filter.TriState::class
|
||||
|
||||
override fun mappings() = listOf(
|
||||
Pair(NAME, Filter.TriState::name),
|
||||
Pair(STATE, Filter.TriState::state)
|
||||
)
|
||||
|
||||
companion object {
|
||||
const val NAME = "name"
|
||||
const val STATE = "state"
|
||||
}
|
||||
}
|
||||
|
||||
class GroupSerializer(override val serializer: FilterSerializer) : Serializer<Filter.Group<Any?>> {
|
||||
override val type = "GROUP"
|
||||
override val clazz = Filter.Group::class
|
||||
|
||||
override fun serialize(json: JsonObject, filter: Filter.Group<Any?>) {
|
||||
json[STATE] = JsonArray().apply {
|
||||
filter.state.forEach {
|
||||
add(if(it is Filter<*>)
|
||||
serializer.serialize(it as Filter<Any?>)
|
||||
else
|
||||
JsonNull.INSTANCE
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun deserialize(json: JsonObject, filter: Filter.Group<Any?>) {
|
||||
json[STATE].asJsonArray.forEachIndexed { index, jsonElement ->
|
||||
if(!jsonElement.isJsonNull)
|
||||
serializer.deserialize(filter.state[index] as Filter<Any?>, jsonElement.asJsonObject)
|
||||
}
|
||||
}
|
||||
|
||||
override fun mappings() = listOf(
|
||||
Pair(NAME, Filter.Group<Any?>::name)
|
||||
)
|
||||
|
||||
companion object {
|
||||
const val NAME = "name"
|
||||
const val STATE = "state"
|
||||
}
|
||||
}
|
||||
|
||||
class SortSerializer(override val serializer: FilterSerializer) : Serializer<Filter.Sort> {
|
||||
override val type = "SORT"
|
||||
override val clazz = Filter.Sort::class
|
||||
|
||||
override fun serialize(json: JsonObject, filter: Filter.Sort) {
|
||||
//Serialize values
|
||||
json[VALUES] = JsonArray().apply {
|
||||
filter.values.forEach { add(it) }
|
||||
}
|
||||
|
||||
//Serialize state
|
||||
json[STATE] = filter.state?.let { (index, ascending) ->
|
||||
JsonObject().apply {
|
||||
this[STATE_INDEX] = index
|
||||
this[STATE_ASCENDING] = ascending
|
||||
}
|
||||
} ?: JsonNull.INSTANCE
|
||||
}
|
||||
|
||||
override fun deserialize(json: JsonObject, filter: Filter.Sort) {
|
||||
//Deserialize state
|
||||
filter.state = json[STATE].nullObj?.let {
|
||||
Filter.Sort.Selection(it[STATE_INDEX].int,
|
||||
it[STATE_ASCENDING].bool)
|
||||
}
|
||||
}
|
||||
|
||||
override fun mappings() = listOf(
|
||||
Pair(NAME, Filter.Sort::name)
|
||||
)
|
||||
|
||||
companion object {
|
||||
const val NAME = "name"
|
||||
const val VALUES = "values"
|
||||
const val STATE = "state"
|
||||
|
||||
const val STATE_INDEX = "index"
|
||||
const val STATE_ASCENDING = "ascending"
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user