Implement Multi-Select List Preference for Mangadex

This commit is contained in:
Syer10
2021-11-28 16:06:27 -05:00
parent da197f89dc
commit 18138b7299
7 changed files with 161 additions and 14 deletions

View File

@@ -3,9 +3,9 @@ import org.gradle.api.JavaVersion
object Config { object Config {
const val tachideskVersion = "v0.5.4" const val tachideskVersion = "v0.5.4"
// Match this to the Tachidesk-Server commit count // Match this to the Tachidesk-Server commit count
const val serverCode = 1043 const val serverCode = 1045
const val preview = true const val preview = true
const val previewCommit = "5e47b7ae6b37931ce3a8eee33cafb9475d7a77bb" const val previewCommit = "2478aa77cd4a71b0ae7c895fce0358ad7c30614b"
val jvmTarget = JavaVersion.VERSION_15 val jvmTarget = JavaVersion.VERSION_15
} }

View File

@@ -0,0 +1,28 @@
/*
* 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("MultiSelectListPreference")
data class MultiSelectListPreference(override val props: MultiSelectListProps) : SourcePreference() {
@Serializable
data class MultiSelectListProps(
override val key: String,
override val title: String,
override val summary: String?,
override val currentValue: List<String>?,
override val defaultValue: List<String>?,
override val defaultValueType: String,
val dialogTitle: String?,
val dialogMessage: String?,
val entries: List<String>,
val entryValues: List<String>
) : Props<List<String>?>
}

View File

@@ -7,6 +7,16 @@
package ca.gosyer.data.models.sourcepreference package ca.gosyer.data.models.sourcepreference
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@Serializable @Serializable
data class SourcePreferenceChange(val position: Int, val value: String) data class SourcePreferenceChange(val position: Int, val value: String) {
constructor(position: Int, value: Any) : this(
position,
if (value is List<*>) {
@Suppress("UNCHECKED_CAST")
Json.encodeToString(value as List<String>)
} else value.toString()
)
}

View File

@@ -150,6 +150,6 @@ class SourceInteractionHandler @Inject constructor(
suspend fun setSourceSetting(sourceId: Long, position: Int, value: Any) = setSourceSetting( suspend fun setSourceSetting(sourceId: Long, position: Int, value: Any) = setSourceSetting(
sourceId, sourceId,
SourcePreferenceChange(position, value.toString()) SourcePreferenceChange(position, value)
) )
} }

View File

@@ -30,9 +30,11 @@ import androidx.compose.foundation.layout.BoxWithConstraintsScope
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeight import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
@@ -40,6 +42,7 @@ import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Checkbox
import androidx.compose.material.ContentAlpha import androidx.compose.material.ContentAlpha
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.LocalContentColor import androidx.compose.material.LocalContentColor
@@ -72,6 +75,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ca.gosyer.ui.base.WindowDialog import ca.gosyer.ui.base.WindowDialog
import ca.gosyer.ui.base.components.ColorPickerDialog import ca.gosyer.ui.base.components.ColorPickerDialog
import kotlinx.coroutines.flow.MutableStateFlow
@Composable @Composable
fun PreferenceRow( fun PreferenceRow(
@@ -263,6 +267,48 @@ fun <T> ChoiceDialog(
} }
} }
fun <T> MultiSelectDialog(
items: List<Pair<T, String>>,
selected: List<T>?,
onCloseRequest: () -> Unit = {},
onFinished: (List<T>) -> Unit,
title: String,
) {
val checkedFlow = MutableStateFlow(selected.orEmpty())
WindowDialog(
onCloseRequest = onCloseRequest,
title = title,
onPositiveButton = {
onFinished(checkedFlow.value)
}
) {
val checked by checkedFlow.collectAsState()
LazyColumn(Modifier.fillMaxSize()) {
items(items) { (value, text) ->
Row(
modifier = Modifier.requiredHeight(48.dp).fillMaxWidth().clickable(
onClick = {
if (value in checked) {
checkedFlow.value -= value
} else {
checkedFlow.value += value
}
}
),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = value in checked,
onCheckedChange = null,
)
Text(text = text, modifier = Modifier.padding(start = 24.dp))
}
}
item { Spacer(Modifier.height(80.dp)) }
}
}
}
@Composable @Composable
fun ColorPreference( fun ColorPreference(
preference: PreferenceMutableStateFlow<Color>, preference: PreferenceMutableStateFlow<Color>,

View File

@@ -28,12 +28,14 @@ import ca.gosyer.ui.base.components.LocalMenuController
import ca.gosyer.ui.base.components.MenuController import ca.gosyer.ui.base.components.MenuController
import ca.gosyer.ui.base.components.Toolbar import ca.gosyer.ui.base.components.Toolbar
import ca.gosyer.ui.base.prefs.ChoiceDialog import ca.gosyer.ui.base.prefs.ChoiceDialog
import ca.gosyer.ui.base.prefs.MultiSelectDialog
import ca.gosyer.ui.base.prefs.PreferenceRow import ca.gosyer.ui.base.prefs.PreferenceRow
import ca.gosyer.ui.base.resources.stringResource import ca.gosyer.ui.base.resources.stringResource
import ca.gosyer.ui.base.vm.viewModel import ca.gosyer.ui.base.vm.viewModel
import ca.gosyer.ui.sources.settings.model.SourceSettingsView.CheckBox 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.EditText
import ca.gosyer.ui.sources.settings.model.SourceSettingsView.List import ca.gosyer.ui.sources.settings.model.SourceSettingsView.List
import ca.gosyer.ui.sources.settings.model.SourceSettingsView.MultiSelect
import ca.gosyer.ui.sources.settings.model.SourceSettingsView.Switch import ca.gosyer.ui.sources.settings.model.SourceSettingsView.Switch
import ca.gosyer.ui.sources.settings.model.SourceSettingsView.TwoState import ca.gosyer.ui.sources.settings.model.SourceSettingsView.TwoState
import ca.gosyer.util.compose.ThemedWindow import ca.gosyer.util.compose.ThemedWindow
@@ -71,6 +73,9 @@ fun SourceSettingsMenu(sourceId: Long, menuController: MenuController? = LocalMe
is EditText -> { is EditText -> {
EditTextPreference(it) EditTextPreference(it)
} }
is MultiSelect -> {
MultiSelectPreference(it)
}
else -> Unit else -> Unit
} }
} }
@@ -120,9 +125,35 @@ private fun ListPreference(list: List) {
onClick = { onClick = {
ChoiceDialog( ChoiceDialog(
list.getOptions(), list.getOptions(),
state.first, state,
onSelected = list::setValue, onSelected = list::updateState,
title = "Select choice" title = title
)
}
)
}
@Composable
private fun MultiSelectPreference(multiSelect: MultiSelect) {
val state by multiSelect.state.collectAsState()
val title = remember(state) { multiSelect.title ?: multiSelect.summary ?: "No title" }
val subtitle = remember(state) {
if (multiSelect.title == null) {
null
} else {
multiSelect.summary
}
}
val dialogTitle = remember(state) { multiSelect.props.dialogTitle ?: multiSelect.title ?: multiSelect.summary ?: "No title" }
PreferenceRow(
title,
subtitle = subtitle,
onClick = {
MultiSelectDialog(
multiSelect.getOptions(),
state,
onFinished = multiSelect::updateState,
title = dialogTitle
) )
} }
) )

View File

@@ -9,6 +9,7 @@ package ca.gosyer.ui.sources.settings.model
import ca.gosyer.data.models.sourcepreference.CheckBoxPreference import ca.gosyer.data.models.sourcepreference.CheckBoxPreference
import ca.gosyer.data.models.sourcepreference.EditTextPreference import ca.gosyer.data.models.sourcepreference.EditTextPreference
import ca.gosyer.data.models.sourcepreference.ListPreference import ca.gosyer.data.models.sourcepreference.ListPreference
import ca.gosyer.data.models.sourcepreference.MultiSelectListPreference
import ca.gosyer.data.models.sourcepreference.SourcePreference import ca.gosyer.data.models.sourcepreference.SourcePreference
import ca.gosyer.data.models.sourcepreference.SwitchPreference import ca.gosyer.data.models.sourcepreference.SwitchPreference
import ca.gosyer.data.models.sourcepreference.TwoStateProps import ca.gosyer.data.models.sourcepreference.TwoStateProps
@@ -16,6 +17,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import java.util.Formatter import java.util.Formatter
import kotlin.collections.List as KtList
sealed class SourceSettingsView<T, R : Any?> { sealed class SourceSettingsView<T, R : Any?> {
abstract val index: Int abstract val index: Int
@@ -76,12 +78,12 @@ sealed class SourceSettingsView<T, R : Any?> {
override val title: String?, override val title: String?,
override val subtitle: String?, override val subtitle: String?,
override val props: ListPreference.ListProps override val props: ListPreference.ListProps
) : SourceSettingsView<ListPreference.ListProps, Pair<String, String>>() { ) : SourceSettingsView<ListPreference.ListProps, String>() {
private val _state = MutableStateFlow( private val _state = MutableStateFlow(
(props.currentValue ?: props.defaultValue ?: "0") to props.entries[props.entryValues.indexOf(props.currentValue ?: props.defaultValue ?: "0")] props.currentValue ?: props.defaultValue ?: "0"
) )
override val state: StateFlow<Pair<String, String>> = _state.asStateFlow() override val state: StateFlow<String> = _state.asStateFlow()
override fun updateState(value: Pair<String, String>) { override fun updateState(value: String) {
_state.value = value _state.value = value
} }
internal constructor(index: Int, preference: ListPreference) : this( internal constructor(index: Int, preference: ListPreference) : this(
@@ -92,14 +94,43 @@ sealed class SourceSettingsView<T, R : Any?> {
) )
override val summary: String? override val summary: String?
get() = subtitle?.let { withFormat(it, state.value.second) } get() = subtitle?.let { withFormat(it, props.entries[props.entryValues.indexOf(state.value)]) }
fun getOptions() = props.entryValues.mapIndexed { index, s ->
s to props.entries[index]
}
}
data class MultiSelect internal constructor(
override val index: Int,
override val title: String?,
override val subtitle: String?,
override val props: MultiSelectListPreference.MultiSelectListProps
) : SourceSettingsView<MultiSelectListPreference.MultiSelectListProps, KtList<String>?>() {
private val _state = MutableStateFlow(
props.currentValue ?: props.defaultValue
)
override val state: StateFlow<KtList<String>?> = _state.asStateFlow()
override fun updateState(value: KtList<String>?) {
_state.value = value
}
internal constructor(index: Int, preference: MultiSelectListPreference) : this(
index,
preference.props.title,
preference.props.summary,
preference.props
)
fun getOptions() = props.entryValues.mapIndexed { index, s -> fun getOptions() = props.entryValues.mapIndexed { index, s ->
s to props.entries[index] s to props.entries[index]
} }
fun setValue(value: String) { fun toggleOption(key: String) {
updateState(value to props.entries[props.entryValues.indexOf(value)]) if (key in state.value.orEmpty()) {
updateState(state.value.orEmpty() - key)
} else {
updateState(state.value.orEmpty() + key)
}
} }
} }
@@ -138,6 +169,7 @@ fun SourceSettingsView(index: Int, preference: SourcePreference): SourceSettings
is CheckBoxPreference -> SourceSettingsView.CheckBox(index, preference) is CheckBoxPreference -> SourceSettingsView.CheckBox(index, preference)
is SwitchPreference -> SourceSettingsView.Switch(index, preference) is SwitchPreference -> SourceSettingsView.Switch(index, preference)
is ListPreference -> SourceSettingsView.List(index, preference) is ListPreference -> SourceSettingsView.List(index, preference)
is MultiSelectListPreference -> SourceSettingsView.MultiSelect(index, preference)
is EditTextPreference -> SourceSettingsView.EditText(index, preference) is EditTextPreference -> SourceSettingsView.EditText(index, preference)
} }
} }