Use TachiWeb filter serializer for saving filters

This commit is contained in:
NerdNumber9 2019-04-06 09:45:28 -04:00
parent 4e2c9dc083
commit 57d83e3d1b
8 changed files with 336 additions and 48 deletions

View File

@ -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'

View File

@ -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

View File

@ -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)

View File

@ -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()

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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"
}
}

View File

@ -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"
}
}