Properly implement Preference

This commit is contained in:
tarsin committed 2023-06-04 23:39:33 +08:00
1 parent 140fac4f3b
commit d44192079d
5 files changed
+301 -45

No files matched your search

@@ -2,18 +2,14 @@ package com.hippo.ehviewer.ui.settings
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -30,18 +26,16 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import com.hippo.ehviewer.ui.login.LocalNavController
import com.hippo.ehviewer.ui.openBrowser
import com.hippo.ehviewer.ui.settings.PreferenceTokens.PreferenceMinHeight
import com.hippo.ehviewer.ui.settings.PreferenceTokens.PreferencePadding
import com.hippo.ehviewer.ui.settings.PreferenceTokens.PreferenceTextPadding
import com.jamal.composeprefs3.ui.prefs.SwitchPref
import com.jamal.composeprefs3.ui.prefs.TextPref
import kotlin.reflect.KMutableProperty0
@Composable
fun PreferenceHeader(icon: Painter, @StringRes title: Int, childRouteName: String) {
val navController = LocalNavController.current
Row(
modifier = Modifier.clickable { navController.navigate(childRouteName) }.fillMaxWidth().height(
PreferenceTokens.PreferenceHeaderHeight,
),
modifier = Modifier.clickable { navController.navigate(childRouteName) }.fillMaxWidth().height(PreferenceTokens.PreferenceHeaderHeight),
verticalAlignment = Alignment.CenterVertically,
) {
Spacer(modifier = Modifier.size(PreferenceTokens.PreferenceIconPadding))
@@ -56,38 +50,14 @@ fun PreferenceHeader(icon: ImageVector, @StringRes title: Int, childRouteName: S
@Composable
fun Preference(title: String, summary: String? = null, onClick: () -> Unit = {}) {
Column(
modifier = Modifier.defaultMinSize(minHeight = PreferenceMinHeight).fillMaxWidth().clickable(onClick = onClick).padding(PreferencePadding),
verticalArrangement = Arrangement.Center,
) {
Text(text = title, style = MaterialTheme.typography.bodyLarge)
summary?.let {
Spacer(modifier = Modifier.size(PreferenceTextPadding))
Text(text = summary, style = MaterialTheme.typography.bodyMedium)
}
}
TextPref(title = title, summary = summary, onClick = onClick)
}
@Composable
fun SwitchPreference(title: String, summary: String?, value: KMutableProperty0<Boolean>) {
var v by remember { mutableStateOf(value.get()) }
fun mutate() = value.set((!value.get()).also { v = it })
Row(
modifier = Modifier.fillMaxWidth().defaultMinSize(minHeight = PreferenceMinHeight).padding(PreferencePadding).clickable { mutate() },
verticalAlignment = Alignment.CenterVertically,
) {
Column(
modifier = Modifier.weight(1F),
verticalArrangement = Arrangement.Center,
) {
Text(text = title, style = MaterialTheme.typography.bodyLarge)
summary?.let {
Spacer(modifier = Modifier.size(PreferenceTextPadding))
Text(text = summary, style = MaterialTheme.typography.bodyMedium)
}
}
Switch(checked = v, onCheckedChange = null)
}
SwitchPref(checked = v, onMutate = ::mutate, title = title, summary = summary)
}
@Composable
@@ -98,14 +68,5 @@ fun UrlPreference(title: String, url: String) {
@Composable
fun HtmlPreference(title: String, summary: AnnotatedString? = null, onClick: () -> Unit = {}) {
Column(
modifier = Modifier.defaultMinSize(minHeight = PreferenceMinHeight).fillMaxWidth().clickable(onClick = onClick).padding(PreferencePadding),
verticalArrangement = Arrangement.Center,
) {
Text(text = title, style = MaterialTheme.typography.bodyLarge)
summary?.let {
Spacer(modifier = Modifier.size(PreferenceTextPadding))
Text(text = summary, style = MaterialTheme.typography.bodyMedium)
}
}
TextPref(title = title, summary = summary, onClick = onClick)
}
@@ -0,0 +1,151 @@
package com.jamal.composeprefs3.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
@Composable
fun PrefsListItem(
modifier: Modifier = Modifier,
enabled: Boolean = true,
darkenOnDisable: Boolean = true,
icon: @Composable (() -> Unit)? = null,
secondaryText: @Composable (() -> Unit)? = null,
trailing: @Composable (() -> Unit)? = null,
textColor: Color = MaterialTheme.colorScheme.onBackground,
minimalHeight: Boolean = false,
text: @Composable () -> Unit,
) {
val typography = MaterialTheme.typography
val styledText = applyTextStyle(
typography.titleMedium,
textColor,
when {
enabled || !darkenOnDisable -> AlphaHigh
else -> AlphaDisabled
},
text,
)!!
val styledSecondaryText = applyTextStyle(
typography.bodyMedium,
textColor,
when {
enabled || !darkenOnDisable -> AlphaMedium
else -> AlphaDisabled
},
secondaryText,
)
val styledTrailing = applyTextStyle(
typography.bodySmall,
textColor,
when {
enabled || !darkenOnDisable -> AlphaHigh
else -> AlphaDisabled
},
trailing,
)
AnyLine.CustomListItem(
modifier = modifier,
minimalHeight,
icon,
styledSecondaryText,
styledTrailing,
styledText,
)
}
private object AnyLine {
private val MinHeight = 48.dp
private val MinHeightSmaller = 32.dp
private val IconMinPaddedWidth = 40.dp
private val ContentPadding = 16.dp
private val VerticalPadding = 12.dp
private val SingleLinePadding = 4.dp // used when no secondary text is supplied
@Composable
fun CustomListItem(
modifier: Modifier = Modifier,
minimalHeight: Boolean = true,
icon: @Composable (() -> Unit)? = null,
secondaryText: (@Composable (() -> Unit))? = null,
trailing: @Composable (() -> Unit)? = null,
text: @Composable (() -> Unit),
) {
Row(
modifier
.heightIn(min = if (minimalHeight) MinHeightSmaller else MinHeight)
.padding(
start = ContentPadding,
end = ContentPadding,
top = when {
secondaryText == null && !minimalHeight -> SingleLinePadding
else -> VerticalPadding
},
bottom = when {
minimalHeight -> 0.dp
secondaryText == null -> SingleLinePadding
else -> VerticalPadding
},
)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
if (icon != null) {
val minSize = IconMinPaddedWidth
Box(
Modifier.sizeIn(minWidth = minSize, minHeight = minSize),
contentAlignment = Alignment.CenterStart,
) { icon() }
}
Column(
verticalArrangement = Arrangement.Center,
modifier = Modifier.weight(1f),
) {
text()
secondaryText?.invoke()
}
trailing?.invoke()
}
}
}
private fun applyTextStyle(
textStyle: TextStyle,
textColor: Color,
contentAlpha: Float,
content: @Composable (() -> Unit)?,
): @Composable (() -> Unit)? {
if (content == null) return null
return {
val newTextStyle = textStyle.copy(
color = textColor.copy(alpha = contentAlpha),
)
CompositionLocalProvider(
LocalTextStyle provides newTextStyle,
) {
content()
}
}
}
private const val AlphaHigh = 1.0f
private const val AlphaMedium = 0.74f
private const val AlphaDisabled = 0.38f
@@ -0,0 +1,16 @@
package com.jamal.composeprefs3.ui
import androidx.compose.runtime.Composable
import java.math.BigDecimal
import java.math.RoundingMode
fun Any?.ifNotNullThen(content: @Composable () -> Unit): (@Composable () -> Unit)? =
if (this != null) content else null
fun roundToDP(value: Double, places: Int): Double {
return BigDecimal(value).setScale(places, RoundingMode.HALF_EVEN).toDouble()
}
fun roundToDP(value: Float, places: Int): Float {
return BigDecimal(value.toDouble()).setScale(places, RoundingMode.HALF_EVEN).toFloat()
}
@@ -0,0 +1,49 @@
package com.jamal.composeprefs3.ui.prefs
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
/**
* Simple preference with a trailing [Switch]
*
* @param key Key used to identify this Pref in the DataStore
* @param title Main text which describes the Pref
* @param modifier Modifier applied to the Text aspect of this Pref
* @param summary Used to give some more information about what this Pref is for
* @param defaultChecked If the switch should be checked by default. Only used if a value for this [key] doesn't already exist in the DataStore
* @param onCheckedChange Will be called with the new state when the state changes
* @param textColor Text colour of the [title] and [summary]
* @param enabled If false, this Pref cannot be checked/unchecked
* @param leadingIcon Icon which is positioned at the start of the Pref
*/
@Composable
fun SwitchPref(
checked: Boolean,
onMutate: () -> Unit,
title: String,
modifier: Modifier = Modifier,
summary: String? = null,
textColor: Color = MaterialTheme.colorScheme.onBackground,
enabled: Boolean = true,
leadingIcon: @Composable (() -> Unit)? = null,
) {
TextPref(
title = title,
modifier = modifier,
textColor = textColor,
summary = summary,
darkenOnDisable = true,
leadingIcon = leadingIcon,
enabled = enabled,
onClick = onMutate,
) {
Switch(
checked = checked,
enabled = enabled,
onCheckedChange = { onMutate() },
)
}
}
@@ -0,0 +1,79 @@
package com.jamal.composeprefs3.ui.prefs
import androidx.compose.foundation.clickable
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString
import com.jamal.composeprefs3.ui.PrefsListItem
import com.jamal.composeprefs3.ui.ifNotNullThen
/**
* Simple Text with title and summary.
* Used to show some information to the user and is the basis of all other preferences.
*
* @param title Main text which describes the Pref
* @param modifier Modifier applied to the Text aspect of this Pref
* @param summary Used to give some more information about what this Pref is for
* @param darkenOnDisable If true, text colors have lower opacity when the Pref is not [enabled]
* @param minimalHeight If true, the height of the pref is reduced, such that other content can be more easily included in the pref.
* Mostly for internal use with custom Prefs
* @param onClick Callback for when this Pref is clicked. Will not be called if Pref is not [enabled]
* @param textColor Text colour of the [title] and [summary]
* @param enabled If false, this Pref cannot be checked/unchecked
* @param leadingIcon Icon which is positioned at the start of the Pref
* @param trailingContent Composable content which is positioned at the end of the Pref
*/
@Composable
fun TextPref(
title: String,
modifier: Modifier = Modifier,
summary: String? = null,
darkenOnDisable: Boolean = false,
minimalHeight: Boolean = false,
onClick: () -> Unit = {},
textColor: Color = MaterialTheme.colorScheme.onBackground,
enabled: Boolean = true,
leadingIcon: @Composable (() -> Unit)? = null,
trailingContent: @Composable (() -> Unit)? = null,
) {
PrefsListItem(
text = { Text(title) },
modifier = if (enabled) modifier.clickable { onClick() } else modifier,
enabled = enabled,
darkenOnDisable = darkenOnDisable,
textColor = textColor,
minimalHeight = minimalHeight,
icon = leadingIcon,
secondaryText = summary.ifNotNullThen { Text(summary!!) },
trailing = trailingContent,
)
}
@Composable
fun TextPref(
title: String,
modifier: Modifier = Modifier,
summary: AnnotatedString? = null,
darkenOnDisable: Boolean = false,
minimalHeight: Boolean = false,
onClick: () -> Unit = {},
textColor: Color = MaterialTheme.colorScheme.onBackground,
enabled: Boolean = true,
leadingIcon: @Composable (() -> Unit)? = null,
trailingContent: @Composable (() -> Unit)? = null,
) {
PrefsListItem(
text = { Text(title) },
modifier = if (enabled) modifier.clickable { onClick() } else modifier,
enabled = enabled,
darkenOnDisable = darkenOnDisable,
textColor = textColor,
minimalHeight = minimalHeight,
icon = leadingIcon,
secondaryText = summary.ifNotNullThen { Text(summary!!) },
trailing = trailingContent,
)
}