Properly implement Preference
This commit is contained in:
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,
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user