diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SourceMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SourceMutation.kt index 8eb02fb3a..6c378eeaf 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SourceMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SourceMutation.kt @@ -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? = null ) data class UpdateSourcePreferenceInput( val clientMutationId: String? = null, @@ -94,7 +97,7 @@ class SourceMutation { ) data class UpdateSourcePreferencePayload( val clientMutationId: String?, - val preferences: List + val preferences: List ) 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) } ) } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt index 3f36703fd..e42a0d453 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt @@ -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) } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt index f41e7a59c..f01ea6e00 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt @@ -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("ExtensionForSourceDataLoader", id) } - fun preferences(): List { - return Source.getSourcePreferences(id).map { PreferenceObject(it.type, it.props) } + fun preferences(): List { + return getSourcePreferencesRaw(id).map { preferenceOf(it) } } - fun filters(): List { - return Search.getFilterList(id, false).map { FilterObject(it.type, it.filter) } + fun filters(): List { + 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, 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, 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 + +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 FilterObject( - val type: String, - val filter: Any +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 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 ) + +fun updateFilterList(source: CatalogueSource, changes: List?): 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, + val entryValues: List +) : Preference + +data class MultiSelectListPreference( + val key: String, + val title: String?, + val summary: String?, + val currentValue: List?, + val default: List?, + val dialogTitle: String?, + val dialogMessage: String?, + val entries: List, + val entryValues: List +) : 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!") + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/SourceController.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/SourceController.kt index 41f881ce4..6af9c8230 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/SourceController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/SourceController.kt @@ -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) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Source.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Source.kt index d151abe8c..1c63cf594 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Source.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Source.kt @@ -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 { + return getSourcePreferencesRaw(sourceId).map { + PreferenceObject(it::class.java.simpleName, it) + } + } + + fun getSourcePreferencesRaw(sourceId: Long): List { 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() - @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" -> jsonMapper.fromJsonString(change.value, List::class.java as Class>).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" -> jsonMapper.fromJsonString(value, List::class.java as Class>).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)