From 09b02dc974e8d0d41c6f8f278151d5e4fd5c4ac1 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski <mail@justjanne.de> Date: Tue, 10 Jun 2025 02:25:02 +0200 Subject: [PATCH] refactor: purchase route --- .../de/chaosdorf/meteroid/MainActivity.kt | 1 + .../de/chaosdorf/meteroid/theme/Color.kt | 3 +- .../chaosdorf/meteroid/theme/ThemeGradient.kt | 4 +- .../de/chaosdorf/meteroid/ui/PurchaseRoute.kt | 261 ------------------ .../meteroid/ui/purchase/PurchaseDrinkTile.kt | 189 +++++++++++++ .../ui/purchase/PurchaseFilterChip.kt | 64 +++++ .../meteroid/ui/purchase/PurchaseFilterRow.kt | 57 ++++ .../meteroid/ui/purchase/PurchaseRoute.kt | 79 ++++++ .../meteroid/ui/purchase/WrappedBanner.kt | 83 ++++++ 9 files changed, 478 insertions(+), 263 deletions(-) delete mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/PurchaseRoute.kt create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseDrinkTile.kt create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseFilterChip.kt create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseFilterRow.kt create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseRoute.kt create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/WrappedBanner.kt diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt index 1a581ff..a4ea473 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt @@ -46,6 +46,7 @@ import de.chaosdorf.meteroid.theme.MeteroidTheme import de.chaosdorf.meteroid.ui.* import de.chaosdorf.meteroid.ui.common.BottomBar import de.chaosdorf.meteroid.ui.common.TopBar +import de.chaosdorf.meteroid.ui.purchase.PurchaseRoute import de.chaosdorf.meteroid.viewmodel.NavigationViewModel import de.chaosdorf.meteroid.viewmodel.* import kotlinx.coroutines.flow.update diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/theme/Color.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/Color.kt index 6fdf1e2..d9ca400 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/theme/Color.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/Color.kt @@ -26,6 +26,7 @@ package de.chaosdorf.meteroid.theme import androidx.compose.material3.ColorScheme import androidx.compose.material3.contentColorFor +import androidx.compose.runtime.Stable import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.graphics.luminance @@ -92,7 +93,7 @@ val md_theme_dark_surfaceTint = Color(0xFFAFC6FF) val md_theme_dark_outlineVariant = Color(0xFF44474F) val md_theme_dark_scrim = Color(0xFF000000) - +@Stable val ColorScheme.secondaryGradient get() = ThemeGradient( listOf( diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/theme/ThemeGradient.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/ThemeGradient.kt index 9e61bf4..234f8ba 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/theme/ThemeGradient.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/ThemeGradient.kt @@ -24,12 +24,14 @@ package de.chaosdorf.meteroid.theme +import androidx.compose.runtime.Immutable import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.TileMode -class ThemeGradient(val colors: List<Color>) { +@Immutable +data class ThemeGradient(val colors: List<Color>) { fun linearGradient( start: Offset = Offset.Zero, end: Offset = Offset.Infinite, diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/PurchaseRoute.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/PurchaseRoute.kt deleted file mode 100644 index b42942f..0000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/PurchaseRoute.kt +++ /dev/null @@ -1,261 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2013-2025 Chaosdorf e.V. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package de.chaosdorf.meteroid.ui - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.GridItemSpan -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import coil3.compose.rememberAsyncImagePainter -import de.chaosdorf.meteroid.model.Drink -import de.chaosdorf.meteroid.theme.onPrimaryContainerTinted -import de.chaosdorf.meteroid.theme.secondaryGradient -import de.chaosdorf.meteroid.ui.common.PriceBadge -import de.chaosdorf.meteroid.viewmodel.Navigator -import de.chaosdorf.meteroid.viewmodel.PurchaseViewModel -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.update -import kotlinx.datetime.Clock -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toLocalDateTime -import java.math.BigDecimal -import java.util.* - -@Composable -fun PurchaseRoute( - viewModel: PurchaseViewModel, - navigator: Navigator, - contentPadding: PaddingValues, -) { - val drinks by viewModel.drinks.collectAsState() - val filters by viewModel.filters.collectAsState() - - LazyVerticalGrid( - GridCells.Adaptive(104.dp), - contentPadding = contentPadding, - modifier = Modifier.padding(horizontal = 8.dp), - ) { - item("filter", span = { GridItemSpan(maxLineSpan) }) { - FlowRow( - modifier = Modifier.padding(horizontal = 12.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - PurchaseFilterChip( - label = "Active", - selected = filters.contains(PurchaseViewModel.Filter.Active), - onClick = { viewModel.toggleFilter(PurchaseViewModel.Filter.Active) } - ) - PurchaseFilterChip( - label = "Coffeine Free", - selected = filters.contains(PurchaseViewModel.Filter.CaffeineFree), - onClick = { viewModel.toggleFilter(PurchaseViewModel.Filter.CaffeineFree) } - ) - } - } - - item("wrapped", span = { GridItemSpan(maxLineSpan) }) { - Surface( - color = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainerTinted, - shape = RoundedCornerShape(8.dp), - modifier = Modifier.padding(vertical = 8.dp, horizontal = 12.dp) - ) { - Row(horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 8.dp)) { - Spacer(Modifier.width(12.dp)) - Column(verticalArrangement = Arrangement.Center) { - val now = Clock.System.now().toLocalDateTime(TimeZone.UTC) - Text( - "Your ${now.year} Wrapped is here", - style = MaterialTheme.typography.bodyLarge, - ) - Text( - "Jump into your year in beverages", - style = MaterialTheme.typography.bodyMedium, - ) - } - Spacer(Modifier.width(8.dp)) - Button( - onClick = { - navigator.backStack.update { - it.plus(MeteroidRoute.Wrapped(viewModel.serverId, viewModel.userId)) - } - }, - ) { - Text("Let's go") - } - Spacer(Modifier.width(4.dp)) - } - } - } - - items( - drinks, - key = { "drink-${it.serverId}-${it.drinkId}" }, - ) { drink -> - PurchaseDrinkTile(drink) { item, count -> - viewModel.purchase(item, count, onBack = {}) - } - } - } -} - -@Composable -fun PurchaseFilterChip( - label: String, - selected: Boolean, - onClick: () -> Unit, -) { - FilterChip( - label = { - Text(label, style = MaterialTheme.typography.labelLarge) - }, - selected = selected, - leadingIcon = { - if (selected) { - Icon( - Icons.Default.Check, - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - } - }, - onClick = onClick, - colors = FilterChipDefaults.filterChipColors( - selectedContainerColor = MaterialTheme.colorScheme.secondaryContainer - ) - ) -} - -@Composable -fun PurchaseDrinkTile( - item: Drink, - modifier: Modifier = Modifier, - onPurchase: (Drink, Int) -> Unit = { _, _ -> } -) { - var purchaseCount by remember { mutableIntStateOf(0) } - val pendingPurchases = purchaseCount != 0 - - LaunchedEffect(purchaseCount) { - delay(2000L) - onPurchase(item, purchaseCount) - purchaseCount = 0 - } - - val thumbPainter = rememberAsyncImagePainter( - item.logoUrl - ) - val drinkPainter = rememberAsyncImagePainter( - item.originalLogoUrl, - error = thumbPainter - ) - - Column( - modifier = modifier - .height(IntrinsicSize.Max) - .alpha(if (item.active) 1.0f else 0.67f) - .clip(RoundedCornerShape(8.dp)) - .clickable { purchaseCount += 1 } - .padding(8.dp) - ) { - Box( - Modifier - .aspectRatio(1.0f) - .background(MaterialTheme.colorScheme.secondaryGradient.verticalGradient(), CircleShape), - contentAlignment = Alignment.Center - ) { - Image( - drinkPainter, - contentDescription = null, - contentScale = ContentScale.Fit, - modifier = Modifier - .alpha(if (pendingPurchases) 0.0f else 1.0f) - .clip(CircleShape) - ) - PriceBadge( - item.price, - modifier = Modifier - .alpha(if (pendingPurchases) 0.0f else 1.0f) - .align(Alignment.BottomEnd) - .paddingFromBaseline(bottom = 12.dp) - ) - Text( - "×$purchaseCount", - fontSize = 36.sp, - fontWeight = FontWeight.Light, - color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.67f), - textAlign = TextAlign.Center, - modifier = Modifier.alpha(if (pendingPurchases) 1.0f else 0.0f) - ) - } - Spacer(Modifier.height(4.dp)) - Text( - item.name, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp), - textAlign = TextAlign.Center, - fontWeight = FontWeight.SemiBold, - style = MaterialTheme.typography.labelLarge, - ) - Spacer(Modifier.height(4.dp)) - Row(modifier = Modifier.align(Alignment.CenterHorizontally)) { - val unitPrice = - if (item.volume <= BigDecimal.ZERO) null - else item.price / item.volume - - Text( - if (unitPrice == null) String.format(Locale.getDefault(), "%.02fl", item.volume) - else String.format(Locale.getDefault(), "%.02fl · %.02f€/l", item.volume, item.price / item.volume), - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp), - textAlign = TextAlign.Center, - fontWeight = FontWeight.SemiBold, - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) - ) - } - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseDrinkTile.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseDrinkTile.kt new file mode 100644 index 0000000..00adcbd --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseDrinkTile.kt @@ -0,0 +1,189 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2025 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.meteroid.ui.purchase + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.paddingFromBaseline +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil3.compose.rememberAsyncImagePainter +import de.chaosdorf.meteroid.model.Drink +import de.chaosdorf.meteroid.theme.secondaryGradient +import de.chaosdorf.meteroid.ui.common.PriceBadge +import kotlinx.coroutines.delay +import java.math.BigDecimal +import java.util.Locale + +@Composable +fun PurchaseDrinkTile( + item: Drink, + modifier: Modifier = Modifier, + onPurchase: (Drink, Int) -> Unit = { _, _ -> } +) { + var purchaseCount by remember { mutableIntStateOf(0) } + val pendingPurchases by remember { + derivedStateOf { + purchaseCount != 0 + } + } + + LaunchedEffect(purchaseCount) { + delay(2000L) + onPurchase(item, purchaseCount) + purchaseCount = 0 + } + + val thumbPainter = rememberAsyncImagePainter( + item.logoUrl + ) + val drinkPainter = rememberAsyncImagePainter( + item.originalLogoUrl, + error = thumbPainter + ) + + val contentAlpha = remember(item) { + if (item.active) 1.0f else 0.67f + } + + Column( + modifier = modifier + .height(IntrinsicSize.Max) + .clip(RoundedCornerShape(8.dp)) + .clickable { purchaseCount += 1 } + .padding(8.dp) + ) { + Box( + Modifier.Companion + .aspectRatio(1.0f) + .background( + brush = MaterialTheme.colorScheme.secondaryGradient.verticalGradient(), + alpha = contentAlpha, + shape = CircleShape, + ), + contentAlignment = Alignment.Companion.Center + ) { + Box( + modifier = Modifier.Companion + .graphicsLayer { + clip = true + alpha = if (pendingPurchases) 0.0f else contentAlpha + } + ) { + Image( + drinkPainter, + contentDescription = null, + contentScale = ContentScale.Companion.Fit, + modifier = Modifier.Companion + .clip(CircleShape) + ) + PriceBadge( + item.price, + modifier = Modifier.Companion + .align(Alignment.Companion.BottomEnd) + .paddingFromBaseline(bottom = 12.dp) + ) + } + Text( + "×$purchaseCount", + fontSize = 36.sp, + fontWeight = FontWeight.Companion.Light, + color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.67f), + textAlign = TextAlign.Companion.Center, + modifier = Modifier.Companion + .graphicsLayer { + clip = true + alpha = if (pendingPurchases) contentAlpha else 0.0f + } + ) + } + Spacer(Modifier.Companion.height(4.dp)) + Text( + item.name, + modifier = Modifier.Companion + .fillMaxWidth() + .padding(horizontal = 8.dp), + textAlign = TextAlign.Companion.Center, + fontWeight = FontWeight.Companion.SemiBold, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = contentAlpha) + ) + Spacer(Modifier.Companion.height(4.dp)) + Row(modifier = Modifier.Companion.align(Alignment.Companion.CenterHorizontally)) { + val label = remember(item) { + val unitPrice = + if (item.volume <= BigDecimal.ZERO) null + else item.price / item.volume + + if (unitPrice == null) String.Companion.format(Locale.getDefault(), "%.02fl", item.volume) + else String.Companion.format( + Locale.getDefault(), + "%.02fl · %.02f€/l", + item.volume, + item.price / item.volume + ) + } + + Text( + label, + modifier = Modifier.Companion + .fillMaxWidth() + .padding(horizontal = 8.dp), + textAlign = TextAlign.Companion.Center, + fontWeight = FontWeight.Companion.SemiBold, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f * contentAlpha) + ) + } + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseFilterChip.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseFilterChip.kt new file mode 100644 index 0000000..609d776 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseFilterChip.kt @@ -0,0 +1,64 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2025 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.meteroid.ui.purchase + +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun PurchaseFilterChip( + label: String, + selected: Boolean, + onClick: () -> Unit, +) { + FilterChip( + label = { + Text(label, style = MaterialTheme.typography.labelLarge) + }, + selected = selected, + leadingIcon = { + if (selected) { + Icon( + Icons.Default.Check, + contentDescription = null, + modifier = Modifier.Companion.size(18.dp) + ) + } + }, + onClick = onClick, + colors = FilterChipDefaults.filterChipColors( + selectedContainerColor = MaterialTheme.colorScheme.secondaryContainer + ) + ) +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseFilterRow.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseFilterRow.kt new file mode 100644 index 0000000..309572a --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseFilterRow.kt @@ -0,0 +1,57 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2025 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.meteroid.ui.purchase + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import de.chaosdorf.meteroid.viewmodel.PurchaseViewModel + +@Preview(showBackground = true) +@Composable +fun PurchaseFilterRow( + filters: Set<PurchaseViewModel.Filter> = emptySet(), + toggleFilter: (filter: PurchaseViewModel.Filter) -> Unit = {}, +) { + FlowRow( + modifier = Modifier.Companion.padding(horizontal = 12.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + PurchaseFilterChip( + label = "Active", + selected = filters.contains(PurchaseViewModel.Filter.Active), + onClick = { toggleFilter(PurchaseViewModel.Filter.Active) } + ) + PurchaseFilterChip( + label = "Coffeine Free", + selected = filters.contains(PurchaseViewModel.Filter.CaffeineFree), + onClick = { toggleFilter(PurchaseViewModel.Filter.CaffeineFree) } + ) + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseRoute.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseRoute.kt new file mode 100644 index 0000000..be3cb02 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseRoute.kt @@ -0,0 +1,79 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2025 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.meteroid.ui.purchase + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import de.chaosdorf.meteroid.ui.MeteroidRoute +import de.chaosdorf.meteroid.viewmodel.Navigator +import de.chaosdorf.meteroid.viewmodel.PurchaseViewModel +import kotlinx.coroutines.flow.update + +@Composable +fun PurchaseRoute( + viewModel: PurchaseViewModel, + navigator: Navigator, + contentPadding: PaddingValues, +) { + val drinks by viewModel.drinks.collectAsState() + val filters by viewModel.filters.collectAsState() + + LazyVerticalGrid( + GridCells.Adaptive(104.dp), + contentPadding = contentPadding, + modifier = Modifier.padding(horizontal = 8.dp), + ) { + item("filter", span = { GridItemSpan(maxLineSpan) }) { + PurchaseFilterRow(filters, viewModel::toggleFilter) + } + + item("wrapped", span = { GridItemSpan(maxLineSpan) }) { + WrappedBanner(onClick = { + navigator.backStack.update { + it.plus(MeteroidRoute.Wrapped(viewModel.serverId, viewModel.userId)) + } + }) + } + + items( + drinks, + key = { "drink-${it.serverId}-${it.drinkId}" }, + ) { drink -> + PurchaseDrinkTile(drink) { item, count -> + viewModel.purchase(item, count, onBack = {}) + } + } + } +} + diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/WrappedBanner.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/WrappedBanner.kt new file mode 100644 index 0000000..b4784d2 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/WrappedBanner.kt @@ -0,0 +1,83 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2025 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.meteroid.ui.purchase + +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.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import de.chaosdorf.meteroid.theme.onPrimaryContainerTinted +import kotlinx.datetime.Clock +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime + +@Preview(showBackground = true) +@Composable +fun WrappedBanner( + onClick: () -> Unit = {} +) { + Surface( + color = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainerTinted, + shape = RoundedCornerShape(8.dp), + modifier = Modifier.Companion.padding(vertical = 8.dp, horizontal = 12.dp) + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Companion.CenterVertically, + modifier = Modifier.Companion.padding(vertical = 8.dp) + ) { + Spacer(Modifier.Companion.width(12.dp)) + Column(verticalArrangement = Arrangement.Center) { + val now = Clock.System.now().toLocalDateTime(TimeZone.Companion.UTC) + Text( + "Your ${now.year} Wrapped is here", + style = MaterialTheme.typography.bodyLarge, + ) + Text( + "Jump into your year in beverages", + style = MaterialTheme.typography.bodyMedium, + ) + } + Spacer(Modifier.Companion.width(8.dp)) + Button(onClick = onClick) { + Text("Let's go") + } + Spacer(Modifier.Companion.width(4.dp)) + } + } +} -- GitLab