Rewrite filter and preference mutations (#577)

This commit is contained in:
Mitchell Syer
2023-06-24 12:28:11 -04:00
committed by GitHub
parent 08af195f11
commit b9b115d0ea
5 changed files with 318 additions and 45 deletions
@@ -1,11 +1,18 @@
package suwayomi.tachidesk.graphql.mutations package suwayomi.tachidesk.graphql.mutations
import androidx.preference.CheckBoxPreference
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.SwitchPreferenceCompat
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.graphql.types.FilterChange
import suwayomi.tachidesk.graphql.types.MangaType import suwayomi.tachidesk.graphql.types.MangaType
import suwayomi.tachidesk.graphql.types.PreferenceObject import suwayomi.tachidesk.graphql.types.Preference
import suwayomi.tachidesk.graphql.types.preferenceOf
import suwayomi.tachidesk.graphql.types.updateFilterList
import suwayomi.tachidesk.manga.impl.MangaList.insertOrGet import suwayomi.tachidesk.manga.impl.MangaList.insertOrGet
import suwayomi.tachidesk.manga.impl.Search
import suwayomi.tachidesk.manga.impl.Source import suwayomi.tachidesk.manga.impl.Source
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
@@ -20,10 +27,6 @@ class SourceMutation {
POPULAR, POPULAR,
LATEST LATEST
} }
data class FilterChange(
val position: Int,
val state: String
)
data class FetchSourceMangaInput( data class FetchSourceMangaInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val source: Long, val source: Long,
@@ -50,11 +53,7 @@ class SourceMutation {
source.fetchSearchManga( source.fetchSearchManga(
page = page, page = page,
query = query.orEmpty(), query = query.orEmpty(),
filters = Search.buildFilterList( filters = updateFilterList(source, filters)
sourceId = sourceId,
changes = filters?.map { Search.FilterChange(it.position, it.state) }
.orEmpty()
)
).awaitSingle() ).awaitSingle()
} }
FetchSourceMangaType.POPULAR -> { FetchSourceMangaType.POPULAR -> {
@@ -85,7 +84,11 @@ class SourceMutation {
data class SourcePreferenceChange( data class SourcePreferenceChange(
val position: Int, val position: Int,
val state: String val switchState: Boolean? = null,
val checkBoxState: Boolean? = null,
val editTextState: String? = null,
val listState: String? = null,
val multiSelectState: List<String>? = null
) )
data class UpdateSourcePreferenceInput( data class UpdateSourcePreferenceInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
@@ -94,7 +97,7 @@ class SourceMutation {
) )
data class UpdateSourcePreferencePayload( data class UpdateSourcePreferencePayload(
val clientMutationId: String?, val clientMutationId: String?,
val preferences: List<PreferenceObject> val preferences: List<Preference>
) )
fun updateSourcePreference( fun updateSourcePreference(
@@ -102,11 +105,20 @@ class SourceMutation {
): UpdateSourcePreferencePayload { ): UpdateSourcePreferencePayload {
val (clientMutationId, sourceId, change) = input val (clientMutationId, sourceId, change) = input
Source.setSourcePreference(sourceId, Source.SourcePreferenceChange(change.position, change.state)) Source.setSourcePreference(sourceId, change.position, "") { preference ->
when (preference) {
is SwitchPreferenceCompat -> change.switchState
is CheckBoxPreference -> change.checkBoxState
is EditTextPreference -> change.editTextState
is ListPreference -> change.listState
is MultiSelectListPreference -> change.multiSelectState?.toSet()
else -> throw RuntimeException("sealed class cannot have more subtypes!")
} ?: throw Exception("Expected change to ${preference::class.simpleName}")
}
return UpdateSourcePreferencePayload( return UpdateSourcePreferencePayload(
clientMutationId = clientMutationId, clientMutationId = clientMutationId,
preferences = Source.getSourcePreferences(sourceId).map { PreferenceObject(it.type, it.props) } preferences = Source.getSourcePreferencesRaw(sourceId).map { preferenceOf(it) }
) )
} }
} }
@@ -11,7 +11,6 @@ import com.expediagroup.graphql.generator.SchemaGeneratorConfig
import com.expediagroup.graphql.generator.TopLevelObject import com.expediagroup.graphql.generator.TopLevelObject
import com.expediagroup.graphql.generator.hooks.FlowSubscriptionSchemaGeneratorHooks import com.expediagroup.graphql.generator.hooks.FlowSubscriptionSchemaGeneratorHooks
import com.expediagroup.graphql.generator.toSchema import com.expediagroup.graphql.generator.toSchema
import graphql.scalars.ExtendedScalars
import graphql.schema.GraphQLType import graphql.schema.GraphQLType
import suwayomi.tachidesk.graphql.mutations.CategoryMutation import suwayomi.tachidesk.graphql.mutations.CategoryMutation
import suwayomi.tachidesk.graphql.mutations.ChapterMutation import suwayomi.tachidesk.graphql.mutations.ChapterMutation
@@ -36,7 +35,6 @@ class CustomSchemaGeneratorHooks : FlowSubscriptionSchemaGeneratorHooks() {
override fun willGenerateGraphQLType(type: KType): GraphQLType? = when (type.classifier as? KClass<*>) { override fun willGenerateGraphQLType(type: KType): GraphQLType? = when (type.classifier as? KClass<*>) {
Long::class -> GraphQLLongAsString // encode to string for JS Long::class -> GraphQLLongAsString // encode to string for JS
Cursor::class -> GraphQLCursor Cursor::class -> GraphQLCursor
Any::class -> ExtendedScalars.Json
else -> super.willGenerateGraphQLType(type) else -> super.willGenerateGraphQLType(type)
} }
} }
@@ -10,6 +10,7 @@ package suwayomi.tachidesk.graphql.types
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.FilterList
import graphql.schema.DataFetchingEnvironment import graphql.schema.DataFetchingEnvironment
import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
@@ -18,14 +19,21 @@ import suwayomi.tachidesk.graphql.server.primitives.Edge
import suwayomi.tachidesk.graphql.server.primitives.Node import suwayomi.tachidesk.graphql.server.primitives.Node
import suwayomi.tachidesk.graphql.server.primitives.NodeList import suwayomi.tachidesk.graphql.server.primitives.NodeList
import suwayomi.tachidesk.graphql.server.primitives.PageInfo import suwayomi.tachidesk.graphql.server.primitives.PageInfo
import suwayomi.tachidesk.manga.impl.Search import suwayomi.tachidesk.manga.impl.Source.getSourcePreferencesRaw
import suwayomi.tachidesk.manga.impl.Source
import suwayomi.tachidesk.manga.impl.extension.Extension import suwayomi.tachidesk.manga.impl.extension.Extension
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
import suwayomi.tachidesk.manga.model.dataclass.SourceDataClass import suwayomi.tachidesk.manga.model.dataclass.SourceDataClass
import suwayomi.tachidesk.manga.model.table.ExtensionTable import suwayomi.tachidesk.manga.model.table.ExtensionTable
import suwayomi.tachidesk.manga.model.table.SourceTable import suwayomi.tachidesk.manga.model.table.SourceTable
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import androidx.preference.CheckBoxPreference as SourceCheckBoxPreference
import androidx.preference.EditTextPreference as SourceEditTextPreference
import androidx.preference.ListPreference as SourceListPreference
import androidx.preference.MultiSelectListPreference as SourceMultiSelectListPreference
import androidx.preference.Preference as SourcePreference
import androidx.preference.SwitchPreferenceCompat as SourceSwitchPreference
import eu.kanade.tachiyomi.source.model.Filter as SourceFilter
class SourceType( class SourceType(
val id: Long, val id: Long,
@@ -67,12 +75,12 @@ class SourceType(
return dataFetchingEnvironment.getValueFromDataLoader<Long, ExtensionType>("ExtensionForSourceDataLoader", id) return dataFetchingEnvironment.getValueFromDataLoader<Long, ExtensionType>("ExtensionForSourceDataLoader", id)
} }
fun preferences(): List<PreferenceObject> { fun preferences(): List<Preference> {
return Source.getSourcePreferences(id).map { PreferenceObject(it.type, it.props) } return getSourcePreferencesRaw(id).map { preferenceOf(it) }
} }
fun filters(): List<FilterObject> { fun filters(): List<Filter> {
return Search.getFilterList(id, false).map { FilterObject(it.type, it.filter) } return getCatalogueSourceOrStub(id).getFilterList().map { filterOf(it) }
} }
} }
@@ -133,12 +141,255 @@ data class SourceNodeList(
} }
} }
data class PreferenceObject( sealed interface Filter
val type: String,
val props: Any data class HeaderFilter(val name: String) : Filter
data class SeparatorFilter(val name: String) : Filter
data class SelectFilter(val name: String, val values: List<String>, val default: Int) : Filter
data class TextFilter(val name: String, val default: String) : Filter
data class CheckBoxFilter(val name: String, val default: Boolean) : Filter
enum class TriState {
IGNORE,
INCLUDE,
EXCLUDE
}
data class TriStateFilter(val name: String, val default: TriState) : Filter
data class SortFilter(val name: String, val values: List<String>, val default: SortSelection?) : Filter {
data class SortSelection(val index: Int, val ascending: Boolean) {
constructor(selection: SourceFilter.Sort.Selection) :
this(selection.index, selection.ascending)
}
}
data class GroupFilter(val name: String, val filters: List<Filter>) : Filter
fun filterOf(filter: SourceFilter<*>): Filter {
return when (filter) {
is SourceFilter.Header -> HeaderFilter(filter.name)
is SourceFilter.Separator -> SeparatorFilter(filter.name)
is SourceFilter.Select<*> -> SelectFilter(filter.name, filter.displayValues, filter.state)
is SourceFilter.Text -> TextFilter(filter.name, filter.state)
is SourceFilter.CheckBox -> CheckBoxFilter(filter.name, filter.state)
is SourceFilter.TriState -> TriStateFilter(
filter.name,
when (filter.state) {
SourceFilter.TriState.STATE_INCLUDE -> TriState.INCLUDE
SourceFilter.TriState.STATE_EXCLUDE -> TriState.EXCLUDE
else -> TriState.IGNORE
}
)
is SourceFilter.Group<*> -> GroupFilter(
filter.name,
filter.state.map { filterOf(it as SourceFilter<*>) }
)
is SourceFilter.Sort -> SortFilter(filter.name, filter.values.asList(), filter.state?.let(SortFilter::SortSelection))
else -> throw RuntimeException("sealed class cannot have more subtypes!")
}
}
/*sealed interface FilterChange {
val position: Int
}
data class GroupFilterChange(
override val position: Int,
val filter: FilterChange
) : FilterChange
data class TriStateFilterChange(
override val position: Int,
val state: TriState
) : FilterChange
data class CheckBoxFilterChange(
override val position: Int,
val state: Boolean
) : FilterChange
data class SelectFilterChange(
override val position: Int,
val state: Int
) : FilterChange
data class TextFilterChange(
override val position: Int,
val state: String
) : FilterChange
data class SortFilterChange(
override val position: Int,
val state: SortFilter.SortSelection
) : FilterChange
private inline fun <reified T> filterChangeAs(filterChange: FilterChange): T {
return filterChange as? T ?: throw Exception("Expected ${T::class.simpleName}, found ${filterChange::class.simpleName}")
}*/
data class FilterChange(
val position: Int,
val selectState: Int? = null,
val textState: String? = null,
val checkBoxState: Boolean? = null,
val triState: TriState? = null,
val sortState: SortFilter.SortSelection? = null,
val groupChange: FilterChange? = null
) )
data class FilterObject( fun updateFilterList(source: CatalogueSource, changes: List<FilterChange>?): FilterList {
val type: String, val filterList = source.getFilterList()
val filter: Any
) changes?.forEach { change ->
when (val filter = filterList[1]) {
is SourceFilter.Header -> {
// NOOP
}
is SourceFilter.Separator -> {
// NOOP
}
is SourceFilter.Select<*> -> {
filter.state = change.selectState ?: throw Exception("Expected select state change at position ${change.position}")
}
is SourceFilter.Text -> {
filter.state = change.textState ?: throw Exception("Expected text state change at position ${change.position}")
}
is SourceFilter.CheckBox -> {
filter.state = change.checkBoxState ?: throw Exception("Expected checkbox state change at position ${change.position}")
}
is SourceFilter.TriState -> {
filter.state = change.triState?.ordinal ?: throw Exception("Expected tri state change at position ${change.position}")
}
is SourceFilter.Group<*> -> {
val groupChange = change.groupChange ?: throw Exception("Expected group change at position ${change.position}")
when (val groupFilter = filter.state[1]) {
is SourceFilter.CheckBox -> {
groupFilter.state = groupChange.checkBoxState ?: throw Exception("Expected checkbox state change at position ${change.position}")
}
is SourceFilter.TriState -> {
groupFilter.state = groupChange.triState?.ordinal ?: throw Exception("Expected tri state change at position ${change.position}")
}
is SourceFilter.Text -> {
groupFilter.state = groupChange.textState ?: throw Exception("Expected text state change at position ${change.position}")
}
is SourceFilter.Select<*> -> {
groupFilter.state = groupChange.selectState ?: throw Exception("Expected select state change at position ${change.position}")
}
}
}
is SourceFilter.Sort -> {
filter.state = change.sortState?.run {
SourceFilter.Sort.Selection(index, ascending)
} ?: throw Exception("Expected sort state change at position ${change.position}")
}
}
}
return filterList
}
sealed interface Preference
data class SwitchPreference(
val key: String,
val title: String,
val summary: String?,
val currentValue: Boolean?,
val default: Boolean
) : Preference
data class CheckBoxPreference(
val key: String,
val title: String,
val summary: String?,
val currentValue: Boolean?,
val default: Boolean
) : Preference
data class EditTextPreference(
val key: String,
val title: String?,
val summary: String?,
val currentValue: String?,
val default: String?,
val dialogTitle: String?,
val dialogMessage: String?,
val text: String?
) : Preference
data class ListPreference(
val key: String,
val title: String?,
val summary: String?,
val currentValue: String?,
val default: String?,
val entries: List<String>,
val entryValues: List<String>
) : Preference
data class MultiSelectListPreference(
val key: String,
val title: String?,
val summary: String?,
val currentValue: List<String>?,
val default: List<String>?,
val dialogTitle: String?,
val dialogMessage: String?,
val entries: List<String>,
val entryValues: List<String>
) : Preference
fun preferenceOf(preference: SourcePreference): Preference {
return when (preference) {
is SourceSwitchPreference -> SwitchPreference(
preference.key,
preference.title.toString(),
preference.summary?.toString(),
preference.currentValue as Boolean,
preference.defaultValue as Boolean
)
is SourceCheckBoxPreference -> CheckBoxPreference(
preference.key,
preference.title.toString(),
preference.summary?.toString(),
preference.currentValue as Boolean,
preference.defaultValue as Boolean
)
is SourceEditTextPreference -> EditTextPreference(
preference.key,
preference.title?.toString(),
preference.summary?.toString(),
(preference.currentValue as CharSequence?)?.toString(),
(preference.defaultValue as CharSequence?)?.toString(),
preference.dialogTitle?.toString(),
preference.dialogMessage?.toString(),
preference.text
)
is SourceListPreference -> ListPreference(
preference.key,
preference.title?.toString(),
preference.summary?.toString(),
(preference.currentValue as CharSequence?)?.toString(),
(preference.defaultValue as CharSequence?)?.toString(),
preference.entries.map { it.toString() },
preference.entryValues.map { it.toString() }
)
is SourceMultiSelectListPreference -> MultiSelectListPreference(
preference.key,
preference.title?.toString(),
preference.summary?.toString(),
(preference.currentValue as Collection<*>?)?.map { it.toString() },
(preference.defaultValue as Collection<*>?)?.map { it.toString() },
preference.dialogTitle?.toString(),
preference.dialogMessage?.toString(),
preference.entries.map { it.toString() },
preference.entryValues.map { it.toString() }
)
else -> throw RuntimeException("sealed class cannot have more subtypes!")
}
}
@@ -135,7 +135,7 @@ object SourceController {
}, },
behaviorOf = { ctx, sourceId -> behaviorOf = { ctx, sourceId ->
val preferenceChange = ctx.bodyAsClass(SourcePreferenceChange::class.java) val preferenceChange = ctx.bodyAsClass(SourcePreferenceChange::class.java)
ctx.json(Source.setSourcePreference(sourceId, preferenceChange)) ctx.json(Source.setSourcePreference(sourceId, preferenceChange.position, preferenceChange.value))
}, },
withResults = { withResults = {
httpCode(HttpCode.OK) httpCode(HttpCode.OK)
@@ -9,6 +9,7 @@ package suwayomi.tachidesk.manga.impl
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import androidx.preference.Preference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.getPreferenceKey import eu.kanade.tachiyomi.source.getPreferenceKey
@@ -96,6 +97,12 @@ object Source {
* Gets a source's PreferenceScreen, puts the result into [preferenceScreenMap] * Gets a source's PreferenceScreen, puts the result into [preferenceScreenMap]
*/ */
fun getSourcePreferences(sourceId: Long): List<PreferenceObject> { fun getSourcePreferences(sourceId: Long): List<PreferenceObject> {
return getSourcePreferencesRaw(sourceId).map {
PreferenceObject(it::class.java.simpleName, it)
}
}
fun getSourcePreferencesRaw(sourceId: Long): List<Preference> {
val source = getCatalogueSourceOrStub(sourceId) val source = getCatalogueSourceOrStub(sourceId)
if (source is ConfigurableSource) { if (source is ConfigurableSource) {
@@ -109,9 +116,7 @@ object Source {
preferenceScreenMap[sourceId] = screen preferenceScreenMap[sourceId] = screen
return screen.preferences.map { return screen.preferences
PreferenceObject(it::class.java.simpleName, it)
}
} }
return emptyList() return emptyList()
} }
@@ -123,18 +128,25 @@ object Source {
private val jsonMapper by DI.global.instance<JsonMapper>() private val jsonMapper by DI.global.instance<JsonMapper>()
@Suppress("IMPLICIT_CAST_TO_ANY", "UNCHECKED_CAST") fun setSourcePreference(
fun setSourcePreference(sourceId: Long, change: SourcePreferenceChange) { sourceId: Long,
val screen = preferenceScreenMap[sourceId]!! position: Int,
val pref = screen.preferences[change.position] value: String,
getValue: (Preference) -> Any = { pref ->
println(jsonMapper::class.java.name) println(jsonMapper::class.java.name)
val newValue = when (pref.defaultValueType) { @Suppress("UNCHECKED_CAST")
"String" -> change.value when (pref.defaultValueType) {
"Boolean" -> change.value.toBoolean() "String" -> value
"Set<String>" -> jsonMapper.fromJsonString(change.value, List::class.java as Class<List<String>>).toSet() "Boolean" -> value.toBoolean()
else -> throw RuntimeException("Unsupported type conversion") "Set<String>" -> jsonMapper.fromJsonString(value, List::class.java as Class<List<String>>).toSet()
else -> throw RuntimeException("Unsupported type conversion")
}
} }
) {
val screen = preferenceScreenMap[sourceId]!!
val pref = screen.preferences[position]
val newValue = getValue(pref)
pref.saveNewValue(newValue) pref.saveNewValue(newValue)
pref.callChangeListener(newValue) pref.callChangeListener(newValue)