diff --git a/app/build.gradle b/app/build.gradle index b6357730c..01c4805b4 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index a9a88be0e..441cc9ecf 100755 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/BrowseCatalogueController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/BrowseCatalogueController.kt index cbdd9df8e..d7b61c3c2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/BrowseCatalogueController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/BrowseCatalogueController.kt @@ -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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/BrowseCataloguePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/BrowseCataloguePresenter.kt index 9d2306dd3..dfcdb8d83 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/BrowseCataloguePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/BrowseCataloguePresenter.kt @@ -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>) { - 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) { + 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> { + fun loadSearches(): List { 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(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() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueNavigationView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueNavigationView.kt index 088b0bfea..df34bafc4 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueNavigationView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueNavigationView.kt @@ -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) { + fun setSavedSearches(id: Long, searches: List) { 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) } } diff --git a/app/src/main/java/exh/EXHSavedSearch.kt b/app/src/main/java/exh/EXHSavedSearch.kt index 1b88a0101..dd7e43dec 100644 --- a/app/src/main/java/exh/EXHSavedSearch.kt +++ b/app/src/main/java/exh/EXHSavedSearch.kt @@ -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>) \ No newline at end of file + val filterList: FilterList) \ No newline at end of file diff --git a/app/src/main/java/xyz/nulldev/ts/api/http/serializer/FilterSerializer.kt b/app/src/main/java/xyz/nulldev/ts/api/http/serializer/FilterSerializer.kt new file mode 100644 index 000000000..b5034a096 --- /dev/null +++ b/app/src/main/java/xyz/nulldev/ts/api/http/serializer/FilterSerializer.kt @@ -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>( + 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)) + } + } + + fun serialize(filter: Filter): 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> + + 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, obj.obj) + } + } + + fun deserialize(filter: Filter, 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> + + 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 Any?>).set(filter, res) + } + } + } + + companion object { + const val TYPE = "_type" + const val CLASS_MAPPINGS = "_cmaps" + } +} diff --git a/app/src/main/java/xyz/nulldev/ts/api/http/serializer/FilterSerializerModels.kt b/app/src/main/java/xyz/nulldev/ts/api/http/serializer/FilterSerializerModels.kt new file mode 100644 index 000000000..41478ee22 --- /dev/null +++ b/app/src/main/java/xyz/nulldev/ts/api/http/serializer/FilterSerializerModels.kt @@ -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> { + fun serialize(json: JsonObject, filter: T) {} + fun deserialize(json: JsonObject, filter: T) {} + + /** + * Automatic two-way mappings between fields and JSON + */ + fun mappings(): List>> = emptyList() + + val serializer: FilterSerializer + val type: String + val clazz: KClass +} + +class HeaderSerializer(override val serializer: FilterSerializer) : Serializer { + 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 { + 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> { + override val type = "SELECT" + override val clazz = Filter.Select::class + + override fun serialize(json: JsonObject, filter: Filter.Select) { + //Serialize values to JSON + json[VALUES] = JsonArray().apply { + filter.values.map { + it.toString() + }.forEach { add(it) } + } + } + + override fun mappings() = listOf( + Pair(NAME, Filter.Select::name), + Pair(STATE, Filter.Select::state) + ) + + companion object { + const val NAME = "name" + const val VALUES = "values" + const val STATE = "state" + } +} + +class TextSerializer(override val serializer: FilterSerializer) : Serializer { + 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 { + 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 { + 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> { + override val type = "GROUP" + override val clazz = Filter.Group::class + + override fun serialize(json: JsonObject, filter: Filter.Group) { + json[STATE] = JsonArray().apply { + filter.state.forEach { + add(if(it is Filter<*>) + serializer.serialize(it as Filter) + else + JsonNull.INSTANCE + ) + } + } + } + + override fun deserialize(json: JsonObject, filter: Filter.Group) { + json[STATE].asJsonArray.forEachIndexed { index, jsonElement -> + if(!jsonElement.isJsonNull) + serializer.deserialize(filter.state[index] as Filter, jsonElement.asJsonObject) + } + } + + override fun mappings() = listOf( + Pair(NAME, Filter.Group::name) + ) + + companion object { + const val NAME = "name" + const val STATE = "state" + } +} + +class SortSerializer(override val serializer: FilterSerializer) : Serializer { + 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" + } +}