diff --git a/app/src/main/java/com/hippo/ehviewer/ui/settings/PreferenceWidgets.kt b/app/src/main/java/com/hippo/ehviewer/ui/settings/PreferenceWidgets.kt index 192f11d4d..e26c1a677 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/settings/PreferenceWidgets.kt +++ b/app/src/main/java/com/hippo/ehviewer/ui/settings/PreferenceWidgets.kt @@ -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) { 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) } diff --git a/app/src/main/java/com/jamal/composeprefs3/ui/PrefsListItem.kt b/app/src/main/java/com/jamal/composeprefs3/ui/PrefsListItem.kt new file mode 100644 index 000000000..1bbdc2edc --- /dev/null +++ b/app/src/main/java/com/jamal/composeprefs3/ui/PrefsListItem.kt @@ -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 diff --git a/app/src/main/java/com/jamal/composeprefs3/ui/PrefsUtils.kt b/app/src/main/java/com/jamal/composeprefs3/ui/PrefsUtils.kt new file mode 100644 index 000000000..b3a254ef0 --- /dev/null +++ b/app/src/main/java/com/jamal/composeprefs3/ui/PrefsUtils.kt @@ -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() +} diff --git a/app/src/main/java/com/jamal/composeprefs3/ui/prefs/SwitchPref.kt b/app/src/main/java/com/jamal/composeprefs3/ui/prefs/SwitchPref.kt new file mode 100644 index 000000000..de60ff74d --- /dev/null +++ b/app/src/main/java/com/jamal/composeprefs3/ui/prefs/SwitchPref.kt @@ -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() }, + ) + } +} diff --git a/app/src/main/java/com/jamal/composeprefs3/ui/prefs/TextPref.kt b/app/src/main/java/com/jamal/composeprefs3/ui/prefs/TextPref.kt new file mode 100644 index 000000000..7186ad608 --- /dev/null +++ b/app/src/main/java/com/jamal/composeprefs3/ui/prefs/TextPref.kt @@ -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, + ) +}