From edd2dc086fe06aaaab93fec28bc4318b45a0cf40 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Thu, 5 Aug 2021 20:24:51 -0400 Subject: [PATCH] Implement source configuration --- .../kotlin/ca/gosyer/data/models/Source.kt | 3 +- .../sourcepreference/CheckBoxPreference.kt | 16 ++ .../sourcepreference/EditTextPreference.kt | 29 ++++ .../models/sourcepreference/ListPreference.kt | 26 +++ .../sourcepreference/SourcePreference.kt | 23 +++ .../SourcePreferenceChange.kt | 12 ++ .../sourcepreference/SwitchPreference.kt | 16 ++ .../models/sourcepreference/TwoStateProps.kt | 19 ++ .../interactions/SourceInteractionHandler.kt | 29 ++++ .../ca/gosyer/data/server/requests/Manga.kt | 1 + .../ca/gosyer/data/server/requests/Sources.kt | 8 + .../ui/base/prefs/PreferencesUiBuilder.kt | 2 +- src/main/kotlin/ca/gosyer/ui/main/MainMenu.kt | 27 ++- .../ca/gosyer/ui/sources/SourcesMenu.kt | 49 ++++-- .../ui/sources/settings/SourceSettingsMenu.kt | 163 ++++++++++++++++++ .../settings/SourceSettingsViewModel.kt | 70 ++++++++ .../settings/model/SourceSettingsView.kt | 145 ++++++++++++++++ 17 files changed, 617 insertions(+), 21 deletions(-) create mode 100644 src/main/kotlin/ca/gosyer/data/models/sourcepreference/CheckBoxPreference.kt create mode 100644 src/main/kotlin/ca/gosyer/data/models/sourcepreference/EditTextPreference.kt create mode 100644 src/main/kotlin/ca/gosyer/data/models/sourcepreference/ListPreference.kt create mode 100644 src/main/kotlin/ca/gosyer/data/models/sourcepreference/SourcePreference.kt create mode 100644 src/main/kotlin/ca/gosyer/data/models/sourcepreference/SourcePreferenceChange.kt create mode 100644 src/main/kotlin/ca/gosyer/data/models/sourcepreference/SwitchPreference.kt create mode 100644 src/main/kotlin/ca/gosyer/data/models/sourcepreference/TwoStateProps.kt create mode 100644 src/main/kotlin/ca/gosyer/ui/sources/settings/SourceSettingsMenu.kt create mode 100644 src/main/kotlin/ca/gosyer/ui/sources/settings/SourceSettingsViewModel.kt create mode 100644 src/main/kotlin/ca/gosyer/ui/sources/settings/model/SourceSettingsView.kt diff --git a/src/main/kotlin/ca/gosyer/data/models/Source.kt b/src/main/kotlin/ca/gosyer/data/models/Source.kt index db0efdbe..283637dc 100644 --- a/src/main/kotlin/ca/gosyer/data/models/Source.kt +++ b/src/main/kotlin/ca/gosyer/data/models/Source.kt @@ -14,7 +14,8 @@ data class Source( val name: String, val lang: String, val iconUrl: String, - val supportsLatest: Boolean + val supportsLatest: Boolean, + val isConfigurable: Boolean ) { fun iconUrl(serverUrl: String) = serverUrl + iconUrl } diff --git a/src/main/kotlin/ca/gosyer/data/models/sourcepreference/CheckBoxPreference.kt b/src/main/kotlin/ca/gosyer/data/models/sourcepreference/CheckBoxPreference.kt new file mode 100644 index 00000000..0bdf383e --- /dev/null +++ b/src/main/kotlin/ca/gosyer/data/models/sourcepreference/CheckBoxPreference.kt @@ -0,0 +1,16 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.data.models.sourcepreference + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +@SerialName("CheckBoxPreference") +data class CheckBoxPreference( + override val props: TwoStateProps +) : SourcePreference() diff --git a/src/main/kotlin/ca/gosyer/data/models/sourcepreference/EditTextPreference.kt b/src/main/kotlin/ca/gosyer/data/models/sourcepreference/EditTextPreference.kt new file mode 100644 index 00000000..76840196 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/data/models/sourcepreference/EditTextPreference.kt @@ -0,0 +1,29 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.data.models.sourcepreference + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +@SerialName("EditTextPreference") +data class EditTextPreference( + override val props: EditTextProps +) : SourcePreference() { + @Serializable + data class EditTextProps( + override val key: String, + override val title: String?, + override val summary: String?, + override val currentValue: String?, + override val defaultValue: String?, + override val defaultValueType: String, + val dialogTitle: String?, + val dialogMessage: String?, + val text: String? + ) : Props +} diff --git a/src/main/kotlin/ca/gosyer/data/models/sourcepreference/ListPreference.kt b/src/main/kotlin/ca/gosyer/data/models/sourcepreference/ListPreference.kt new file mode 100644 index 00000000..c921567a --- /dev/null +++ b/src/main/kotlin/ca/gosyer/data/models/sourcepreference/ListPreference.kt @@ -0,0 +1,26 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.data.models.sourcepreference + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +@SerialName("ListPreference") +data class ListPreference(override val props: ListProps) : SourcePreference() { + @Serializable + data class ListProps( + override val key: String, + override val title: String, + override val summary: String?, + override val currentValue: String?, + override val defaultValue: String?, + override val defaultValueType: String, + val entries: List, + val entryValues: List + ) : Props +} diff --git a/src/main/kotlin/ca/gosyer/data/models/sourcepreference/SourcePreference.kt b/src/main/kotlin/ca/gosyer/data/models/sourcepreference/SourcePreference.kt new file mode 100644 index 00000000..37526088 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/data/models/sourcepreference/SourcePreference.kt @@ -0,0 +1,23 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.data.models.sourcepreference + +import kotlinx.serialization.Serializable + +@Serializable +sealed class SourcePreference { + abstract val props: Props<*> +} + +interface Props { + val key: String + val title: String? + val summary: String? + val currentValue: T + val defaultValue: T + val defaultValueType: String +} diff --git a/src/main/kotlin/ca/gosyer/data/models/sourcepreference/SourcePreferenceChange.kt b/src/main/kotlin/ca/gosyer/data/models/sourcepreference/SourcePreferenceChange.kt new file mode 100644 index 00000000..9f5a7916 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/data/models/sourcepreference/SourcePreferenceChange.kt @@ -0,0 +1,12 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.data.models.sourcepreference + +import kotlinx.serialization.Serializable + +@Serializable +data class SourcePreferenceChange(val position: Int, val value: String) diff --git a/src/main/kotlin/ca/gosyer/data/models/sourcepreference/SwitchPreference.kt b/src/main/kotlin/ca/gosyer/data/models/sourcepreference/SwitchPreference.kt new file mode 100644 index 00000000..dffb2ce1 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/data/models/sourcepreference/SwitchPreference.kt @@ -0,0 +1,16 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.data.models.sourcepreference + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +@SerialName("SwitchPreferenceCompat") +data class SwitchPreference( + override val props: TwoStateProps +) : SourcePreference() diff --git a/src/main/kotlin/ca/gosyer/data/models/sourcepreference/TwoStateProps.kt b/src/main/kotlin/ca/gosyer/data/models/sourcepreference/TwoStateProps.kt new file mode 100644 index 00000000..091d7dd6 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/data/models/sourcepreference/TwoStateProps.kt @@ -0,0 +1,19 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.data.models.sourcepreference + +import kotlinx.serialization.Serializable + +@Serializable +data class TwoStateProps( + override val key: String, + override val title: String?, + override val summary: String?, + override val currentValue: Boolean?, + override val defaultValue: Boolean?, + override val defaultValueType: String +) : Props diff --git a/src/main/kotlin/ca/gosyer/data/server/interactions/SourceInteractionHandler.kt b/src/main/kotlin/ca/gosyer/data/server/interactions/SourceInteractionHandler.kt index cbbe1b06..6aa77cd8 100644 --- a/src/main/kotlin/ca/gosyer/data/server/interactions/SourceInteractionHandler.kt +++ b/src/main/kotlin/ca/gosyer/data/server/interactions/SourceInteractionHandler.kt @@ -8,18 +8,25 @@ package ca.gosyer.data.server.interactions import ca.gosyer.data.models.MangaPage import ca.gosyer.data.models.Source +import ca.gosyer.data.models.sourcepreference.SourcePreference +import ca.gosyer.data.models.sourcepreference.SourcePreferenceChange import ca.gosyer.data.server.Http import ca.gosyer.data.server.ServerPreferences import ca.gosyer.data.server.requests.getFilterListQuery +import ca.gosyer.data.server.requests.getSourceSettingsQuery import ca.gosyer.data.server.requests.globalSearchQuery import ca.gosyer.data.server.requests.sourceInfoQuery import ca.gosyer.data.server.requests.sourceLatestQuery import ca.gosyer.data.server.requests.sourceListQuery import ca.gosyer.data.server.requests.sourcePopularQuery import ca.gosyer.data.server.requests.sourceSearchQuery +import ca.gosyer.data.server.requests.updateSourceSettingQuery import ca.gosyer.util.lang.withIOContext import io.ktor.client.request.get +import io.ktor.client.request.post import io.ktor.client.statement.HttpResponse +import io.ktor.http.ContentType +import io.ktor.http.contentType import javax.inject.Inject class SourceInteractionHandler @Inject constructor( @@ -90,4 +97,26 @@ class SourceInteractionHandler @Inject constructor( } suspend fun getFilterList(source: Source) = getFilterList(source.id) + + suspend fun getSourceSettings(sourceId: Long) = withIOContext { + client.get>( + serverUrl + getSourceSettingsQuery(sourceId) + ) + } + + suspend fun getSourceSettings(source: Source) = getSourceSettings(source.id) + + suspend fun setSourceSetting(sourceId: Long, sourcePreference: SourcePreferenceChange) = withIOContext { + client.post( + serverUrl + updateSourceSettingQuery(sourceId) + ) { + contentType(ContentType.Application.Json) + body = sourcePreference + } + } + + suspend fun setSourceSetting(sourceId: Long, position: Int, value: Any) = setSourceSetting( + sourceId, + SourcePreferenceChange(position, value.toString()) + ) } diff --git a/src/main/kotlin/ca/gosyer/data/server/requests/Manga.kt b/src/main/kotlin/ca/gosyer/data/server/requests/Manga.kt index 225b1121..997443b8 100644 --- a/src/main/kotlin/ca/gosyer/data/server/requests/Manga.kt +++ b/src/main/kotlin/ca/gosyer/data/server/requests/Manga.kt @@ -14,5 +14,6 @@ fun mangaQuery(mangaId: Long) = fun mangaThumbnailQuery(mangaId: Long) = "/api/v1/manga/$mangaId/thumbnail" +@Post fun updateMangaMetaRequest(mangaId: Long) = "/api/v1/manga/$mangaId/meta" diff --git a/src/main/kotlin/ca/gosyer/data/server/requests/Sources.kt b/src/main/kotlin/ca/gosyer/data/server/requests/Sources.kt index 2df5e15b..958fc709 100644 --- a/src/main/kotlin/ca/gosyer/data/server/requests/Sources.kt +++ b/src/main/kotlin/ca/gosyer/data/server/requests/Sources.kt @@ -33,3 +33,11 @@ fun sourceSearchQuery(sourceId: Long, searchTerm: String, pageNum: Int) = @Get fun getFilterListQuery(sourceId: Long) = "/api/v1/source/$sourceId/filters/" + +@Get +fun getSourceSettingsQuery(sourceId: Long) = + "/api/v1/source/$sourceId/preferences" + +@Post +fun updateSourceSettingQuery(sourceId: Long) = + "/api/v1/source/$sourceId/preferences" diff --git a/src/main/kotlin/ca/gosyer/ui/base/prefs/PreferencesUiBuilder.kt b/src/main/kotlin/ca/gosyer/ui/base/prefs/PreferencesUiBuilder.kt index 92d07680..6070601a 100644 --- a/src/main/kotlin/ca/gosyer/ui/base/prefs/PreferencesUiBuilder.kt +++ b/src/main/kotlin/ca/gosyer/ui/base/prefs/PreferencesUiBuilder.kt @@ -214,7 +214,7 @@ fun ChoicePreference( ) } -private fun ChoiceDialog( +fun ChoiceDialog( items: List>, selected: T?, onDismissRequest: () -> Unit = {}, diff --git a/src/main/kotlin/ca/gosyer/ui/main/MainMenu.kt b/src/main/kotlin/ca/gosyer/ui/main/MainMenu.kt index 9675aa64..c72b0347 100644 --- a/src/main/kotlin/ca/gosyer/ui/main/MainMenu.kt +++ b/src/main/kotlin/ca/gosyer/ui/main/MainMenu.kt @@ -71,6 +71,7 @@ import ca.gosyer.ui.settings.SettingsScreen import ca.gosyer.ui.settings.SettingsServerScreen import ca.gosyer.ui.sources.SourcesMenu import ca.gosyer.ui.sources.openSourcesMenu +import ca.gosyer.ui.sources.settings.SourceSettingsMenu import com.github.zsoltk.compose.router.BackStack import com.github.zsoltk.compose.router.Router import com.github.zsoltk.compose.savedinstancestate.Bundle @@ -140,19 +141,25 @@ fun SideMenuItem(topLevelMenu: TopLevelMenus, backStack: BackStack) { @Composable fun MainWindow(rootBundle: Bundle, backStack: BackStack) { - Column(Modifier.fillMaxSize()) { + Box(Modifier.fillMaxSize()) { BundleScope("K${backStack.lastIndex}", rootBundle, false) { when (val routing = backStack.last()) { is Route.Library -> LibraryScreen { backStack.push(Route.Manga(it)) } - is Route.Sources -> SourcesMenu { + is Route.Sources -> SourcesMenu( + { + backStack.push(Route.SourceSettings(it)) + } + ) { backStack.push(Route.Manga(it)) } is Route.Extensions -> ExtensionsMenu() is Route.Manga -> MangaMenu(routing.mangaId, backStack) is Route.Downloads -> DownloadsMenu() + is Route.SourceSettings -> SourceSettingsMenu(routing.sourceId, backStack) + is Route.Settings -> SettingsScreen(backStack) is Route.SettingsGeneral -> SettingsGeneralScreen(backStack) is Route.SettingsAppearance -> SettingsAppearance(backStack) @@ -168,6 +175,20 @@ fun MainWindow(rootBundle: Bundle, backStack: BackStack) { is Route.SettingsAdvanced -> SettingsAdvancedScreen(backStack) } } + /*Box(Modifier.padding(bottom = 32.dp).align(Alignment.BottomCenter)) { + val shape = RoundedCornerShape(50.dp) + Box( + Modifier + .width(200.dp) + .defaultMinSize(minHeight = 64.dp) + .shadow(4.dp, shape) + .background(SolidColor(Color.Gray), alpha = 0.2F) + .clip(shape), + contentAlignment = Alignment.Center + ) { + Text("Test text") + } + }*/ } } @@ -283,6 +304,8 @@ sealed class Route { data class Manga(val mangaId: Long) : Route() object Downloads : Route() + data class SourceSettings(val sourceId: Long) : Route() + object Settings : Route() object SettingsGeneral : Route() object SettingsAppearance : Route() diff --git a/src/main/kotlin/ca/gosyer/ui/sources/SourcesMenu.kt b/src/main/kotlin/ca/gosyer/ui/sources/SourcesMenu.kt index 75fe4f54..3449b74c 100644 --- a/src/main/kotlin/ca/gosyer/ui/sources/SourcesMenu.kt +++ b/src/main/kotlin/ca/gosyer/ui/sources/SourcesMenu.kt @@ -22,6 +22,7 @@ import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Home +import androidx.compose.material.icons.rounded.Settings import androidx.compose.material.icons.rounded.Translate import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -43,6 +44,7 @@ import ca.gosyer.ui.extensions.LanguageDialog import ca.gosyer.ui.manga.openMangaMenu import ca.gosyer.ui.sources.components.SourceHomeScreen import ca.gosyer.ui.sources.components.SourceScreen +import ca.gosyer.ui.sources.settings.openSourceSettingsMenu import ca.gosyer.util.compose.ThemedWindow import com.github.zsoltk.compose.savedinstancestate.Bundle import com.github.zsoltk.compose.savedinstancestate.BundleScope @@ -54,20 +56,21 @@ fun openSourcesMenu() { CompositionLocalProvider( LocalSavedInstanceState provides Bundle() ) { - SourcesMenu { - openMangaMenu(it) - } + SourcesMenu( + ::openSourceSettingsMenu, + ::openMangaMenu + ) } } } @Composable -fun SourcesMenu(onMangaClick: (Long) -> Unit) { - SourcesMenu(LocalSavedInstanceState.current, onMangaClick) +fun SourcesMenu(onSourceSettingsClick: (Long) -> Unit, onMangaClick: (Long) -> Unit) { + SourcesMenu(LocalSavedInstanceState.current, onSourceSettingsClick, onMangaClick) } @Composable -fun SourcesMenu(bundle: Bundle, onMangaClick: (Long) -> Unit) { +fun SourcesMenu(bundle: Bundle, onSourceSettingsClick: (Long) -> Unit, onMangaClick: (Long) -> Unit) { val vm = viewModel { bundle } @@ -93,17 +96,29 @@ fun SourcesMenu(bundle: Bundle, onMangaClick: (Long) -> Unit) { search = if (sourceSearchEnabled) vm::search else null, searchSubmit = vm::submitSearch, actions = { - if (selectedSourceTab == null) { - ActionIcon( - { - val enabledLangs = MutableStateFlow(vm.languages.value) - LanguageDialog(enabledLangs, vm.getSourceLanguages().toList()) { - vm.setEnabledLanguages(enabledLangs.value) - } - }, - stringResource("enabled_languages"), - Icons.Rounded.Translate - ) + selectedSourceTab.let { selectedSource -> + if (selectedSource == null) { + ActionIcon( + { + val enabledLangs = MutableStateFlow(vm.languages.value) + LanguageDialog(enabledLangs, vm.getSourceLanguages().toList()) { + vm.setEnabledLanguages(enabledLangs.value) + } + }, + stringResource("enabled_languages"), + Icons.Rounded.Translate + ) + } else { + if (selectedSource.isConfigurable) { + ActionIcon( + { + onSourceSettingsClick(selectedSource.id) + }, + stringResource("location_settings"), + Icons.Rounded.Settings + ) + } + } } } ) diff --git a/src/main/kotlin/ca/gosyer/ui/sources/settings/SourceSettingsMenu.kt b/src/main/kotlin/ca/gosyer/ui/sources/settings/SourceSettingsMenu.kt new file mode 100644 index 00000000..effabbb7 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/ui/sources/settings/SourceSettingsMenu.kt @@ -0,0 +1,163 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.ui.sources.settings + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.Checkbox +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Switch +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import ca.gosyer.BuildConfig +import ca.gosyer.ui.base.WindowDialog +import ca.gosyer.ui.base.components.Toolbar +import ca.gosyer.ui.base.prefs.ChoiceDialog +import ca.gosyer.ui.base.prefs.PreferenceRow +import ca.gosyer.ui.base.resources.stringResource +import ca.gosyer.ui.base.vm.viewModel +import ca.gosyer.ui.main.Route +import ca.gosyer.ui.sources.settings.model.SourceSettingsView.CheckBox +import ca.gosyer.ui.sources.settings.model.SourceSettingsView.EditText +import ca.gosyer.ui.sources.settings.model.SourceSettingsView.List +import ca.gosyer.ui.sources.settings.model.SourceSettingsView.Switch +import ca.gosyer.ui.sources.settings.model.SourceSettingsView.TwoState +import ca.gosyer.util.compose.ThemedWindow +import com.github.zsoltk.compose.router.BackStack +import kotlinx.coroutines.flow.MutableStateFlow + +fun openSourceSettingsMenu(sourceId: Long) { + ThemedWindow(BuildConfig.NAME) { + SourceSettingsMenu(sourceId) + } +} + +@Composable +fun SourceSettingsMenu(sourceId: Long, backStack: BackStack? = null) { + val vm = viewModel { + SourceSettingsViewModel.Params(sourceId) + } + val settings by vm.sourceSettings.collectAsState() + + Column { + Toolbar(stringResource("location_settings"), backStack, backStack != null) + LazyColumn { + items(settings, { it.props.hashCode() }) { + when (it) { + is CheckBox, is Switch -> { + TwoStatePreference(it as TwoState, it is CheckBox) + } + is List -> { + ListPreference(it) + } + is EditText -> { + EditTextPreference(it) + } + else -> Unit + } + } + } + } +} + +@Composable +private fun TwoStatePreference(twoState: TwoState, checkbox: Boolean) { + val state by twoState.state.collectAsState() + val title = remember(state) { twoState.title ?: twoState.summary ?: "No title" } + val subtitle = remember(state) { + if (twoState.title == null) { + null + } else { + twoState.summary + } + } + PreferenceRow( + title, + subtitle = subtitle, + onClick = { twoState.updateState(!state) }, + action = { + if (checkbox) { + Checkbox(checked = state, onCheckedChange = null) + } else { + Switch(checked = state, onCheckedChange = null) + } + } + ) +} + +@Composable +private fun ListPreference(list: List) { + val state by list.state.collectAsState() + val title = remember(state) { list.title ?: list.summary ?: "No title" } + val subtitle = remember(state) { + if (list.title == null) { + null + } else { + list.summary + } + } + PreferenceRow( + title, + subtitle = subtitle, + onClick = { + ChoiceDialog( + list.getOptions(), + state.first, + onSelected = list::setValue, + title = "Select choice" + ) + } + ) +} + +@Composable +private fun EditTextPreference(editText: EditText) { + val state by editText.state.collectAsState() + val title = remember(state) { editText.title ?: editText.summary ?: "No title" } + val subtitle = remember(state) { + if (editText.title == null) { + null + } else { + editText.summary + } + } + PreferenceRow( + title, + subtitle = subtitle, + onClick = { + val editTextFlow = MutableStateFlow(TextFieldValue(state)) + WindowDialog( + editText.dialogTitle ?: BuildConfig.NAME, + onPositiveButton = { + editText.updateState(editTextFlow.value.text) + } + ) { + if (editText.dialogMessage != null) { + Text(editText.dialogMessage) + Spacer(Modifier.height(8.dp)) + } + + val text by editTextFlow.collectAsState() + OutlinedTextField( + text, + onValueChange = { + editTextFlow.value = it + } + ) + } + } + ) +} diff --git a/src/main/kotlin/ca/gosyer/ui/sources/settings/SourceSettingsViewModel.kt b/src/main/kotlin/ca/gosyer/ui/sources/settings/SourceSettingsViewModel.kt new file mode 100644 index 00000000..bd733435 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/ui/sources/settings/SourceSettingsViewModel.kt @@ -0,0 +1,70 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.ui.sources.settings + +import ca.gosyer.data.models.sourcepreference.SourcePreference +import ca.gosyer.data.server.interactions.SourceInteractionHandler +import ca.gosyer.ui.base.vm.ViewModel +import ca.gosyer.ui.sources.settings.model.SourceSettingsView +import ca.gosyer.util.lang.throwIfCancellation +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import java.util.concurrent.CopyOnWriteArrayList +import javax.inject.Inject + +class SourceSettingsViewModel @Inject constructor( + private val params: Params, + private val sourceHandler: SourceInteractionHandler +) : ViewModel() { + private val _loading = MutableStateFlow(true) + val loading = _loading.asStateFlow() + + private val _sourceSettings = MutableStateFlow>>(emptyList()) + val sourceSettings = _sourceSettings.asStateFlow() + + private val subscriptions: CopyOnWriteArrayList = CopyOnWriteArrayList() + + init { + getSourceSettings() + + sourceSettings.onEach { settings -> + subscriptions.forEach { it.cancel() } + subscriptions.clear() + subscriptions += settings.map { setting -> + setting.state.drop(1).onEach { + if (it != null) { + sourceHandler.setSourceSetting(params.sourceId, setting.index, it) + getSourceSettings() + } + }.launchIn(scope) + } + }.launchIn(scope) + } + + private fun getSourceSettings() { + scope.launch { + try { + _sourceSettings.value = sourceHandler.getSourceSettings(params.sourceId).toView() + } catch (e: Exception) { + e.throwIfCancellation() + } finally { + _loading.value = false + } + } + } + + data class Params(val sourceId: Long) + + private fun List.toView() = mapIndexed { index, sourcePreference -> + SourceSettingsView(index, sourcePreference) + } +} diff --git a/src/main/kotlin/ca/gosyer/ui/sources/settings/model/SourceSettingsView.kt b/src/main/kotlin/ca/gosyer/ui/sources/settings/model/SourceSettingsView.kt new file mode 100644 index 00000000..5f447e03 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/ui/sources/settings/model/SourceSettingsView.kt @@ -0,0 +1,145 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.ui.sources.settings.model + +import ca.gosyer.data.models.sourcepreference.CheckBoxPreference +import ca.gosyer.data.models.sourcepreference.EditTextPreference +import ca.gosyer.data.models.sourcepreference.ListPreference +import ca.gosyer.data.models.sourcepreference.SourcePreference +import ca.gosyer.data.models.sourcepreference.SwitchPreference +import ca.gosyer.data.models.sourcepreference.TwoStateProps +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import java.util.Formatter + +sealed class SourceSettingsView { + abstract val index: Int + abstract val title: String? + abstract val subtitle: String? + abstract val state: StateFlow + abstract fun updateState(value: R) + + abstract val props: T + + open val summary: String? + get() = subtitle?.let { withFormat(it, state.value) } + + abstract class TwoState( + props: TwoStateProps, + private val _state: MutableStateFlow = MutableStateFlow( + props.currentValue + ?: props.defaultValue + ?: false + ) + ) : SourceSettingsView() { + override val state: StateFlow = _state.asStateFlow() + override fun updateState(value: Boolean) { + _state.value = value + } + } + + data class CheckBox internal constructor( + override val index: Int, + override val title: String?, + override val subtitle: String?, + override val props: TwoStateProps + ) : TwoState(props) { + internal constructor(index: Int, preference: CheckBoxPreference) : this( + index, + preference.props.title, + preference.props.summary, + preference.props + ) + } + + data class Switch internal constructor( + override val index: Int, + override val title: String?, + override val subtitle: String?, + override val props: TwoStateProps + ) : TwoState(props) { + internal constructor(index: Int, preference: SwitchPreference) : this( + index, + preference.props.title, + preference.props.summary, + preference.props + ) + } + + data class List internal constructor( + override val index: Int, + override val title: String?, + override val subtitle: String?, + override val props: ListPreference.ListProps + ) : SourceSettingsView>() { + private val _state = MutableStateFlow( + (props.currentValue ?: props.defaultValue ?: "0") to props.entries[props.entryValues.indexOf(props.currentValue ?: props.defaultValue ?: "0")] + ) + override val state: StateFlow> = _state.asStateFlow() + override fun updateState(value: Pair) { + _state.value = value + } + internal constructor(index: Int, preference: ListPreference) : this( + index, + preference.props.title, + preference.props.summary, + preference.props + ) + + override val summary: String? + get() = subtitle?.let { withFormat(it, state.value.second) } + + fun getOptions() = props.entryValues.mapIndexed { index, s -> + s to props.entries[index] + } + + fun setValue(value: String) { + updateState(value to props.entries[props.entryValues.indexOf(value)]) + } + } + + data class EditText internal constructor( + override val index: Int, + override val title: String?, + override val subtitle: String?, + val dialogTitle: String?, + val dialogMessage: String?, + override val props: EditTextPreference.EditTextProps + ) : SourceSettingsView() { + private val _state = MutableStateFlow(props.currentValue ?: props.defaultValue.orEmpty()) + override val state: StateFlow = _state.asStateFlow() + override fun updateState(value: String) { + _state.value = value + } + internal constructor(index: Int, preference: EditTextPreference) : this( + index, + preference.props.title, + preference.props.summary, + preference.props.dialogTitle, + preference.props.dialogMessage, + preference.props + ) + } +} + +fun withFormat(text: String, value: Any?): String { + return Formatter().format(text, value) + .let { formatter -> + formatter.toString() + .also { formatter.close() } + } +} + +fun SourceSettingsView(index: Int, preference: SourcePreference): SourceSettingsView<*, *> { + return when (preference) { + is CheckBoxPreference -> SourceSettingsView.CheckBox(index, preference) + is SwitchPreference -> SourceSettingsView.Switch(index, preference) + is ListPreference -> SourceSettingsView.List(index, preference) + is EditTextPreference -> SourceSettingsView.EditText(index, preference) + } +}