diff --git a/api/src/main/kotlin/de/chaosdorf/mete/model/DrinkModel.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/DrinkModel.kt index 5019a19205bd5416866be4e26a76769886d1167c..f63a8e9187bf0503aadd044601418bd9143a3b68 100644 --- a/api/src/main/kotlin/de/chaosdorf/mete/model/DrinkModel.kt +++ b/api/src/main/kotlin/de/chaosdorf/mete/model/DrinkModel.kt @@ -33,5 +33,5 @@ interface DrinkModel { val volume: BigDecimal val caffeine: Int? val price: BigDecimal - val logoUrl: String + val logoUrl: String? } diff --git a/api/src/main/kotlin/de/chaosdorf/mete/model/UserModel.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/UserModel.kt index 6d46048b5dce582564c39cf671f0470adbf8de16..aa51899d8941c94fdb4f926d77bcc76b8f55af93 100644 --- a/api/src/main/kotlin/de/chaosdorf/mete/model/UserModel.kt +++ b/api/src/main/kotlin/de/chaosdorf/mete/model/UserModel.kt @@ -30,7 +30,7 @@ interface UserModel { val userId: UserId val active: Boolean val name: String - val email: String + val email: String? val balance: BigDecimal val audit: Boolean val redirect: Boolean diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/DrinkModelV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/DrinkModelV1.kt index 30755a7165cd70c49500a8ba095f09acd4d9025f..d6027fb5fb06a8e72ccc8e08051c0acabe35a285 100644 --- a/api/src/main/kotlin/de/chaosdorf/mete/v1/DrinkModelV1.kt +++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/DrinkModelV1.kt @@ -49,17 +49,17 @@ internal data class DrinkModelV1( @Serializable(with = BigDecimalSerializer::class) override val price: BigDecimal, @SerialName("logo_file_name") - val logoFileName: String, + val logoFileName: String?, @SerialName("created_at") val createdAt: Instant, @SerialName("updated_at") val updatedAt: Instant, @SerialName("logo_content_type") - val logoContentType: String, + val logoContentType: String?, @SerialName("logo_file_size") - val logoFileSize: Long, + val logoFileSize: Long?, @SerialName("logo_updated_at") - val logoUpdatedAt: Instant, + val logoUpdatedAt: Instant?, @SerialName("logo_url") - override val logoUrl: String, + override val logoUrl: String?, ) : DrinkModel diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/UserModelV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/UserModelV1.kt index 75d6edccca00a13fd028679787d40cfcd4bdb27d..9f6d420e253cfbaa94507df221d7e2a0993fc848 100644 --- a/api/src/main/kotlin/de/chaosdorf/mete/v1/UserModelV1.kt +++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/UserModelV1.kt @@ -39,7 +39,7 @@ internal data class UserModelV1( @SerialName("name") override val name: String, @SerialName("email") - override val email: String, + override val email: String?, @SerialName("balance") @Serializable(with = BigDecimalSerializer::class) override val balance: BigDecimal, diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt index a43e4cacfbe397ee8d31dbbb00124f66e9fa41eb..8dadcc4530733e3152e9307c031e47f9ced33d4f 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt @@ -43,7 +43,6 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.navigation import androidx.navigation.compose.rememberNavController -import de.chaosdorf.meteroid.ui.transactions.TransactionListScreen import de.chaosdorf.meteroid.ui.drinks.DrinkListScreen import de.chaosdorf.meteroid.ui.drinks.DrinkListViewModel import de.chaosdorf.meteroid.ui.money.MoneyListScreen @@ -51,8 +50,11 @@ import de.chaosdorf.meteroid.ui.money.MoneyListViewModel import de.chaosdorf.meteroid.ui.navigation.Routes import de.chaosdorf.meteroid.ui.servers.AddServerScreen import de.chaosdorf.meteroid.ui.servers.ServerListScreen +import de.chaosdorf.meteroid.ui.transactions.TransactionListScreen import de.chaosdorf.meteroid.ui.transactions.TransactionViewModel import de.chaosdorf.meteroid.ui.users.UserListScreen +import de.chaosdorf.meteroid.ui.wrapped.WrappedScreen +import de.chaosdorf.meteroid.ui.wrapped.WrappedViewModel import kotlinx.coroutines.launch @Composable @@ -158,6 +160,10 @@ fun AppRouter(viewModel: AppViewModel = viewModel()) { val transactionViewModel = hiltViewModel<TransactionViewModel>() TransactionListScreen(transactionViewModel, navController::navigate) } + composable(Routes.Home.Wrapped) { _ -> + val wrappedViewModel = hiltViewModel<WrappedViewModel>() + WrappedScreen(wrappedViewModel, navController::navigate) + } } } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListScreen.kt index 6604ee78fe352e078c380428d6f8cd801d340fad..73332041878de24c291c4343ed23f66cdfa85e4f 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListScreen.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListScreen.kt @@ -49,6 +49,10 @@ import de.chaosdorf.meteroid.ui.navigation.HomeSections import de.chaosdorf.meteroid.ui.navigation.MeteroidBottomBar import de.chaosdorf.meteroid.ui.navigation.MeteroidTopBar import de.chaosdorf.meteroid.ui.navigation.Routes +import kotlinx.datetime.Clock +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import java.time.Month @OptIn(ExperimentalLayoutApi::class) @Composable @@ -86,6 +90,8 @@ fun DrinkListScreen( MeteroidBottomBar( currentRoute = HomeSections.PURCHASE, historyEnabled = account?.user?.audit == true, + wrappedEnabled = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) + .month.let { it == Month.NOVEMBER || it == Month.DECEMBER }, navigateTo = onNavigate ) }, diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkTile.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkTile.kt index 7c141b9503ddef01638d7deeedb333a2707852e5..ab9b07c052f48a4d853fb4d05e6d68e4def8b967 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkTile.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkTile.kt @@ -68,7 +68,7 @@ fun DrinkTile( item.logoUrl ) val drinkPainter = rememberAsyncImagePainter( - item.logoUrl.replace("/thumb/", "/original/"), + item.originalLogoUrl, error = thumbPainter ) diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListScreen.kt index d553e02dfc60f6fd7dc0b8983647fb9dfe73c956..c6e9dfc4e96a5a74af47b94152e9d7951a888a17 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListScreen.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListScreen.kt @@ -47,6 +47,10 @@ import de.chaosdorf.meteroid.ui.navigation.HomeSections import de.chaosdorf.meteroid.ui.navigation.MeteroidBottomBar import de.chaosdorf.meteroid.ui.navigation.MeteroidTopBar import de.chaosdorf.meteroid.ui.navigation.Routes +import kotlinx.datetime.Clock +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import java.time.Month @Composable fun MoneyListScreen( @@ -81,6 +85,8 @@ fun MoneyListScreen( MeteroidBottomBar( currentRoute = HomeSections.DEPOSIT, historyEnabled = account?.user?.audit == true, + wrappedEnabled = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) + .month.let { it == Month.NOVEMBER || it == Month.DECEMBER }, navigateTo = onNavigate ) }, diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/HomeSections.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/HomeSections.kt index c0879743211d72fca287405f5dbd5243784856c1..08f21ccad6a3ed5ba7644442bbb5c4599c4e9936 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/HomeSections.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/HomeSections.kt @@ -25,18 +25,25 @@ package de.chaosdorf.meteroid.ui.navigation import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Celebration +import androidx.compose.material.icons.outlined.History +import androidx.compose.material.icons.outlined.LocalAtm +import androidx.compose.material.icons.twotone.Celebration import androidx.compose.material.icons.twotone.History import androidx.compose.material.icons.twotone.LocalAtm import androidx.compose.ui.graphics.vector.ImageVector import de.chaosdorf.meteroid.icons.MeteroidIcons +import de.chaosdorf.meteroid.icons.outlined.WaterFull import de.chaosdorf.meteroid.icons.twotone.WaterFull enum class HomeSections( override val title: String, override val icon: ImageVector, + override val iconActive: ImageVector, override val route: String ) : MeteroidNavSection { - PURCHASE("Drinks", MeteroidIcons.TwoTone.WaterFull, Routes.Home.Purchase), - DEPOSIT("Money", Icons.TwoTone.LocalAtm, Routes.Home.Deposit), - HISTORY("History", Icons.TwoTone.History, Routes.Home.History); + PURCHASE("Drinks", MeteroidIcons.Outlined.WaterFull, MeteroidIcons.TwoTone.WaterFull, Routes.Home.Purchase), + DEPOSIT("Money", Icons.Outlined.LocalAtm, Icons.TwoTone.LocalAtm, Routes.Home.Deposit), + HISTORY("History", Icons.Outlined.History, Icons.TwoTone.History, Routes.Home.History), + WRAPPED("Wrapped", Icons.Outlined.Celebration, Icons.TwoTone.Celebration, Routes.Home.Wrapped); } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidBottomBar.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidBottomBar.kt index 18238c82faec1b9d23fe0113c896108a5b8f8155..760314f3b1ddb5c5684d1c73e526fcb667739afb 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidBottomBar.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidBottomBar.kt @@ -37,25 +37,33 @@ fun <T : MeteroidNavSection> MeteroidBottomBar( currentRoute: T, navigateTo: (String, NavOptions) -> Unit, historyEnabled: Boolean, + wrappedEnabled: Boolean, modifier: Modifier = Modifier ) { NavigationBar { for (route in HomeSections.entries) { - NavigationBarItem( - icon = { Icon(route.icon, contentDescription = route.title) }, - label = { Text(route.title) }, - selected = route == currentRoute, - onClick = { - navigateTo( - route.route, - NavOptions.Builder() - .setPopUpTo(Routes.Home.Root, true) - .build() - ) - }, - modifier = modifier, - enabled = route != HomeSections.HISTORY || historyEnabled - ) + if (wrappedEnabled || route != HomeSections.WRAPPED) { + NavigationBarItem( + icon = { + Icon( + if (route == currentRoute) route.iconActive else route.icon, + contentDescription = route.title + ) + }, + label = { Text(route.title) }, + selected = route == currentRoute, + onClick = { + navigateTo( + route.route, + NavOptions.Builder() + .setPopUpTo(Routes.Home.Root, true) + .build() + ) + }, + modifier = modifier, + enabled = route != HomeSections.HISTORY || historyEnabled + ) + } } } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidNavSection.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidNavSection.kt index f89144fcbf37f8c13f75dc011de5c305e8bf85f4..d56b347cb8b721e072530335fbc2ec6773f331ee 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidNavSection.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidNavSection.kt @@ -29,5 +29,6 @@ import androidx.compose.ui.graphics.vector.ImageVector interface MeteroidNavSection { val title: String val icon: ImageVector + val iconActive: ImageVector val route: String } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/Routes.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/Routes.kt index f9938c462ca77b3fdf2ee291a48d3394cbeb68fe..a475733f3df74271439259ce2c0132709ba32cf6 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/Routes.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/Routes.kt @@ -44,5 +44,6 @@ object Routes { const val Deposit = "$Root/deposit" const val Purchase = "$Root/purchase" const val History = "$Root/history" + const val Wrapped = "$Root/wrapped" } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListItem.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListItem.kt index 5b5c6682244098d71a4154223e682a1b4b8ed50b..7376c6d1ca56052a378411dd2570ad4823c27199 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListItem.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListItem.kt @@ -89,7 +89,7 @@ fun TransactionListItem( drink.logoUrl ) val originalPainter = rememberAsyncImagePainter( - drink.logoUrl.replace("/thumb/", "/original/"), + drink.originalLogoUrl, error = thumbPainter ) diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListScreen.kt index bc1eefc70d93302aca1e1e7942d61f382fca37ee..ee0a63947b72a16fd9b80b457bb967ec0af5a41a 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListScreen.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListScreen.kt @@ -40,6 +40,10 @@ import androidx.navigation.NavOptions import de.chaosdorf.meteroid.ui.navigation.HomeSections import de.chaosdorf.meteroid.ui.navigation.MeteroidBottomBar import de.chaosdorf.meteroid.ui.navigation.MeteroidTopBar +import kotlinx.datetime.Clock +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import java.time.Month @Composable fun TransactionListScreen( @@ -67,6 +71,8 @@ fun TransactionListScreen( MeteroidBottomBar( currentRoute = HomeSections.HISTORY, historyEnabled = account?.user?.audit == true, + wrappedEnabled = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) + .month.let { it == Month.NOVEMBER || it == Month.DECEMBER }, navigateTo = onNavigate ) }, diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedScreen.kt new file mode 100644 index 0000000000000000000000000000000000000000..b36cf87a8460da6361bb2ef208f04f0e154bfedc --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedScreen.kt @@ -0,0 +1,188 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 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.wrapped + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ListItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.navigation.NavOptions +import coil.compose.rememberAsyncImagePainter +import de.chaosdorf.meteroid.R +import de.chaosdorf.meteroid.ui.navigation.HomeSections +import de.chaosdorf.meteroid.ui.navigation.MeteroidBottomBar +import de.chaosdorf.meteroid.ui.navigation.MeteroidTopBar +import kotlinx.datetime.Clock +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import java.time.Month +import java.time.format.TextStyle + +@Composable +fun WrappedScreen( + viewModel: WrappedViewModel, + onNavigate: (String, NavOptions) -> Unit +) { + val account by viewModel.account.collectAsState() + val slides by viewModel.slides.collectAsState() + val snackbarHostState = remember { SnackbarHostState() } + + LaunchedEffect(account) { + val offline = viewModel.checkOffline(account?.server) + snackbarHostState.currentSnackbarData?.dismiss() + if (offline) { + snackbarHostState.showSnackbar( + message = "Unable to connect to server", + duration = SnackbarDuration.Indefinite + ) + } + } + + Scaffold( + topBar = { MeteroidTopBar(account, onNavigate, viewModel::togglePin) }, + bottomBar = { + MeteroidBottomBar( + currentRoute = HomeSections.WRAPPED, + historyEnabled = account?.user?.audit == true, + wrappedEnabled = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) + .month.let { it == Month.NOVEMBER || it == Month.DECEMBER }, + navigateTo = onNavigate + ) + }, + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) + } + ) { paddingValues: PaddingValues -> + LazyColumn(contentPadding = paddingValues) { + items(slides) { slide -> + when (slide) { + is WrappedSlide.MostBoughtDrink -> + ListItem( + headlineContent = { + Text("Your favorite drink is ${slide.drink.name}") + }, + supportingContent = { + Text("At least you enjoyed it ${slide.count} times this year.") + }, + leadingContent = { + val thumbPainter = rememberAsyncImagePainter( + slide.drink.logoUrl + ) + val drinkPainter = rememberAsyncImagePainter( + slide.drink.originalLogoUrl, + error = thumbPainter + ) + + Image( + drinkPainter, + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier.size(72.dp) + ) + } + ) + + is WrappedSlide.Caffeine -> + ListItem( + headlineContent = { + Text("You consumed ${slide.total} mg of caffeine this year.") + }, + supportingContent = { + slide.wouldKill?.let { animal -> + Text("This could kill a medium-weight ${animal.name}. Glad you're still here.") + } + }, + leadingContent = { + val painter = painterResource( + when (slide.wouldKill) { + WrappedSlide.Caffeine.Animal.Squirrel -> R.drawable.wrapped_squirrel + WrappedSlide.Caffeine.Animal.Rat -> R.drawable.wrapped_rat + WrappedSlide.Caffeine.Animal.Cat -> R.drawable.wrapped_cat + WrappedSlide.Caffeine.Animal.Koala -> R.drawable.wrapped_koala + WrappedSlide.Caffeine.Animal.Lynx -> R.drawable.wrapped_lynx + WrappedSlide.Caffeine.Animal.Jaguar -> R.drawable.wrapped_jaguar + WrappedSlide.Caffeine.Animal.Reindeer -> R.drawable.wrapped_reindeer + WrappedSlide.Caffeine.Animal.Gorilla -> R.drawable.wrapped_gorilla + WrappedSlide.Caffeine.Animal.Lion -> R.drawable.wrapped_lion + WrappedSlide.Caffeine.Animal.Bear -> R.drawable.wrapped_bear + WrappedSlide.Caffeine.Animal.Moose -> R.drawable.wrapped_moose + else -> R.drawable.wrapped_coffee_beans + } + ) + + Image( + painter, + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier.size(72.dp) + ) + } + ) + + is WrappedSlide.MostActive -> + ListItem( + headlineContent = { + Text( + "You were most active on ${ + slide.weekday.getDisplayName( + TextStyle.FULL, + LocalConfiguration.current.locale + ) + }s at ${slide.hour} o'clock." + ) + }, + leadingContent = { + val painter = painterResource(R.drawable.wrapped_clock) + + Image( + painter, + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier.size(72.dp) + ) + } + ) + } + } + } + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedSlide.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedSlide.kt new file mode 100644 index 0000000000000000000000000000000000000000..4899c2ecda023e56684a4cdaa1d58fe2f90f8953 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedSlide.kt @@ -0,0 +1,123 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 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.wrapped + +import de.chaosdorf.mete.model.DrinkId +import de.chaosdorf.meteroid.model.Drink +import de.chaosdorf.meteroid.model.Transaction +import kotlinx.datetime.DayOfWeek +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime + +sealed class WrappedSlide { + interface Factory { + fun create( + transactions: List<Transaction>, + drinks: Map<DrinkId, Drink> + ): WrappedSlide? + } + + data class MostBoughtDrink( + val drink: Drink, + val count: Int, + ) : WrappedSlide() { + companion object : Factory { + override fun create( + transactions: List<Transaction>, + drinks: Map<DrinkId, Drink> + ): WrappedSlide? = transactions + .mapNotNull { drinks[it.drinkId] } + .groupingBy { it } + .eachCount() + .maxByOrNull { it.value } + ?.let { (mostBoughtDrink, count) -> + MostBoughtDrink(mostBoughtDrink, count) + } + } + } + + data class Caffeine( + val total: Double, + val wouldKill: Animal?, + ) : WrappedSlide() { + enum class Animal(val lethalDosage: Double) { + Hamster(0.25), + Squirrel(0.3), + Rat(0.4), + GuineaPig(0.9), + Lemur(2.5), + Cat(5.0), + Koala(9.0), + Coyote(13.0), + Lynx(23.0), + Capybara(55.0), + Jaguar(81.0), + Reindeer(101.0), + Gorilla(140.0), + Lion(175.0), + Bear(278.0), + Moose(368.0), + Bison(540.0) + } + + companion object : Factory { + override fun create( + transactions: List<Transaction>, + drinks: Map<DrinkId, Drink> + ): WrappedSlide = transactions + .mapNotNull { drinks[it.drinkId] } + .mapNotNull { drink -> drink.caffeine?.let { it * drink.volume.toDouble() * 10 } } + .sum() + .let { dosage -> + Caffeine( + dosage, + Animal.values() + .sortedBy(Animal::lethalDosage) + .firstOrNull { it.lethalDosage < dosage } + ) + } + } + } + + data class MostActive( + val weekday: DayOfWeek, + val hour: Int, + ) : WrappedSlide() { + companion object : Factory { + override fun create( + transactions: List<Transaction>, + drinks: Map<DrinkId, Drink> + ): WrappedSlide = transactions + .map { it.timestamp.toLocalDateTime(TimeZone.currentSystemDefault()) } + .groupingBy { Pair(it.dayOfWeek, it.hour) } + .eachCount() + .maxBy { it.value } + .key + .let { (dayOfWeek, hour) -> + MostActive(dayOfWeek, hour) + } + } + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..722226a4dfc8c7c0b7f2bfde6c43e20b096cee1c --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedViewModel.kt @@ -0,0 +1,117 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 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.wrapped + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import de.chaosdorf.mete.model.DrinkId +import de.chaosdorf.meteroid.model.AccountInfo +import de.chaosdorf.meteroid.model.Drink +import de.chaosdorf.meteroid.model.DrinkRepository +import de.chaosdorf.meteroid.model.Server +import de.chaosdorf.meteroid.model.TransactionRepository +import de.chaosdorf.meteroid.sync.AccountProvider +import de.chaosdorf.meteroid.sync.SyncManager +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.datetime.Clock +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.Month +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toInstant +import kotlinx.datetime.toLocalDateTime +import javax.inject.Inject + +@HiltViewModel +class WrappedViewModel @Inject constructor( + private val accountProvider: AccountProvider, + repository: TransactionRepository, + drinkRepository: DrinkRepository, + private val syncManager: SyncManager +) : ViewModel() { + val account: StateFlow<AccountInfo?> = accountProvider.account + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) + + val slides: StateFlow<List<WrappedSlide>> = accountProvider.account + .flatMapLatest { account -> + account?.let { (server, maybeUser) -> + maybeUser?.let { user -> + combine( + repository.getAllFlow(server.serverId, user.userId), + drinkRepository.getAllFlow(server.serverId) + ) { transactions, drinks -> + val drinkMap: Map<DrinkId, Drink> = drinks.associateBy(Drink::drinkId) + val factories = listOf( + WrappedSlide.MostBoughtDrink, + WrappedSlide.Caffeine, + WrappedSlide.MostActive + ) + val timeZone = TimeZone.currentSystemDefault() + val now = Clock.System.now().toLocalDateTime(timeZone) + val yearBegin = LocalDateTime( + year = now.year, + month = Month.JANUARY, + dayOfMonth = 1, + hour = 0, + minute = 0, + second = 0 + ).toInstant(timeZone) + val yearEnd = LocalDateTime( + year = now.year + 1, + month = Month.JANUARY, + dayOfMonth = 1, + hour = 0, + minute = 0, + second = 0 + ).toInstant(timeZone) + val thisYear = transactions.filter { + it.timestamp in yearBegin..yearEnd + } + factories.mapNotNull { it.create(thisYear, drinkMap) } + } + } + } ?: flowOf(emptyList()) + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) + + fun togglePin() { + account.value?.let { account -> + account.user?.let { user -> + viewModelScope.launch { + accountProvider.togglePin(account.server.serverId, user.userId) + } + } + } + } + + suspend fun checkOffline(server: Server?): Boolean = + if (server == null) true + else syncManager.checkOffline(server) +} diff --git a/app/src/main/res/drawable-nodpi/wrapped_bear.png b/app/src/main/res/drawable-nodpi/wrapped_bear.png new file mode 100644 index 0000000000000000000000000000000000000000..fd2eaa74f3bbcfac66295c78dd24cb14e2cb369f Binary files /dev/null and b/app/src/main/res/drawable-nodpi/wrapped_bear.png differ diff --git a/app/src/main/res/drawable-nodpi/wrapped_cat.png b/app/src/main/res/drawable-nodpi/wrapped_cat.png new file mode 100644 index 0000000000000000000000000000000000000000..dd61bfabbb9adf46ca3e9e101108af9b4c511016 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/wrapped_cat.png differ diff --git a/app/src/main/res/drawable-nodpi/wrapped_clock.png b/app/src/main/res/drawable-nodpi/wrapped_clock.png new file mode 100644 index 0000000000000000000000000000000000000000..665761506811fc156a35c72acdf21e6ab301cdb7 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/wrapped_clock.png differ diff --git a/app/src/main/res/drawable-nodpi/wrapped_coffee_beans.png b/app/src/main/res/drawable-nodpi/wrapped_coffee_beans.png new file mode 100644 index 0000000000000000000000000000000000000000..6851bc699bb711b9d8ece400561b4bf19e48d10b Binary files /dev/null and b/app/src/main/res/drawable-nodpi/wrapped_coffee_beans.png differ diff --git a/app/src/main/res/drawable-nodpi/wrapped_gorilla.png b/app/src/main/res/drawable-nodpi/wrapped_gorilla.png new file mode 100644 index 0000000000000000000000000000000000000000..36bd7eacdca5ab950962662d77025a929bcf6e13 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/wrapped_gorilla.png differ diff --git a/app/src/main/res/drawable-nodpi/wrapped_jaguar.png b/app/src/main/res/drawable-nodpi/wrapped_jaguar.png new file mode 100644 index 0000000000000000000000000000000000000000..13478112412447063fd3bccdaf17fcd04e03a5c5 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/wrapped_jaguar.png differ diff --git a/app/src/main/res/drawable-nodpi/wrapped_koala.png b/app/src/main/res/drawable-nodpi/wrapped_koala.png new file mode 100644 index 0000000000000000000000000000000000000000..caf71732522b5aa321780ace30e7a3bdd625e7be Binary files /dev/null and b/app/src/main/res/drawable-nodpi/wrapped_koala.png differ diff --git a/app/src/main/res/drawable-nodpi/wrapped_lion.png b/app/src/main/res/drawable-nodpi/wrapped_lion.png new file mode 100644 index 0000000000000000000000000000000000000000..b92bd4816e520119e75cec2c4bea807355fef877 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/wrapped_lion.png differ diff --git a/app/src/main/res/drawable-nodpi/wrapped_lynx.png b/app/src/main/res/drawable-nodpi/wrapped_lynx.png new file mode 100644 index 0000000000000000000000000000000000000000..a6e93ff24b72d058252f814f454b7fb8ca46077a Binary files /dev/null and b/app/src/main/res/drawable-nodpi/wrapped_lynx.png differ diff --git a/app/src/main/res/drawable-nodpi/wrapped_moose.png b/app/src/main/res/drawable-nodpi/wrapped_moose.png new file mode 100644 index 0000000000000000000000000000000000000000..e25310389cb1804e368d8a0c7e4b20c3f1f5cf24 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/wrapped_moose.png differ diff --git a/app/src/main/res/drawable-nodpi/wrapped_rat.png b/app/src/main/res/drawable-nodpi/wrapped_rat.png new file mode 100644 index 0000000000000000000000000000000000000000..7aaf3239adc21c7f346f16522dea64fd974970de Binary files /dev/null and b/app/src/main/res/drawable-nodpi/wrapped_rat.png differ diff --git a/app/src/main/res/drawable-nodpi/wrapped_reindeer.png b/app/src/main/res/drawable-nodpi/wrapped_reindeer.png new file mode 100644 index 0000000000000000000000000000000000000000..52e6d5976e43714c99794d94b58c6344eb4664d6 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/wrapped_reindeer.png differ diff --git a/app/src/main/res/drawable-nodpi/wrapped_squirrel.png b/app/src/main/res/drawable-nodpi/wrapped_squirrel.png new file mode 100644 index 0000000000000000000000000000000000000000..14a1b1d96a5aef442cd785498fef3701475532ea Binary files /dev/null and b/app/src/main/res/drawable-nodpi/wrapped_squirrel.png differ diff --git a/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json b/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json index 315971c57694c8ba40ca571ae15663f067952a65..92efcbbdb205fafdea4788b4f830b585852e13a6 100644 --- a/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json +++ b/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json @@ -2,11 +2,11 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "f794acceadd9ed28da1b218972b5e530", + "identityHash": "e26316f758271c58bc953e756fc16e7d", "entities": [ { "tableName": "Drink", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` INTEGER NOT NULL, `drinkId` INTEGER NOT NULL, `active` INTEGER NOT NULL, `name` TEXT NOT NULL, `volume` TEXT NOT NULL, `caffeine` INTEGER, `price` TEXT NOT NULL, `logoUrl` TEXT NOT NULL, PRIMARY KEY(`serverId`, `drinkId`), FOREIGN KEY(`serverId`) REFERENCES `Server`(`serverId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` INTEGER NOT NULL, `drinkId` INTEGER NOT NULL, `active` INTEGER NOT NULL, `name` TEXT NOT NULL, `volume` TEXT NOT NULL, `caffeine` INTEGER, `price` TEXT NOT NULL, `logoUrl` TEXT, PRIMARY KEY(`serverId`, `drinkId`), FOREIGN KEY(`serverId`) REFERENCES `Server`(`serverId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", "fields": [ { "fieldPath": "serverId", @@ -54,7 +54,7 @@ "fieldPath": "logoUrl", "columnName": "logoUrl", "affinity": "TEXT", - "notNull": true + "notNull": false } ], "primaryKey": { @@ -119,7 +119,7 @@ }, { "tableName": "User", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` INTEGER NOT NULL, `userId` INTEGER NOT NULL, `active` INTEGER NOT NULL, `name` TEXT NOT NULL, `email` TEXT NOT NULL, `balance` TEXT NOT NULL, `audit` INTEGER NOT NULL, `redirect` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `userId`), FOREIGN KEY(`serverId`) REFERENCES `Server`(`serverId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` INTEGER NOT NULL, `userId` INTEGER NOT NULL, `active` INTEGER NOT NULL, `name` TEXT NOT NULL, `email` TEXT, `balance` TEXT NOT NULL, `audit` INTEGER NOT NULL, `redirect` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `userId`), FOREIGN KEY(`serverId`) REFERENCES `Server`(`serverId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", "fields": [ { "fieldPath": "serverId", @@ -149,7 +149,7 @@ "fieldPath": "email", "columnName": "email", "affinity": "TEXT", - "notNull": true + "notNull": false }, { "fieldPath": "balance", @@ -358,7 +358,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f794acceadd9ed28da1b218972b5e530')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e26316f758271c58bc953e756fc16e7d')" ] } } \ No newline at end of file diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Drink.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Drink.kt index fe82de5c3c428098d6ae2c2bc708ac28eeee1e25..18d5a79f78d29eb6b0b19fb5bec93f657aeb6244 100644 --- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Drink.kt +++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Drink.kt @@ -50,8 +50,11 @@ data class Drink( val volume: BigDecimal, val caffeine: Int?, val price: BigDecimal, - val logoUrl: String, + val logoUrl: String?, ) { + val originalLogoUrl + get() = logoUrl?.replace("/thumb/", "/original/") + companion object { fun fromModel(server: Server, value: DrinkModel) = Drink( server.serverId, @@ -61,7 +64,9 @@ data class Drink( value.volume, value.caffeine, value.price, - URI.create(server.url).resolve(value.logoUrl).toString() + value.logoUrl?.let { + URI.create(server.url).resolve(it).toString() + } ) } } diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/User.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/User.kt index 8d06a2d44f5738eb2664688b399b365fa79cbe8c..8052cde69ea1e8741709c690ae36726743b5c3fa 100644 --- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/User.kt +++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/User.kt @@ -48,18 +48,20 @@ data class User( val userId: UserId, val active: Boolean, val name: String, - val email: String, + val email: String?, val balance: BigDecimal, val audit: Boolean, val redirect: Boolean, ) { @OptIn(ExperimentalStdlibApi::class) - val gravatarUrl: String by lazy { - val normalised: String = email.lowercase(Locale.ROOT) - val binaryData: ByteArray = normalised.toByteArray(Charsets.UTF_8) - val binaryHash: ByteArray = MessageDigest.getInstance("MD5").digest(binaryData) - val hash: String = binaryHash.toHexString() - "https://www.gravatar.com/avatar/$hash?d=404&s=640" + val gravatarUrl: String? by lazy { + email?.let { + val normalised: String = it.lowercase(Locale.ROOT) + val binaryData: ByteArray = normalised.toByteArray(Charsets.UTF_8) + val binaryHash: ByteArray = MessageDigest.getInstance("MD5").digest(binaryData) + val hash: String = binaryHash.toHexString() + "https://www.gravatar.com/avatar/$hash?d=404&s=640" + } } companion object {