From e9ae5e415ea7b1938c7ff9c5e718e5ddacfd2426 Mon Sep 17 00:00:00 2001 From: AlexJDowson <43926762+AlexJDowson@users.noreply.github.com> Date: Sun, 14 Sep 2025 11:59:07 +0100 Subject: [PATCH 1/2] feat: add support for custom cup sizes --- .../hydro/ui/CupCarouselSelection.kt | 96 +++++++++++++++++++ .../hydro/ui/base/HydrationCarousel.kt | 4 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/at/florianschuster/hydro/ui/CupCarouselSelection.kt b/app/src/main/java/at/florianschuster/hydro/ui/CupCarouselSelection.kt index f1b3fac..937d99d 100644 --- a/app/src/main/java/at/florianschuster/hydro/ui/CupCarouselSelection.kt +++ b/app/src/main/java/at/florianschuster/hydro/ui/CupCarouselSelection.kt @@ -1,24 +1,34 @@ package at.florianschuster.hydro.ui +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import at.florianschuster.hydro.AppAction import at.florianschuster.hydro.AppState import at.florianschuster.hydro.model.Cup +import at.florianschuster.hydro.model.Milliliters +import at.florianschuster.hydro.model.icon import at.florianschuster.hydro.ui.base.HydrationCarousel @Composable @@ -61,6 +71,51 @@ fun CupCarouselSelection( } else { dispatch(AppAction.SetSelectedCups(state.selectedCups + cup)) } + }, + trailingContent = { + var showCustomDialog by remember { mutableStateOf(false) } + + OutlinedCard( + modifier = Modifier.clickable { showCustomDialog = true }, + colors = CardDefaults.outlinedCardColors( + containerColor = MaterialTheme.colorScheme.surface + ), + border = CardDefaults.outlinedCardBorder(false) + ) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + modifier = Modifier.size(28.dp), + painter = Milliliters(500).icon(), + contentDescription = null + ) + Text("Custom…", style = MaterialTheme.typography.labelLarge) + } + } + + + if (showCustomDialog) { + CustomMlDialog( + initial = 500, + onConfirm = { ml -> + showCustomDialog = false + val bounded = ml.coerceIn(50, 5000) + if (state.selectedCups.any { it.milliliters == Milliliters(bounded) }) return@CustomMlDialog + if (state.selectedCups.size >= 3) { + showCanOnlySelectThreeAlert = true + } else { + dispatch( + AppAction.SetSelectedCups( + state.selectedCups + Cup(milliliters = Milliliters(bounded)) + ) + ) + } + }, + onDismiss = { showCustomDialog = false } + ) + } } ) if (showCanOnlySelectThreeAlert) { @@ -78,3 +133,44 @@ fun CupCarouselSelection( } } } +@Composable +private fun CustomMlDialog( + initial: Int, + onConfirm: (Int) -> Unit, + onDismiss: () -> Unit, +) { + var text by remember { mutableStateOf(initial.toString()) } + val parsed = text.filter(Char::isDigit).toIntOrNull() + val valid = parsed != null && parsed in 50..5000 + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text("Custom size") }, + text = { + Column { + androidx.compose.material3.OutlinedTextField( + value = text, + onValueChange = { s -> text = s.filter(Char::isDigit).take(5) }, + label = { Text("Millilitres") }, + singleLine = true, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number + ), + suffix = { Text("ml") }, + isError = !valid + ) + Spacer(Modifier.height(4.dp)) + if (!valid) { + Text("Enter a value between 50 and 5000 ml", + color = MaterialTheme.colorScheme.error) + } + } + }, + confirmButton = { + Button(enabled = valid, onClick = { onConfirm(parsed!!) }) { Text("OK") } + }, + dismissButton = { + androidx.compose.material3.TextButton(onClick = onDismiss) { Text("Cancel") } + } + ) +} diff --git a/app/src/main/java/at/florianschuster/hydro/ui/base/HydrationCarousel.kt b/app/src/main/java/at/florianschuster/hydro/ui/base/HydrationCarousel.kt index fbf2d1a..87d5d5b 100644 --- a/app/src/main/java/at/florianschuster/hydro/ui/base/HydrationCarousel.kt +++ b/app/src/main/java/at/florianschuster/hydro/ui/base/HydrationCarousel.kt @@ -32,7 +32,8 @@ fun HydrationCarousel( liquidUnit: LiquidUnit, selected: List = emptyList(), onClick: (index: Int, Milliliters) -> Unit = { _, _ -> }, - contentBelowItem: @Composable (index: Int) -> Unit = {} + contentBelowItem: @Composable (index: Int) -> Unit = {}, + trailingContent: @Composable () -> Unit = {} ) { LazyRow( modifier = modifier, @@ -49,6 +50,7 @@ fun HydrationCarousel( contentBelowItem = contentBelowItem ) } + item { trailingContent() } } } From 94ed49611b4e05c9c522902a83aa0d7e46201b8f Mon Sep 17 00:00:00 2001 From: AlexJDowson <43926762+AlexJDowson@users.noreply.github.com> Date: Sun, 14 Sep 2025 15:47:15 +0100 Subject: [PATCH 2/2] fix: make custom cup size validation respect unit (ml vs fl oz) --- .../hydro/ui/CupCarouselSelection.kt | 64 ++++++++++++------- 1 file changed, 41 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/at/florianschuster/hydro/ui/CupCarouselSelection.kt b/app/src/main/java/at/florianschuster/hydro/ui/CupCarouselSelection.kt index 937d99d..ac7e192 100644 --- a/app/src/main/java/at/florianschuster/hydro/ui/CupCarouselSelection.kt +++ b/app/src/main/java/at/florianschuster/hydro/ui/CupCarouselSelection.kt @@ -30,6 +30,7 @@ import at.florianschuster.hydro.model.Cup import at.florianschuster.hydro.model.Milliliters import at.florianschuster.hydro.model.icon import at.florianschuster.hydro.ui.base.HydrationCarousel +import at.florianschuster.hydro.model.LiquidUnit @Composable fun CupCarouselSelection( @@ -95,14 +96,13 @@ fun CupCarouselSelection( } } - if (showCustomDialog) { - CustomMlDialog( - initial = 500, + CustomSizeDialog( + liquidUnit = state.liquidUnit, onConfirm = { ml -> showCustomDialog = false val bounded = ml.coerceIn(50, 5000) - if (state.selectedCups.any { it.milliliters == Milliliters(bounded) }) return@CustomMlDialog + if (state.selectedCups.any { it.milliliters == Milliliters(bounded) }) return@CustomSizeDialog if (state.selectedCups.size >= 3) { showCanOnlySelectThreeAlert = true } else { @@ -134,14 +134,28 @@ fun CupCarouselSelection( } } @Composable -private fun CustomMlDialog( - initial: Int, - onConfirm: (Int) -> Unit, +private fun CustomSizeDialog( + liquidUnit: LiquidUnit, + onConfirm: (Int /* ml */) -> Unit, onDismiss: () -> Unit, ) { - var text by remember { mutableStateOf(initial.toString()) } - val parsed = text.filter(Char::isDigit).toIntOrNull() - val valid = parsed != null && parsed in 50..5000 + val (unitLabel, toMlFactor, minMl) = when (liquidUnit) { + LiquidUnit.Milliliter -> Triple("ml", 1.0, 50) + LiquidUnit.USFluidOunce -> Triple("fl oz (US)", 29.5735, 1) + LiquidUnit.UKFluidOunce -> Triple("fl oz (UK)", 28.4131, 1) + } + val maxMl = 5000 + val minDisplay = (minMl / toMlFactor).coerceAtLeast(1.0).toInt() + val maxDisplay = (maxMl / toMlFactor).toInt() + + var text by remember { mutableStateOf("") } + val sanitized = remember(text) { + text.replace(',', '.') + .filterIndexed { i, c -> c.isDigit() || (c == '.' && !text.take(i).contains('.')) } + } + val entered = sanitized.toDoubleOrNull() + val ml = entered?.let { (it * toMlFactor).toInt() } + val valid = ml != null && ml in minMl..maxMl AlertDialog( onDismissRequest = onDismiss, @@ -150,27 +164,31 @@ private fun CustomMlDialog( Column { androidx.compose.material3.OutlinedTextField( value = text, - onValueChange = { s -> text = s.filter(Char::isDigit).take(5) }, - label = { Text("Millilitres") }, + onValueChange = { text = it }, + label = { Text("Amount") }, + placeholder = { Text(if (liquidUnit == LiquidUnit.Milliliter) "500" else "16") }, singleLine = true, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Number - ), - suffix = { Text("ml") }, - isError = !valid + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + suffix = { Text(unitLabel) }, + isError = !valid && text.isNotEmpty() ) Spacer(Modifier.height(4.dp)) - if (!valid) { - Text("Enter a value between 50 and 5000 ml", - color = MaterialTheme.colorScheme.error) - } + Text( + if (valid && ml != null) "≈ $ml ml" + else "Enter a value between $minDisplay–$maxDisplay $unitLabel", + color = if (valid || text.isEmpty()) + MaterialTheme.colorScheme.onSurfaceVariant + else + MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall + ) } }, confirmButton = { - Button(enabled = valid, onClick = { onConfirm(parsed!!) }) { Text("OK") } + Button(enabled = valid, onClick = { onConfirm(ml!!) }) { Text("OK") } }, dismissButton = { androidx.compose.material3.TextButton(onClick = onDismiss) { Text("Cancel") } } ) -} +} \ No newline at end of file