Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
Numérotation `MAJEUR.MINEUR.CORRECTIF`. Le numéro vit dans `app/build.gradle.kts`
(`versionName`), et `versionCode` s'incrémente de un à chaque version publiée.

## 1.1.1 (2026-11-02)

### Corrections
- Les macros proposées étaient intenables en cas de surpoids : rapportées au poids total, protéines et lipides mangeaient presque tout l'objectif et ne laissaient qu'une cinquantaine de grammes de glucides. Les protéines suivent maintenant un poids de référence plafonné — la masse grasse n'a pas de besoin protéique — et les lipides sont une part des calories, non un montant par kilo. Pour 126 kg à 2 353 kcal : 186 g de protéines, 237 g de glucides et 73 g de lipides, contre 277 / 57 / 113 avant.

## 1.1.0 (2026-11-02)

### Nouveautés
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# FitTrack

**Version 1.1.0** — les nouveautés de chaque version sont dans [CHANGELOG.md](CHANGELOG.md).
**Version 1.1.1** — les nouveautés de chaque version sont dans [CHANGELOG.md](CHANGELOG.md).

Application Android pour **suivre ses séances de salle** et **tracker ses calories et ses macros**.
Pas de compte, pas de serveur, pas de publicité : tout est stocké sur le téléphone.
Expand Down
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ android {
applicationId = "com.kaz229.fittrack"
minSdk = 26
targetSdk = 35
versionCode = 3
versionName = "1.1.0"
versionCode = 4
versionName = "1.1.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

Expand Down
56 changes: 40 additions & 16 deletions app/src/main/java/com/kaz229/fittrack/data/Needs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ fun computeNeeds(profile: NeedsProfile, weightKg: Double): Needs? {
trainingKcal = trainingKcal.roundToInt(),
maintenance = maintenance.roundToInt(),
target = target,
macros = macrosFor(profile.goal, weightKg, target),
macros = macrosFor(profile.goal, weightKg, profile.heightCm, target),
weeklyDeficit = deficit * 7,
achievableWeeklyLossKg = if (deficit > 0) deficit * 7 / KCAL_PER_KG else 0.0,
cappedByFloor = profile.goal == Goal.LOSE && wanted < floor,
Expand All @@ -144,27 +144,51 @@ private fun metKcal(intensity: Intensity, minutes: Int, weightKg: Double): Doubl
intensity.met * 3.5 * weightKg / 200.0 * minutes

/**
* Protéines et lipides d'abord, glucides avec ce qui reste.
* Poids sur lequel asseoir les besoins en protéines.
*
* Les protéines se calent sur le poids (plus hautes en perte, pour garder le muscle),
* les lipides sur un plancher hormonal, et les glucides absorbent le solde — ce sont
* eux qui bougent quand l'objectif change.
* Les rapporter au poids total donne des quantités intenables dès qu'il y a du surpoids :
* la masse grasse n'a pas de besoin protéique. La référence est donc plafonnée au poids
* d'un IMC de 25, majoré de 15 % pour ne pas pénaliser une vraie masse musculaire.
*/
private fun macrosFor(goal: Goal, weightKg: Double, target: Int): MacroGoals {
private fun referenceWeight(weightKg: Double, heightCm: Int): Double {
if (heightCm <= 0) return weightKg
val meters = heightCm / 100.0
return minOf(weightKg, 25.0 * meters * meters * 1.15)
}

/**
* Protéines d'abord, lipides en part des calories, glucides avec ce qui reste.
*
* Les lipides sont une **proportion** de l'objectif, pas un montant par kilo : à poids
* élevé, 0,9 g/kg dépassait 40 % des calories et ne laissait presque rien aux glucides.
* Un plancher les empêche de tomber trop bas quand l'objectif est serré.
*/
private fun macrosFor(goal: Goal, weightKg: Double, heightCm: Int, target: Int): MacroGoals {
val reference = referenceWeight(weightKg, heightCm)
val proteinPerKg = when (goal) {
Goal.LOSE -> 2.2
Goal.LOSE -> 2.0
Goal.GAIN -> 1.8
Goal.MAINTAIN -> 1.8
Goal.MAINTAIN -> 1.6
}
val protein = (weightKg * proteinPerKg).roundToInt()
val fat = (weightKg * 0.9).roundToInt()
val protein = (reference * proteinPerKg).roundToInt()

// 28 % des calories en lipides : la part basse de ce qui se recommande couramment,
// sans descendre sous le seuil hormonal de 0,6 g par kilo de référence.
val fatFloor = (reference * 0.6).roundToInt()
val fat = maxOf((target * 0.28 / 9).roundToInt(), fatFloor)

val remaining = target - protein * 4 - fat * 9
// Jamais de glucides négatifs : sur un objectif très bas, on rogne les lipides
val carbs = if (remaining >= 0) {
remaining / 4
if (remaining >= 0) {
return MacroGoals(proteinG = protein, carbsG = remaining / 4, fatG = fat)
}
// Objectif trop serré pour les deux : on rogne les lipides jusqu'à leur plancher,
// puis les protéines, pour garder des glucides utilisables.
val reducedFat = maxOf(((target - protein * 4) / 9), fatFloor / 2).coerceAtLeast(0)
val leftover = target - protein * 4 - reducedFat * 9
return if (leftover >= 0) {
MacroGoals(proteinG = protein, carbsG = leftover / 4, fatG = reducedFat)
} else {
0
val reducedProtein = ((target - reducedFat * 9) / 4).coerceAtLeast(0)
MacroGoals(proteinG = reducedProtein, carbsG = 0, fatG = reducedFat)
}
val adjustedFat = if (remaining >= 0) fat else ((target - protein * 4) / 9).coerceAtLeast(0)
return MacroGoals(proteinG = protein, carbsG = carbs, fatG = adjustedFat)
}
34 changes: 32 additions & 2 deletions app/src/test/java/com/kaz229/fittrack/NeedsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,38 @@ class NeedsTest {
val maintien = computeNeeds(base, 75.0)!!
val perte = computeNeeds(base.copy(goal = Goal.LOSE), 75.0)!!

assertEquals(135, maintien.macros.proteinG) // 1,8 g/kg
assertEquals(165, perte.macros.proteinG) // 2,2 g/kg
assertEquals(120, maintien.macros.proteinG) // 1,6 g/kg
assertEquals(150, perte.macros.proteinG) // 2,0 g/kg
assertTrue(perte.macros.proteinG > maintien.macros.proteinG)
}

@Test
fun `en surpoids les proteines suivent un poids de reference, pas le poids total`() {
// 126 kg pour 1,80 m : 2 g/kg donneraient 252 g, intenables et inutiles —
// la masse grasse n'a pas de besoin protéique.
val lourd = computeNeeds(base.copy(goal = Goal.LOSE), weightKg = 126.0)!!

assertTrue("Obtenu ${lourd.macros.proteinG} g", lourd.macros.proteinG in 150..200)
}

@Test
fun `les glucides restent utilisables meme a poids eleve`() {
// Le défaut d'origine : protéines et lipides au poids total mangeaient 2129 des
// 2353 kcal, et il ne restait que 57 g de glucides.
val lourd = computeNeeds(base.copy(goal = Goal.LOSE), weightKg = 126.0)!!

assertTrue("Obtenu ${lourd.macros.carbsG} g", lourd.macros.carbsG > 150)
}

@Test
fun `les lipides sont une part des calories, pas un montant par kilo`() {
val leger = computeNeeds(base, 60.0)!!
val lourd = computeNeeds(base, 126.0)!!

// À 0,9 g/kg, le lourd aurait 113 g ; en part des calories il reste mesuré
assertTrue("Obtenu ${lourd.macros.fatG} g", lourd.macros.fatG < 110)
assertTrue(lourd.macros.fatG * 9 < lourd.target * 0.35)
assertTrue(leger.macros.fatG * 9 < leger.target * 0.35)
}

@Test
Expand Down
Loading