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

View File

@@ -1,11 +1,18 @@
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.transactions.transaction
import suwayomi.tachidesk.graphql.types.FilterChange
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.Search
import suwayomi.tachidesk.manga.impl.Source
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
@@ -20,10 +27,6 @@ class SourceMutation {
POPULAR,
LATEST
}
data class FilterChange(
val position: Int,
val state: String
)
data class FetchSourceMangaInput(
val clientMutationId: String? = null,
val source: Long,
@@ -50,11 +53,7 @@ class SourceMutation {
source.fetchSearchManga(
page = page,
query = query.orEmpty(),
filters = Search.buildFilterList(
sourceId = sourceId,
changes = filters?.map { Search.FilterChange(it.position, it.state) }
.orEmpty()
)
filters = updateFilterList(source, filters)
).awaitSingle()
}
FetchSourceMangaType.POPULAR -> {
@@ -85,7 +84,11 @@ class SourceMutation {
data class SourcePreferenceChange(
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(
val clientMutationId: String? = null,
@@ -94,7 +97,7 @@ class SourceMutation {
)
data class UpdateSourcePreferencePayload(
val clientMutationId: String?,
val preferences: List<PreferenceObject>
val preferences: List<Preference>
)
fun updateSourcePreference(
@@ -102,11 +105,20 @@ class SourceMutation {
): UpdateSourcePreferencePayload {
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(
clientMutationId = clientMutationId,
preferences = Source.getSourcePreferences(sourceId).map { PreferenceObject(it.type, it.props) }
preferences = Source.getSourcePreferencesRaw(sourceId).map { preferenceOf(it) }
)
}
}

View File

@@ -11,7 +11,6 @@ import com.expediagroup.graphql.generator.SchemaGeneratorConfig
import com.expediagroup.graphql.generator.TopLevelObject
import com.expediagroup.graphql.generator.hooks.FlowSubscriptionSchemaGeneratorHooks
import com.expediagroup.graphql.generator.toSchema
import graphql.scalars.ExtendedScalars
import graphql.schema.GraphQLType
import suwayomi.tachidesk.graphql.mutations.CategoryMutation
import suwayomi.tachidesk.graphql.mutations.ChapterMutation
@@ -36,7 +35,6 @@ class CustomSchemaGeneratorHooks : FlowSubscriptionSchemaGeneratorHooks() {
override fun willGenerateGraphQLType(type: KType): GraphQLType? = when (type.classifier as? KClass<*>) {
Long::class -> GraphQLLongAsString // encode to string for JS
Cursor::class -> GraphQLCursor
Any::class -> ExtendedScalars.Json
else -> super.willGenerateGraphQLType(type)
}
}

View File

@@ -10,6 +10,7 @@ package suwayomi.tachidesk.graphql.types
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.FilterList
import graphql.schema.DataFetchingEnvironment
import org.jetbrains.exposed.sql.ResultRow
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.NodeList
import suwayomi.tachidesk.graphql.server.primitives.PageInfo
import suwayomi.tachidesk.manga.impl.Search
import suwayomi.tachidesk.manga.impl.Source
import suwayomi.tachidesk.manga.impl.Source.getSourcePreferencesRaw
import suwayomi.tachidesk.manga.impl.extension.Extension
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.table.ExtensionTable
import suwayomi.tachidesk.manga.model.table.SourceTable
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(
val id: Long,
@@ -67,12 +75,12 @@ class SourceType(
return dataFetchingEnvironment.getValueFromDataLoader<Long, ExtensionType>("ExtensionForSourceDataLoader", id)
}
fun preferences(): List<PreferenceObject> {
return Source.getSourcePreferences(id).map { PreferenceObject(it.type, it.props) }
fun preferences(): List<Preference> {
return getSourcePreferencesRaw(id).map { preferenceOf(it) }
}
fun filters(): List<FilterObject> {
return Search.getFilterList(id, false).map { FilterObject(it.type, it.filter) }
fun filters(): List<Filter> {
return getCatalogueSourceOrStub(id).getFilterList().map { filterOf(it) }
}
}
@@ -133,12 +141,255 @@ data class SourceNodeList(
}
}
data class PreferenceObject(
val type: String,
val props: Any
sealed interface Filter
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(
val type: String,
val filter: Any
)
fun updateFilterList(source: CatalogueSource, changes: List<FilterChange>?): FilterList {
val filterList = source.getFilterList()
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!")
}
}

View File

@@ -135,7 +135,7 @@ object SourceController {
},
behaviorOf = { ctx, sourceId ->
val preferenceChange = ctx.bodyAsClass(SourcePreferenceChange::class.java)
ctx.json(Source.setSourcePreference(sourceId, preferenceChange))
ctx.json(Source.setSourcePreference(sourceId, preferenceChange.position, preferenceChange.value))
},
withResults = {
httpCode(HttpCode.OK)

View File

@@ -9,6 +9,7 @@ package suwayomi.tachidesk.manga.impl
import android.app.Application
import android.content.Context
import androidx.preference.Preference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.getPreferenceKey
@@ -96,6 +97,12 @@ object Source {
* Gets a source's PreferenceScreen, puts the result into [preferenceScreenMap]
*/
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)
if (source is ConfigurableSource) {
@@ -109,9 +116,7 @@ object Source {
preferenceScreenMap[sourceId] = screen
return screen.preferences.map {
PreferenceObject(it::class.java.simpleName, it)
}
return screen.preferences
}
return emptyList()
}
@@ -123,18 +128,25 @@ object Source {
private val jsonMapper by DI.global.instance<JsonMapper>()
@Suppress("IMPLICIT_CAST_TO_ANY", "UNCHECKED_CAST")
fun setSourcePreference(sourceId: Long, change: SourcePreferenceChange) {
val screen = preferenceScreenMap[sourceId]!!
val pref = screen.preferences[change.position]
println(jsonMapper::class.java.name)
val newValue = when (pref.defaultValueType) {
"String" -> change.value
"Boolean" -> change.value.toBoolean()
"Set<String>" -> jsonMapper.fromJsonString(change.value, List::class.java as Class<List<String>>).toSet()
else -> throw RuntimeException("Unsupported type conversion")
fun setSourcePreference(
sourceId: Long,
position: Int,
value: String,
getValue: (Preference) -> Any = { pref ->
println(jsonMapper::class.java.name)
@Suppress("UNCHECKED_CAST")
when (pref.defaultValueType) {
"String" -> value
"Boolean" -> value.toBoolean()
"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.callChangeListener(newValue)