Skip to content

Commit 71d6ee6

Browse files
authored
feat: calories card details (#38)
1 parent 8a7e2f5 commit 71d6ee6

28 files changed

+1574
-514
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ A free and open-source calorie counter app
1414
<img src="metadata/en-US/images/phoneScreenshots/200_meal_day.png" width="30%"/>
1515
<img src="metadata/en-US/images/phoneScreenshots/300_search.png" width="30%"/>
1616
<img src="metadata/en-US/images/phoneScreenshots/400_measure_product.png" width="30%"/>
17+
<img src="metadata/en-US/images/phoneScreenshots/201_diary_day_details.png" width="30%"/>
1718
<img src="metadata/en-US/images/phoneScreenshots/500_create_product.png" width="30%"/>
1819
<img src="metadata/en-US/images/phoneScreenshots/600_settings.png" width="30%"/>
1920
</div>

app/src/commonMain/composeResources/values/strings.xml

+2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
<string name="headline_open_food_facts" translatable="false">Open Food Facts</string>
3434
<string name="headline_all_day" description="All-day (meal)">All-day</string>
3535
<string name="headline_time_based_ordering">Time-based ordering</string>
36+
<string name="headline_incomplete_products">Incomplete products</string>
3637

3738
<string name="headline_remote_food_database">Remote food database</string>
3839
<string name="open_food_facts_disclaimer">Open Food Facts is a community-driven project. Data may be incomplete, outdated, or inaccurate. This app is not responsible for the validity of the information. Please verify before use.</string>
@@ -175,6 +176,7 @@
175176
<string name="description_open_food_facts_clear_cache" description="Warn user that it will cause full database re-download.">Clearing the cache will cause the entire Open Food Facts database to be re-downloaded. It is not recommended to clear the cache unless you know what you are doing.</string>
176177
<string name="description_time_based_meals_sorting">When enabled, meals that are currently happening will be displayed first</string>
177178
<string name="description_action_include_all_day_meals">All-day meals will be included in the current meal list</string>
179+
<string name="description_incomplete_nutrition_data">Some of the used products lack this value; therefore, the displayed value might not be accurate</string>
178180

179181
<string name="link_icons8" translatable="false">https://icons8.com/</string>
180182
<string name="headline_launcher_icon_by_icons8">Launcher icon by Icons8</string>

app/src/commonMain/kotlin/com/maksimowiczm/foodyou/feature/diary/DiaryFeature.kt

+99-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
11
package com.maksimowiczm.foodyou.feature.diary
22

3+
import androidx.compose.animation.core.FastOutLinearInEasing
4+
import androidx.compose.animation.core.LinearOutSlowInEasing
5+
import androidx.compose.animation.core.tween
6+
import androidx.compose.animation.scaleOut
7+
import androidx.compose.animation.slideInVertically
8+
import androidx.compose.animation.slideOutVertically
9+
import androidx.compose.material3.MaterialTheme
10+
import androidx.compose.material3.Surface
11+
import androidx.compose.ui.unit.dp
312
import androidx.navigation.NavController
413
import androidx.navigation.NavGraphBuilder
14+
import androidx.navigation.compose.composable
515
import androidx.navigation.navOptions
616
import androidx.navigation.toRoute
717
import com.maksimowiczm.foodyou.feature.Feature
@@ -18,9 +28,11 @@ import com.maksimowiczm.foodyou.feature.diary.data.ProductRepositoryImpl
1828
import com.maksimowiczm.foodyou.feature.diary.database.DiaryDatabase
1929
import com.maksimowiczm.foodyou.feature.diary.network.OpenFoodFactsRemoteMediatorFactory
2030
import com.maksimowiczm.foodyou.feature.diary.network.ProductRemoteMediatorFactory
21-
import com.maksimowiczm.foodyou.feature.diary.ui.DiaryViewModel
2231
import com.maksimowiczm.foodyou.feature.diary.ui.MealApp
2332
import com.maksimowiczm.foodyou.feature.diary.ui.caloriescard.CaloriesCard
33+
import com.maksimowiczm.foodyou.feature.diary.ui.caloriescard.CaloriesCardViewModel
34+
import com.maksimowiczm.foodyou.feature.diary.ui.caloriesscreen.CaloriesScreen
35+
import com.maksimowiczm.foodyou.feature.diary.ui.caloriesscreen.CaloriesScreenViewModel
2436
import com.maksimowiczm.foodyou.feature.diary.ui.goalssettings.GoalsSettingsListItem
2537
import com.maksimowiczm.foodyou.feature.diary.ui.goalssettings.GoalsSettingsScreen
2638
import com.maksimowiczm.foodyou.feature.diary.ui.goalssettings.GoalsSettingsViewModel
@@ -38,16 +50,21 @@ import com.maksimowiczm.foodyou.feature.diary.ui.openfoodfactssettings.OpenFoodF
3850
import com.maksimowiczm.foodyou.feature.diary.ui.openfoodfactssettings.buildOpenFoodFactsSettingsListItem
3951
import com.maksimowiczm.foodyou.feature.diary.ui.openfoodfactssettings.flagCdnCountryFlag
4052
import com.maksimowiczm.foodyou.feature.diary.ui.product.create.CreateProductViewModel
53+
import com.maksimowiczm.foodyou.feature.diary.ui.product.update.UpdateProductDialog
4154
import com.maksimowiczm.foodyou.feature.diary.ui.product.update.UpdateProductViewModel
4255
import com.maksimowiczm.foodyou.feature.diary.ui.search.OpenFoodFactsSearchHintViewModel
4356
import com.maksimowiczm.foodyou.feature.diary.ui.search.SearchViewModel
4457
import com.maksimowiczm.foodyou.navigation.crossfadeComposable
4558
import com.maksimowiczm.foodyou.navigation.forwardBackwardComposable
59+
import com.maksimowiczm.foodyou.ui.motion.crossfadeIn
60+
import kotlinx.datetime.LocalDate
4661
import kotlinx.serialization.Serializable
62+
import org.koin.compose.viewmodel.koinViewModel
4763
import org.koin.core.module.Module
4864
import org.koin.core.module.dsl.factoryOf
4965
import org.koin.core.module.dsl.singleOf
5066
import org.koin.core.module.dsl.viewModelOf
67+
import org.koin.core.parameter.parametersOf
5168
import org.koin.dsl.bind
5269
import org.koin.dsl.module
5370

@@ -77,10 +94,21 @@ object DiaryFeature : Feature {
7794
)
7895
}
7996
),
80-
HomeFeature { _, modifier, homeState ->
97+
HomeFeature { animatedVisibilityScope, modifier, homeState ->
8198
CaloriesCard(
99+
animatedVisibilityScope = animatedVisibilityScope,
82100
homeState = homeState,
83-
modifier = modifier
101+
modifier = modifier,
102+
onClick = {
103+
navController.navigate(
104+
route = CaloriesDetails(
105+
epochDay = homeState.selectedDate.toEpochDays()
106+
),
107+
navOptions = navOptions {
108+
launchSingleTop = true
109+
}
110+
)
111+
}
84112
)
85113
}
86114
)
@@ -139,6 +167,12 @@ object DiaryFeature : Feature {
139167
@Serializable
140168
private data class MealAdd(val epochDay: Int, val mealId: Long)
141169

170+
@Serializable
171+
private data class CaloriesDetails(val epochDay: Int)
172+
173+
@Serializable
174+
private data class EditProductDialog(val productId: Long)
175+
142176
override fun NavGraphBuilder.graph(navController: NavController) {
143177
forwardBackwardComposable<GoalsSettings> {
144178
GoalsSettingsScreen(
@@ -208,6 +242,65 @@ object DiaryFeature : Feature {
208242
}
209243
)
210244
}
245+
246+
crossfadeComposable<CaloriesDetails> {
247+
val (epochDay) = it.toRoute<CaloriesDetails>()
248+
val date = LocalDate.fromEpochDays(epochDay)
249+
250+
CaloriesScreen(
251+
date = date,
252+
animatedVisibilityScope = this@crossfadeComposable,
253+
onProductClick = {
254+
navController.navigate(
255+
route = EditProductDialog(
256+
productId = it.id
257+
),
258+
navOptions = navOptions {
259+
launchSingleTop = true
260+
}
261+
)
262+
}
263+
)
264+
}
265+
266+
composable<EditProductDialog>(
267+
enterTransition = {
268+
crossfadeIn() + slideInVertically(
269+
animationSpec = tween(
270+
easing = LinearOutSlowInEasing
271+
),
272+
initialOffsetY = { it }
273+
)
274+
},
275+
exitTransition = {
276+
slideOutVertically(
277+
animationSpec = tween(
278+
easing = FastOutLinearInEasing
279+
),
280+
targetOffsetY = { it }
281+
) + scaleOut(
282+
targetScale = 0.8f,
283+
animationSpec = tween(
284+
easing = FastOutLinearInEasing
285+
)
286+
)
287+
}
288+
) {
289+
val (productId) = it.toRoute<EditProductDialog>()
290+
291+
Surface(
292+
shadowElevation = 6.dp,
293+
shape = MaterialTheme.shapes.medium
294+
) {
295+
UpdateProductDialog(
296+
onClose = { navController.popBackStack<EditProductDialog>(inclusive = true) },
297+
onSuccess = { navController.popBackStack<EditProductDialog>(inclusive = true) },
298+
viewModel = koinViewModel(
299+
parameters = { parametersOf(productId) }
300+
)
301+
)
302+
}
303+
}
211304
}
212305

213306
override val module: Module = module {
@@ -228,8 +321,6 @@ object DiaryFeature : Feature {
228321

229322
factoryOf(::DiaryRepositoryImpl).bind<DiaryRepository>()
230323

231-
viewModelOf(::DiaryViewModel)
232-
233324
viewModelOf(::GoalsSettingsViewModel)
234325

235326
viewModelOf(::MealsSettingsScreenViewModel)
@@ -245,5 +336,8 @@ object DiaryFeature : Feature {
245336
factory { get<DiaryDatabase>().addFoodDao() }
246337
factory { get<DiaryDatabase>().productDao() }
247338
factory { get<DiaryDatabase>().openFoodFactsDao() }
339+
340+
viewModelOf(::CaloriesCardViewModel)
341+
viewModelOf(::CaloriesScreenViewModel)
248342
}
249343
}

app/src/commonMain/kotlin/com/maksimowiczm/foodyou/feature/diary/data/model/DailyGoals.kt

+6-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.maksimowiczm.foodyou.feature.diary.data.model
22

33
import com.maksimowiczm.foodyou.feature.diary.data.NutrientsHelper
4-
import kotlin.math.roundToInt
54

65
data class DailyGoals(
76
val calories: Int,
@@ -42,20 +41,20 @@ data class DailyGoals(
4241
/**
4342
* Proteins goal in grams.
4443
*/
45-
val proteinsAsGrams: Int
46-
get() = (calories * proteins / NutrientsHelper.PROTEINS).roundToInt()
44+
val proteinsAsGrams: Float
45+
get() = (calories * proteins / NutrientsHelper.PROTEINS)
4746

4847
/**
4948
* Carbohydrates goal in grams.
5049
*/
51-
val carbohydratesAsGrams: Int
52-
get() = (calories * carbohydrates / NutrientsHelper.CARBOHYDRATES).roundToInt()
50+
val carbohydratesAsGrams: Float
51+
get() = (calories * carbohydrates / NutrientsHelper.CARBOHYDRATES)
5352

5453
/**
5554
* Fats goal in grams.
5655
*/
57-
val fatsAsGrams: Int
58-
get() = (calories * fats / NutrientsHelper.FATS).roundToInt()
56+
val fatsAsGrams: Float
57+
get() = (calories * fats / NutrientsHelper.FATS)
5958
}
6059

6160
fun defaultGoals() = DailyGoals(
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package com.maksimowiczm.foodyou.feature.diary.data.model
22

3-
import com.maksimowiczm.foodyou.feature.diary.data.NutrientsHelper
4-
import kotlin.math.roundToInt
53
import kotlinx.datetime.LocalDate
64

75
data class DiaryDay(
@@ -20,43 +18,73 @@ data class DiaryDay(
2018
/**
2119
* Total calories for the meal in the diary day.
2220
*/
23-
fun totalCalories(meal: Meal) = mealProductMap[meal]?.sumOf { it.calories } ?: 0
21+
fun totalCalories(meal: Meal) = mealProductMap[meal]?.sumOf { it.calories } ?: 0f
2422

25-
fun totalProteins(meal: Meal) = mealProductMap[meal]?.sumOf { it.proteins } ?: 0
23+
fun totalProteins(meal: Meal) = mealProductMap[meal]?.sumOf { it.proteins } ?: 0f
2624

27-
fun totalCarbohydrates(meal: Meal) = mealProductMap[meal]?.sumOf { it.carbohydrates } ?: 0
25+
fun totalCarbohydrates(meal: Meal) = mealProductMap[meal]?.sumOf { it.carbohydrates } ?: 0f
2826

29-
fun totalFats(meal: Meal) = mealProductMap[meal]?.sumOf { it.fats } ?: 0
27+
fun totalFats(meal: Meal) = mealProductMap[meal]?.sumOf { it.fats } ?: 0f
3028

31-
val totalCalories: Int
29+
/**
30+
* Total calories for the diary day.
31+
*/
32+
val totalCalories: Float
3233
get() = mealProductMap.values.flatten().sumOf { it.calories }
3334

34-
val totalCaloriesProteins: Int
35-
get() = mealProductMap.values.flatten().fold(0f) { acc, product ->
36-
NutrientsHelper.proteinsToCalories(product.proteins) + acc
37-
}.roundToInt()
38-
39-
val totalCaloriesCarbohydrates: Int
40-
get() = mealProductMap.values.flatten().fold(0f) { acc, product ->
41-
NutrientsHelper.carbohydratesToCalories(product.carbohydrates) + acc
42-
}.roundToInt()
35+
fun totalCalories(meals: List<Meal>) = meals.sumOf { totalCalories(it) }
4336

44-
val totalCaloriesFats: Int
45-
get() = mealProductMap.values.flatten().fold(0f) { acc, product ->
46-
NutrientsHelper.fatsToCalories(product.fats) + acc
47-
}.roundToInt()
48-
49-
val totalProteins: Int
37+
val totalProteins: Float
5038
get() = mealProductMap.values.flatten().sumOf { it.proteins }
5139

52-
val totalCarbohydrates: Int
40+
fun totalProteins(meals: List<Meal>) = meals.sumOf { totalProteins(it) }
41+
42+
val totalCarbohydrates: Float
5343
get() = mealProductMap.values.flatten().sumOf { it.carbohydrates }
5444

55-
val totalFats: Int
45+
fun totalCarbohydrates(meals: List<Meal>) = meals.sumOf { totalCarbohydrates(it) }
46+
47+
val totalFats: Float
5648
get() = mealProductMap.values.flatten().sumOf { it.fats }
5749

50+
fun totalFats(meals: List<Meal>) = meals.sumOf { totalFats(it) }
51+
52+
/**
53+
* Total nutrient for the given meals in the diary day.
54+
*/
55+
fun total(nutrient: Nutrient, meals: List<Meal>): NutrientSummary {
56+
val products = meals.flatMap { mealProductMap[it] ?: emptyList() }
57+
58+
val incomplete = products.any { it.product.nutrients.get(nutrient, it.weight) == null }
59+
60+
val total = products.fold(0f) { acc, product ->
61+
val value = product.product.nutrients.get(nutrient, product.weight) ?: 0f
62+
value + acc
63+
}
64+
65+
return if (incomplete) {
66+
NutrientSummary.Incomplete(total)
67+
} else {
68+
NutrientSummary.Complete(total)
69+
}
70+
}
71+
5872
/**
5973
* List of all meals in the diary day.
6074
*/
6175
val meals: List<Meal> get() = mealProductMap.keys.toList()
76+
77+
sealed interface NutrientSummary {
78+
val value: Float
79+
80+
@JvmInline
81+
value class Complete(override val value: Float) : NutrientSummary
82+
83+
@JvmInline
84+
value class Incomplete(override val value: Float) : NutrientSummary
85+
}
86+
}
87+
88+
private inline fun <T> List<T>.sumOf(selector: (T) -> Float) = fold(0f) { acc, element ->
89+
selector(element) + acc
6290
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.maksimowiczm.foodyou.feature.diary.data.model
2+
3+
enum class Nutrient {
4+
Calories,
5+
Proteins,
6+
Carbohydrates,
7+
Sugars,
8+
Fats,
9+
SaturatedFats,
10+
Salt,
11+
Sodium,
12+
Fiber
13+
}

app/src/commonMain/kotlin/com/maksimowiczm/foodyou/feature/diary/data/model/Nutrients.kt

+23-12
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,32 @@ data class Nutrients(
1414
val sodium: Float? = null,
1515
val fiber: Float? = null
1616
) {
17+
// Required fields
1718
fun calories(weight: Float): Float = calories * weight / 100
18-
1919
fun proteins(weight: Float): Float = proteins * weight / 100
20-
2120
fun carbohydrates(weight: Float): Float = carbohydrates * weight / 100
22-
23-
fun sugars(weight: Float): Float? = sugars?.times(weight / 100)
24-
2521
fun fats(weight: Float): Float = fats * weight / 100
2622

27-
fun saturatedFats(weight: Float): Float? = saturatedFats?.times(weight / 100)
28-
29-
fun salt(weight: Float): Float? = salt?.times(weight / 100)
30-
31-
fun sodium(weight: Float): Float? = sodium?.times(weight / 100)
32-
33-
fun fiber(weight: Float): Float? = fiber?.times(weight / 100)
23+
// Optional fields
24+
fun get(nutrient: Nutrient, weight: Float): Float? = when (nutrient) {
25+
Nutrient.Calories -> calories(weight)
26+
Nutrient.Proteins -> proteins(weight)
27+
Nutrient.Carbohydrates -> carbohydrates(weight)
28+
Nutrient.Sugars -> sugars?.times(weight)?.div(100)
29+
Nutrient.Fats -> fats(weight)
30+
Nutrient.SaturatedFats -> saturatedFats?.times(weight)?.div(100)
31+
Nutrient.Salt -> salt?.times(weight)?.div(100)
32+
Nutrient.Sodium -> sodium?.times(weight)?.div(100)
33+
Nutrient.Fiber -> fiber?.times(weight)?.div(100)
34+
}
35+
36+
/**
37+
* All fields are present.
38+
*/
39+
val isComplete: Boolean
40+
get() = sugars != null &&
41+
saturatedFats != null &&
42+
salt != null &&
43+
sodium != null &&
44+
fiber != null
3445
}

0 commit comments

Comments
 (0)