mirror of
https://github.com/Suwayomi/TachideskJUI.git
synced 2025-12-10 06:42:05 +01:00
Implement source configuration
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
@@ -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<String?>
|
||||
}
|
||||
@@ -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<String>,
|
||||
val entryValues: List<String>
|
||||
) : Props<String?>
|
||||
}
|
||||
@@ -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<T> {
|
||||
val key: String
|
||||
val title: String?
|
||||
val summary: String?
|
||||
val currentValue: T
|
||||
val defaultValue: T
|
||||
val defaultValueType: String
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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<Boolean?>
|
||||
@@ -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<List<SourcePreference>>(
|
||||
serverUrl + getSourceSettingsQuery(sourceId)
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getSourceSettings(source: Source) = getSourceSettings(source.id)
|
||||
|
||||
suspend fun setSourceSetting(sourceId: Long, sourcePreference: SourcePreferenceChange) = withIOContext {
|
||||
client.post<HttpResponse>(
|
||||
serverUrl + updateSourceSettingQuery(sourceId)
|
||||
) {
|
||||
contentType(ContentType.Application.Json)
|
||||
body = sourcePreference
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setSourceSetting(sourceId: Long, position: Int, value: Any) = setSourceSetting(
|
||||
sourceId,
|
||||
SourcePreferenceChange(position, value.toString())
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -214,7 +214,7 @@ fun <Key> ChoicePreference(
|
||||
)
|
||||
}
|
||||
|
||||
private fun <T> ChoiceDialog(
|
||||
fun <T> ChoiceDialog(
|
||||
items: List<Pair<T, String>>,
|
||||
selected: T?,
|
||||
onDismissRequest: () -> Unit = {},
|
||||
|
||||
@@ -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<Route>) {
|
||||
|
||||
@Composable
|
||||
fun MainWindow(rootBundle: Bundle, backStack: BackStack<Route>) {
|
||||
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<Route>) {
|
||||
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()
|
||||
|
||||
@@ -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<SourcesMenuViewModel> {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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<Route>? = null) {
|
||||
val vm = viewModel<SourceSettingsViewModel> {
|
||||
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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -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<List<SourceSettingsView<*, *>>>(emptyList())
|
||||
val sourceSettings = _sourceSettings.asStateFlow()
|
||||
|
||||
private val subscriptions: CopyOnWriteArrayList<Job> = 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<SourcePreference>.toView() = mapIndexed { index, sourcePreference ->
|
||||
SourceSettingsView(index, sourcePreference)
|
||||
}
|
||||
}
|
||||
@@ -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<T, R : Any?> {
|
||||
abstract val index: Int
|
||||
abstract val title: String?
|
||||
abstract val subtitle: String?
|
||||
abstract val state: StateFlow<R>
|
||||
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<Boolean> = MutableStateFlow(
|
||||
props.currentValue
|
||||
?: props.defaultValue
|
||||
?: false
|
||||
)
|
||||
) : SourceSettingsView<TwoStateProps, Boolean>() {
|
||||
override val state: StateFlow<Boolean> = _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<ListPreference.ListProps, Pair<String, String>>() {
|
||||
private val _state = MutableStateFlow(
|
||||
(props.currentValue ?: props.defaultValue ?: "0") to props.entries[props.entryValues.indexOf(props.currentValue ?: props.defaultValue ?: "0")]
|
||||
)
|
||||
override val state: StateFlow<Pair<String, String>> = _state.asStateFlow()
|
||||
override fun updateState(value: Pair<String, String>) {
|
||||
_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<EditTextPreference.EditTextProps, String>() {
|
||||
private val _state = MutableStateFlow(props.currentValue ?: props.defaultValue.orEmpty())
|
||||
override val state: StateFlow<String> = _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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user