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) // Debug network interceptor (EH)
devImplementation "com.squareup.okhttp3:logging-interceptor:3.10.0" 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) // Firebase (EH)
implementation 'com.google.firebase:firebase-perf:16.0.0' implementation 'com.google.firebase:firebase-perf:16.0.0'
implementation 'com.crashlytics.sdk.android:crashlytics:2.9.4' 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.KotlinNamesAnnotationIntrospector$hasCreatorAnnotation$1
-dontwarn com.fasterxml.jackson.module.kotlin.KotlinValueInstantiator -dontwarn com.fasterxml.jackson.module.kotlin.KotlinValueInstantiator
-dontwarn exh.metadata.MetadataUtilKt$joinTagsToGenreString$2 -dontwarn exh.metadata.MetadataUtilKt$joinTagsToGenreString$2
-keep class xyz.nulldev.** { *; }
# Realm # Realm
-dontnote rx.internal.util.PlatformDependent -dontnote rx.internal.util.PlatformDependent

View File

@ -143,7 +143,7 @@ open class BrowseCatalogueController(bundle: Bundle) :
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.END) drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.END)
// EXH --> // EXH -->
navView.setSavedSearches(presenter.loadSearches().map { it.second }) navView.setSavedSearches(presenter.source.id, presenter.loadSearches())
navView.onSaveClicked = { navView.onSaveClicked = {
MaterialDialog.Builder(navView.context) MaterialDialog.Builder(navView.context)
.title("Save current search query?") .title("Save current search query?")
@ -151,13 +151,13 @@ open class BrowseCatalogueController(bundle: Bundle) :
val oldSavedSearches = presenter.loadSearches() val oldSavedSearches = presenter.loadSearches()
if(searchName.isNotBlank() if(searchName.isNotBlank()
&& oldSavedSearches.size < CatalogueNavigationView.MAX_SAVED_SEARCHES) { && oldSavedSearches.size < CatalogueNavigationView.MAX_SAVED_SEARCHES) {
val newSearches = oldSavedSearches + (presenter.source.id to EXHSavedSearch( val newSearches = oldSavedSearches + EXHSavedSearch(
searchName.toString().trim(), searchName.toString().trim(),
presenter.query, presenter.query,
presenter.sourceFilters.toList() presenter.sourceFilters
)) )
presenter.saveSearches(newSearches) presenter.saveSearches(newSearches)
navView.setSavedSearches(newSearches.map { it.second }) navView.setSavedSearches(presenter.source.id, newSearches)
} }
} }
.positiveText("Save") .positiveText("Save")
@ -182,23 +182,23 @@ open class BrowseCatalogueController(bundle: Bundle) :
return@cb return@cb
} }
presenter.sourceFilters = FilterList(search.second.filterList) presenter.sourceFilters = FilterList(search.filterList)
navView.setFilters(presenter.filterItems) navView.setFilters(presenter.filterItems)
val allDefault = presenter.sourceFilters == presenter.source.getFilterList() val allDefault = presenter.sourceFilters == presenter.source.getFilterList()
showProgressBar() showProgressBar()
adapter?.clear() adapter?.clear()
drawer.closeDrawer(Gravity.END) 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() activity?.invalidateOptionsMenu()
} }
navView.onSavedSearchDeleteClicked = cb@{ indexToDelete -> navView.onSavedSearchDeleteClicked = cb@{ indexToDelete, name ->
val savedSearches = presenter.loadSearches() val savedSearches = presenter.loadSearches()
val search = savedSearches.getOrNull(indexToDelete) val search = savedSearches.getOrNull(indexToDelete)
if(search == null) { if(search == null || search.name != name) {
MaterialDialog.Builder(navView.context) MaterialDialog.Builder(navView.context)
.title("Failed to delete saved search!") .title("Failed to delete saved search!")
.content("An error occurred while deleting the search.") .content("An error occurred while deleting the search.")
@ -210,7 +210,7 @@ open class BrowseCatalogueController(bundle: Bundle) :
MaterialDialog.Builder(navView.context) MaterialDialog.Builder(navView.context)
.title("Delete saved search query?") .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") .positiveText("Cancel")
.negativeText("Confirm") .negativeText("Confirm")
.onNegative { _, _ -> .onNegative { _, _ ->
@ -218,7 +218,7 @@ open class BrowseCatalogueController(bundle: Bundle) :
index != indexToDelete index != indexToDelete
} }
presenter.saveSearches(newSearches) presenter.saveSearches(newSearches)
navView.setSavedSearches(newSearches.map { it.second }) navView.setSavedSearches(presenter.source.id, newSearches)
} }
.cancelable(true) .cancelable(true)
.canceledOnTouchOutside(true) .canceledOnTouchOutside(true)

View File

@ -1,14 +1,9 @@
package eu.kanade.tachiyomi.ui.catalogue.browse package eu.kanade.tachiyomi.ui.catalogue.browse
import android.os.Bundle import android.os.Bundle
import com.fasterxml.jackson.annotation.JsonAutoDetect import com.github.salomonbrys.kotson.*
import com.fasterxml.jackson.annotation.PropertyAccessor import com.google.gson.JsonObject
import com.fasterxml.jackson.core.JsonProcessingException import com.google.gson.JsonParser
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 eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.flexibleadapter.items.ISectionable import eu.davidea.flexibleadapter.items.ISectionable
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
@ -35,6 +30,8 @@ import timber.log.Timber
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import xyz.nulldev.ts.api.http.serializer.FilterSerializer
import java.lang.RuntimeException
/** /**
* Presenter of [BrowseCatalogueController]. * Presenter of [BrowseCatalogueController].
@ -386,31 +383,35 @@ open class BrowseCataloguePresenter(
} }
// EXH --> // EXH -->
private val sourceManager: SourceManager by injectLazy() private val jsonParser = JsonParser()
private fun mapper() = jacksonObjectMapper().enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL) private val filterSerializer = FilterSerializer()
.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE) fun saveSearches(searches: List<EXHSavedSearch>) {
.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY) val otherSerialized = prefs.eh_savedSearches().getOrDefault().filter {
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) !it.startsWith("${source.id}:")
fun saveSearches(searches: List<Pair<Long, EXHSavedSearch>>) { }
val m = mapper() val newSerialized = searches.map {
val serialized = searches.map { "${source.id}:" + jsonObject(
"${it.first}:" + m.writeValueAsString(it.second) "name" to it.name,
}.toSet() "query" to it.query,
prefs.eh_savedSearches().set(serialized) "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() val loaded = prefs.eh_savedSearches().getOrDefault()
return loaded.map { return loaded.map {
try { try {
val id = it.substringBefore(':').toLong() val id = it.substringBefore(':').toLong()
val content = it.substringAfter(':') if(id != source.id) return@map null
val newMapper = mapper() val content = jsonParser.parse(it.substringAfter(':')).obj
.setTypeFactory(TypeFactory.defaultInstance() val originalFilters = source.getFilterList()
.withClassLoader(sourceManager.getOrStub(id).javaClass.classLoader)) filterSerializer.deserialize(originalFilters, content["filters"].array)
id to newMapper.readValue<EXHSavedSearch>(content) EXHSavedSearch(content["name"].string,
content["query"].string,
} catch(t: JsonProcessingException) { originalFilters)
} catch(t: RuntimeException) {
// Load failed // Load failed
Timber.e(t, "Failed to load saved search!") Timber.e(t, "Failed to load saved search!")
t.printStackTrace() t.printStackTrace()

View File

@ -38,7 +38,7 @@ class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs:
// EXH <-- // EXH <--
// EXH --> // EXH -->
var onSavedSearchDeleteClicked: (Int) -> Unit = {} var onSavedSearchDeleteClicked: (Int, String) -> Unit = { index, name -> }
// EXH <-- // EXH <--
init { init {
@ -58,7 +58,7 @@ class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs:
} }
// EXH --> // EXH -->
fun setSavedSearches(searches: List<EXHSavedSearch>) { fun setSavedSearches(id: Long, searches: List<EXHSavedSearch>) {
saved_searches.removeAllViews() saved_searches.removeAllViews()
val outValue = TypedValue() val outValue = TypedValue()
@ -76,7 +76,7 @@ class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs:
restoreBtn.setBackgroundResource(outValue.resourceId) restoreBtn.setBackgroundResource(outValue.resourceId)
restoreBtn.setPadding(8.dpToPx, 8.dpToPx, 8.dpToPx, 8.dpToPx) restoreBtn.setPadding(8.dpToPx, 8.dpToPx, 8.dpToPx, 8.dpToPx)
restoreBtn.setOnClickListener { onSavedSearchClicked(index) } restoreBtn.setOnClickListener { onSavedSearchClicked(index) }
restoreBtn.setOnLongClickListener { onSavedSearchDeleteClicked(index); true } restoreBtn.setOnLongClickListener { onSavedSearchDeleteClicked(index, search.name); true }
saved_searches.addView(restoreBtn) saved_searches.addView(restoreBtn)
} }
} }

View File

@ -1,7 +1,7 @@
package exh package exh
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.FilterList
data class EXHSavedSearch(val name: String, data class EXHSavedSearch(val name: String,
val query: 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"
}
}