Statistik: PB-listan laddade inte — init-ordning i StatsViewModel + fallback mot äldre gym-API
All checks were successful
release / build-release (push) Successful in 4m22s
All checks were successful
release / build-release (push) Successful in 4m22s
- StatsViewModel: pbMode/pbs deklarerades efter init { load() } → refreshPbs()
körde på null vid första öppningen (NPE som runCatching svalde). Listan kom
först när man bytte läge/period. Deklarationerna flyttade före init.
- personalBestsDetailed: känner igen schemafel ("does not exist") från ett
äldre API och faller tillbaka på den gamla personalBests-frågan +
favoriteExercises; PbList.legacyApi styr UI:t (lägen/sök/ark avstängda,
notis om att gym-API:t behöver uppdateras).
- Fel vid PB-hämtning visas under rubriken i stället för tom lista;
LiftStatusSheet/PbSearchCard visar serverns fel i stället för "offline?".
- README: API-krav för rekordstatus, fallback, Waydroid-testflöde.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NGT3xpdm51McmSrJcHnrzq
This commit is contained in:
19
README.md
19
README.md
@@ -41,6 +41,17 @@ I VS Code: kör build-tasken (`Ctrl+Skift+B`) — den lägger APK:n i
|
|||||||
`build/FitnessDroid.apk`. Tasken `installera på telefon (adb)` installerar den
|
`build/FitnessDroid.apk`. Tasken `installera på telefon (adb)` installerar den
|
||||||
direkt på en ansluten telefon med USB-felsökning på.
|
direkt på en ansluten telefon med USB-felsökning på.
|
||||||
|
|
||||||
|
### Testa lokalt i Waydroid
|
||||||
|
|
||||||
|
Waydroid (x86_64, Android 13) fungerar som emulator på brasse-linux01. Sessionen startas
|
||||||
|
med `waydroid session start` + `waydroid show-full-ui`, sedan
|
||||||
|
`adb connect 192.168.240.112:5555` (IP:n från `waydroid status`) — första gången måste
|
||||||
|
*Allow USB debugging* godkännas i Waydroid-fönstret. Installera med
|
||||||
|
`adb install -r app/build/outputs/apk/debug/app-debug.apk`; skärmdumpar via
|
||||||
|
`adb exec-out screencap -p > fil.png`, UI-koordinater via `uiautomator dump`.
|
||||||
|
Saknar Android nät bortom värden är det Dockers FORWARD-spärr — se infra-Doc
|
||||||
|
`hosts/brasse-linux01.md` (Waydroid-avsnittet). Testkonto: `bb01`.
|
||||||
|
|
||||||
### Testa mot API:t
|
### Testa mot API:t
|
||||||
|
|
||||||
Logga in med ditt vanliga gym-konto (LDAP). API-url:en går att ändra under
|
Logga in med ditt vanliga gym-konto (LDAP). API-url:en går att ändra under
|
||||||
@@ -59,6 +70,14 @@ bästa est. 1RM. Stjärnan på ett PB-kort gör övningen till **favorit** (samm
|
|||||||
favoriter som övningsväljaren) så att den ligger överst i PB-listan i både app och webb. "Räknas ej" tas ur rekord, 1RM och PB-höjdpunkter på servern
|
favoriter som övningsväljaren) så att den ligger överst i PB-listan i både app och webb. "Räknas ej" tas ur rekord, 1RM och PB-höjdpunkter på servern
|
||||||
(kod: `ui/common/LiftStatusSheet.kt`, API: `setLiftStatus`/`setSessionSetStatus`).
|
(kod: `ui/common/LiftStatusSheet.kt`, API: `setLiftStatus`/`setSessionSetStatus`).
|
||||||
|
|
||||||
|
**Kräver gym-API:t från 2026-09-06 eller senare** (`personalBests(mode)`, `pbLookup`,
|
||||||
|
`pbCandidates`, `liftDetail`, `setLiftStatus`). Mot ett äldre API faller appen tillbaka på
|
||||||
|
den gamla PB-frågan: rekorden visas fortfarande (favoriter hämtas via `favoriteExercises`),
|
||||||
|
men lägesväxeln, sökkortet och detaljarket är avstängda och en notis säger att servern
|
||||||
|
behöver uppdateras. Fel vid hämtningen visas under rubriken i stället för att listan tyst
|
||||||
|
blir tom. Schemafel känns igen på HotChocolates "does not exist" (`GraphQlException.isSchemaMismatch()`
|
||||||
|
i `data/GraphQlClient.kt`).
|
||||||
|
|
||||||
## Signeringsnyckeln
|
## Signeringsnyckeln
|
||||||
|
|
||||||
Nyckeln är **inte** incheckad (repot är publikt). Lokalt ligger den i
|
Nyckeln är **inte** incheckad (repot är publikt). Lokalt ligger den i
|
||||||
|
|||||||
@@ -17,6 +17,26 @@ import java.util.concurrent.TimeUnit
|
|||||||
|
|
||||||
class GraphQlException(val messages: List<String>) : Exception(messages.joinToString("; "))
|
class GraphQlException(val messages: List<String>) : Exception(messages.joinToString("; "))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sant när servern inte känner till ett fält eller argument i frågan — dvs. den
|
||||||
|
* kör ett äldre gym-API än appen är byggd för (HotChocolate svarar
|
||||||
|
* "The field `x` does not exist on the type `Y`." / "The argument `x` does not exist.").
|
||||||
|
* Nät- och auth-fel ger aldrig detta.
|
||||||
|
*/
|
||||||
|
fun GraphQlException.isSchemaMismatch(): Boolean =
|
||||||
|
messages.any { it.contains("does not exist") }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Feltext att visa användaren: pekar ut ett för gammalt gym-API på servern,
|
||||||
|
* annars serverns eget GraphQL-meddelande, och för allt annat (IO, timeout) [offline].
|
||||||
|
*/
|
||||||
|
fun Throwable.userMessage(offline: String): String = when {
|
||||||
|
this is GraphQlException && isSchemaMismatch() ->
|
||||||
|
"Servern kör ett äldre gym-API som saknar den här funktionen — uppdatera gym-API:t."
|
||||||
|
this is GraphQlException -> messages.joinToString("; ")
|
||||||
|
else -> offline
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Minimal GraphQL-klient över OkHttp. Samma request-form som webbklienten
|
* Minimal GraphQL-klient över OkHttp. Samma request-form som webbklienten
|
||||||
* (gymTrackerApi.js): POST {query, variables} + "Authorization: Bearer <jwt>".
|
* (gymTrackerApi.js): POST {query, variables} + "Authorization: Bearer <jwt>".
|
||||||
|
|||||||
@@ -299,19 +299,51 @@ class GymRepository(
|
|||||||
|
|
||||||
/* ---------- Rekordstatus (träning / tävling / räknas ej) ---------- */
|
/* ---------- Rekordstatus (träning / tävling / räknas ej) ---------- */
|
||||||
|
|
||||||
/** PB-lista med detaljer per rekord. mode: all | competition | training. */
|
/**
|
||||||
suspend fun personalBestsDetailed(mode: String = "all"): List<PbEntry> {
|
* PB-lista med detaljer per rekord. mode: all | competition | training.
|
||||||
val pb = client.execute(
|
*
|
||||||
|
* Kör servern ett äldre gym-API (utan rekordstatus, favoriter i PB-svaret och
|
||||||
|
* pbLookup) svarar den med schemafel på den fullständiga frågan. Då hämtas i
|
||||||
|
* stället den gamla frågan (övning + reps/vikt/datum, favoriter via
|
||||||
|
* favoriteExercises) och [PbList.legacyApi] blir true så att UI:t kan visa
|
||||||
|
* rekorden men gömma det servern inte kan. Nät- och auth-fel kastas vidare.
|
||||||
|
*/
|
||||||
|
suspend fun personalBestsDetailed(mode: String = "all"): PbList {
|
||||||
|
val token = auth.bearerToken()
|
||||||
|
val full = try {
|
||||||
|
client.execute(
|
||||||
"query(\$m:String){personalBests(mode:\$m){exerciseTypeId exerciseTypeName isFavorite repRecords{reps weight date liftId notes status statusNote sessionName estimated1Rm}}}",
|
"query(\$m:String){personalBests(mode:\$m){exerciseTypeId exerciseTypeName isFavorite repRecords{reps weight date liftId notes status statusNote sessionName estimated1Rm}}}",
|
||||||
buildJsonObject { put("m", mode) },
|
buildJsonObject { put("m", mode) },
|
||||||
auth.bearerToken(),
|
token,
|
||||||
)
|
)
|
||||||
return pb["personalBests"]!!.jsonArray.map { p ->
|
} catch (e: GraphQlException) {
|
||||||
|
if (!e.isSchemaMismatch()) throw e
|
||||||
|
null
|
||||||
|
}
|
||||||
|
if (full != null) return PbList(parsePbEntries(full), legacyApi = false)
|
||||||
|
|
||||||
|
val legacy = client.execute(
|
||||||
|
"query{personalBests{exerciseTypeId exerciseTypeName repRecords{reps weight date}}}",
|
||||||
|
token = token,
|
||||||
|
)
|
||||||
|
val favorites = runCatching {
|
||||||
|
client.execute("query{favoriteExercises{id}}", token = token)["favoriteExercises"]!!
|
||||||
|
.jsonArray.map { it.jsonObject["id"]!!.jsonPrimitive.int }.toSet()
|
||||||
|
}.getOrDefault(emptySet())
|
||||||
|
return PbList(parsePbEntries(legacy, favorites), legacyApi = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parsePbEntries(
|
||||||
|
data: kotlinx.serialization.json.JsonObject,
|
||||||
|
favorites: Set<Int> = emptySet(),
|
||||||
|
): List<PbEntry> =
|
||||||
|
data["personalBests"]!!.jsonArray.map { p ->
|
||||||
val po = p.jsonObject
|
val po = p.jsonObject
|
||||||
|
val typeId = po["exerciseTypeId"]?.jsonPrimitive?.intOrNull ?: 0
|
||||||
PbEntry(
|
PbEntry(
|
||||||
exerciseName = po["exerciseTypeName"]?.jsonPrimitive?.contentOrNull() ?: "Övning",
|
exerciseName = po["exerciseTypeName"]?.jsonPrimitive?.contentOrNull() ?: "Övning",
|
||||||
exerciseTypeId = po["exerciseTypeId"]?.jsonPrimitive?.intOrNull ?: 0,
|
exerciseTypeId = typeId,
|
||||||
isFavorite = po["isFavorite"]?.jsonPrimitive?.booleanOrNull ?: false,
|
isFavorite = po["isFavorite"]?.jsonPrimitive?.booleanOrNull ?: (typeId in favorites),
|
||||||
records = po["repRecords"]!!.jsonArray.mapNotNull { r ->
|
records = po["repRecords"]!!.jsonArray.mapNotNull { r ->
|
||||||
val ro = r.jsonObject
|
val ro = r.jsonObject
|
||||||
PbRecord(
|
PbRecord(
|
||||||
@@ -328,7 +360,6 @@ class GymRepository(
|
|||||||
}.sortedBy { it.reps },
|
}.sortedBy { it.reps },
|
||||||
)
|
)
|
||||||
}.sortedWith(compareByDescending<PbEntry> { it.isFavorite }.thenBy { it.exerciseName })
|
}.sortedWith(compareByDescending<PbEntry> { it.isFavorite }.thenBy { it.exerciseName })
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PB-sökning: reps vid given vikt eller vikt vid givna reps – exakt träff eller
|
* PB-sökning: reps vid given vikt eller vikt vid givna reps – exakt träff eller
|
||||||
@@ -784,7 +815,7 @@ class GymRepository(
|
|||||||
}
|
}
|
||||||
}.getOrDefault(emptyMap())
|
}.getOrDefault(emptyMap())
|
||||||
|
|
||||||
val pbs: List<PbEntry> = runCatching { personalBestsDetailed("all") }.getOrDefault(emptyList())
|
val pbs: List<PbEntry> = runCatching { personalBestsDetailed("all").entries }.getOrDefault(emptyList())
|
||||||
|
|
||||||
fun kpi(key: String) = s[key]?.jsonPrimitive?.doubleOrNull ?: 0.0
|
fun kpi(key: String) = s[key]?.jsonPrimitive?.doubleOrNull ?: 0.0
|
||||||
fun prevKpi(key: String) = prev?.get(key)?.jsonPrimitive?.doubleOrNull
|
fun prevKpi(key: String) = prev?.get(key)?.jsonPrimitive?.doubleOrNull
|
||||||
@@ -994,6 +1025,13 @@ data class PbEntry(
|
|||||||
val isFavorite: Boolean = false,
|
val isFavorite: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Svar från [GymRepository.personalBestsDetailed]. [legacyApi] = servern kör ett
|
||||||
|
* äldre gym-API: rekorden finns men utan lyft-id/status (liftId = 0), läget
|
||||||
|
* (mode) ignoreras och pbLookup/pbCandidates/liftDetail saknas.
|
||||||
|
*/
|
||||||
|
data class PbList(val entries: List<PbEntry>, val legacyApi: Boolean = false)
|
||||||
|
|
||||||
data class PbLookupHit(
|
data class PbLookupHit(
|
||||||
val weight: Double,
|
val weight: Double,
|
||||||
val reps: Int,
|
val reps: Int,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import eu.brassepc.fitnessdroid.data.userMessage
|
||||||
import eu.brassepc.fitnessdroid.data.GymRepository
|
import eu.brassepc.fitnessdroid.data.GymRepository
|
||||||
import eu.brassepc.fitnessdroid.data.LiftDetail
|
import eu.brassepc.fitnessdroid.data.LiftDetail
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -118,7 +119,7 @@ fun LiftStatusSheet(
|
|||||||
status = cur?.status ?: (target as? LiftStatusTarget.Set)?.status ?: "training"
|
status = cur?.status ?: (target as? LiftStatusTarget.Set)?.status ?: "training"
|
||||||
note = cur?.statusNote ?: (target as? LiftStatusTarget.Set)?.statusNote ?: ""
|
note = cur?.statusNote ?: (target as? LiftStatusTarget.Set)?.statusNote ?: ""
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
error = "Kunde inte hämta lyftet — offline?"
|
error = e.userMessage("Kunde inte hämta lyftet — offline?")
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
@@ -219,7 +220,7 @@ fun LiftStatusSheet(
|
|||||||
onChanged()
|
onChanged()
|
||||||
load(keepId = f?.id)
|
load(keepId = f?.id)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
error = "Kunde inte spara — offline?"
|
error = e.userMessage("Kunde inte spara — offline?")
|
||||||
} finally {
|
} finally {
|
||||||
saving = false
|
saving = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import eu.brassepc.fitnessdroid.data.userMessage
|
||||||
import eu.brassepc.fitnessdroid.data.GymRepository
|
import eu.brassepc.fitnessdroid.data.GymRepository
|
||||||
import eu.brassepc.fitnessdroid.data.PbEntry
|
import eu.brassepc.fitnessdroid.data.PbEntry
|
||||||
import eu.brassepc.fitnessdroid.data.PbLookup
|
import eu.brassepc.fitnessdroid.data.PbLookup
|
||||||
@@ -74,7 +75,7 @@ fun PbSearchCard(
|
|||||||
result = if (byWeight) repo.pbLookup(ex.exerciseTypeId, weight = n, mode = mode)
|
result = if (byWeight) repo.pbLookup(ex.exerciseTypeId, weight = n, mode = mode)
|
||||||
else repo.pbLookup(ex.exerciseTypeId, reps = n.toInt(), mode = mode)
|
else repo.pbLookup(ex.exerciseTypeId, reps = n.toInt(), mode = mode)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
error = "Kunde inte söka — offline?"
|
error = e.userMessage("Kunde inte söka — offline?")
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import androidx.lifecycle.viewmodel.initializer
|
|||||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||||
import eu.brassepc.fitnessdroid.data.GymRepository
|
import eu.brassepc.fitnessdroid.data.GymRepository
|
||||||
import eu.brassepc.fitnessdroid.data.StatsBundle
|
import eu.brassepc.fitnessdroid.data.StatsBundle
|
||||||
|
import eu.brassepc.fitnessdroid.data.userMessage
|
||||||
import eu.brassepc.fitnessdroid.ui.appContainer
|
import eu.brassepc.fitnessdroid.ui.appContainer
|
||||||
import eu.brassepc.fitnessdroid.ui.common.LiftStatusSheet
|
import eu.brassepc.fitnessdroid.ui.common.LiftStatusSheet
|
||||||
import eu.brassepc.fitnessdroid.ui.common.LiftStatusTarget
|
import eu.brassepc.fitnessdroid.ui.common.LiftStatusTarget
|
||||||
@@ -76,6 +77,19 @@ class StatsViewModel(
|
|||||||
val bodyMeasurements = MutableStateFlow<List<eu.brassepc.fitnessdroid.data.BodyMeasurement>>(emptyList())
|
val bodyMeasurements = MutableStateFlow<List<eu.brassepc.fitnessdroid.data.BodyMeasurement>>(emptyList())
|
||||||
val activities = MutableStateFlow<List<eu.brassepc.fitnessdroid.data.Activity>>(emptyList())
|
val activities = MutableStateFlow<List<eu.brassepc.fitnessdroid.data.Activity>>(emptyList())
|
||||||
|
|
||||||
|
// ── Personbästa: läge (alla giltiga / tävling / träning) + lista ──
|
||||||
|
// OBS: måste deklareras FÖRE init-blocket nedan – Kotlin initierar i
|
||||||
|
// deklarationsordning och init { load() } startar refreshPbs() direkt på
|
||||||
|
// huvudtråden. Låg de efter init var pbMode null vid första hämtningen
|
||||||
|
// (NPE som runCatching svalde → PB-listan tom tills man bytte period).
|
||||||
|
val pbMode = MutableStateFlow("all")
|
||||||
|
val pbs = MutableStateFlow<List<eu.brassepc.fitnessdroid.data.PbEntry>>(emptyList())
|
||||||
|
/** Servern kör ett äldre gym-API: rekorden visas men status, lägen och sökning saknas. */
|
||||||
|
val pbLegacyApi = MutableStateFlow(false)
|
||||||
|
/** Senaste felet vid hämtning av rekorden (visas i stället för "Inga rekord än"). */
|
||||||
|
val pbError = MutableStateFlow<String?>(null)
|
||||||
|
val pbLoading = MutableStateFlow(true)
|
||||||
|
|
||||||
private fun periodStart(): java.time.Instant? {
|
private fun periodStart(): java.time.Instant? {
|
||||||
val now = java.time.Instant.now()
|
val now = java.time.Instant.now()
|
||||||
return when (period.value) {
|
return when (period.value) {
|
||||||
@@ -131,10 +145,7 @@ class StatsViewModel(
|
|||||||
refreshPbs()
|
refreshPbs()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Personbästa: läge (alla giltiga / tävling / träning) + lista ──
|
// ── Personbästa: hämtning ──
|
||||||
val pbMode = MutableStateFlow("all")
|
|
||||||
val pbs = MutableStateFlow<List<eu.brassepc.fitnessdroid.data.PbEntry>>(emptyList())
|
|
||||||
|
|
||||||
fun setPbMode(mode: String) {
|
fun setPbMode(mode: String) {
|
||||||
if (pbMode.value == mode) return
|
if (pbMode.value == mode) return
|
||||||
pbMode.value = mode
|
pbMode.value = mode
|
||||||
@@ -143,7 +154,20 @@ class StatsViewModel(
|
|||||||
|
|
||||||
fun refreshPbs() {
|
fun refreshPbs() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
runCatching { pbs.value = repo.personalBestsDetailed(pbMode.value) }
|
pbLoading.value = true
|
||||||
|
try {
|
||||||
|
val result = repo.personalBestsDetailed(pbMode.value)
|
||||||
|
pbs.value = result.entries
|
||||||
|
pbLegacyApi.value = result.legacyApi
|
||||||
|
pbError.value = null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Behåll det som redan visas men säg vad som gick fel. Tidigare
|
||||||
|
// svaldes felet och listan såg ut som "Inga rekord än" – så såg det
|
||||||
|
// ut när prod-API:t låg efter appen (2026-09).
|
||||||
|
pbError.value = e.userMessage("Kunde inte hämta personbästa — offline?")
|
||||||
|
} finally {
|
||||||
|
pbLoading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,6 +196,9 @@ fun StatsScreen(viewModel: StatsViewModel = viewModel(factory = StatsViewModel.F
|
|||||||
val period by viewModel.period.collectAsStateWithLifecycle()
|
val period by viewModel.period.collectAsStateWithLifecycle()
|
||||||
val pbMode by viewModel.pbMode.collectAsStateWithLifecycle()
|
val pbMode by viewModel.pbMode.collectAsStateWithLifecycle()
|
||||||
val pbs by viewModel.pbs.collectAsStateWithLifecycle()
|
val pbs by viewModel.pbs.collectAsStateWithLifecycle()
|
||||||
|
val pbLegacyApi by viewModel.pbLegacyApi.collectAsStateWithLifecycle()
|
||||||
|
val pbError by viewModel.pbError.collectAsStateWithLifecycle()
|
||||||
|
val pbLoading by viewModel.pbLoading.collectAsStateWithLifecycle()
|
||||||
var pbSheet by remember { mutableStateOf<LiftStatusTarget?>(null) }
|
var pbSheet by remember { mutableStateOf<LiftStatusTarget?>(null) }
|
||||||
|
|
||||||
pbSheet?.let { target ->
|
pbSheet?.let { target ->
|
||||||
@@ -401,13 +428,31 @@ fun StatsScreen(viewModel: StatsViewModel = viewModel(factory = StatsViewModel.F
|
|||||||
items(PB_MODES, key = { it.first }) { (key, label) ->
|
items(PB_MODES, key = { it.first }) { (key, label) ->
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = pbMode == key,
|
selected = pbMode == key,
|
||||||
|
// Äldre server känner inte till lägen – bara "Alla giltiga" går.
|
||||||
|
enabled = !pbLegacyApi || key == "all",
|
||||||
onClick = { viewModel.setPbMode(key) },
|
onClick = { viewModel.setPbMode(key) },
|
||||||
label = { Text(label) },
|
label = { Text(label) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (pbLegacyApi) {
|
||||||
|
Text(
|
||||||
|
"Servern kör ett äldre gym-API: rekorden visas, men tävling/räknas ej, detaljer och sökning kräver att gym-API:t uppdateras.",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(top = 6.dp),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (pbs.isNotEmpty()) {
|
pbError?.let {
|
||||||
|
Text(
|
||||||
|
it,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
modifier = Modifier.padding(top = 6.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pbs.isNotEmpty() && !pbLegacyApi) {
|
||||||
item {
|
item {
|
||||||
PbSearchCard(
|
PbSearchCard(
|
||||||
exercises = pbs,
|
exercises = pbs,
|
||||||
@@ -417,11 +462,14 @@ fun StatsScreen(viewModel: StatsViewModel = viewModel(factory = StatsViewModel.F
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (pbs.isEmpty()) {
|
if (pbs.isEmpty() && pbError == null) {
|
||||||
item {
|
item {
|
||||||
Text(
|
Text(
|
||||||
if (pbMode == "competition") "Inga tävlingslyft märkta än. Tryck på ett rekord eller ett set i historiken och välj Tävling."
|
when {
|
||||||
else "Inga rekord än.",
|
pbLoading -> "Hämtar rekord…"
|
||||||
|
pbMode == "competition" -> "Inga tävlingslyft märkta än. Tryck på ett rekord eller ett set i historiken och välj Tävling."
|
||||||
|
else -> "Inga rekord än."
|
||||||
|
},
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
@@ -450,7 +498,8 @@ fun StatsScreen(viewModel: StatsViewModel = viewModel(factory = StatsViewModel.F
|
|||||||
pb.records.forEach { r ->
|
pb.records.forEach { r ->
|
||||||
androidx.compose.material3.AssistChip(
|
androidx.compose.material3.AssistChip(
|
||||||
onClick = {
|
onClick = {
|
||||||
pbSheet = LiftStatusTarget.Lift(pb.exerciseTypeId, pb.exerciseName, r.reps, r.liftId)
|
// liftId = 0 på äldre server – inget ark att öppna.
|
||||||
|
if (r.liftId > 0) pbSheet = LiftStatusTarget.Lift(pb.exerciseTypeId, pb.exerciseName, r.reps, r.liftId)
|
||||||
},
|
},
|
||||||
label = {
|
label = {
|
||||||
Text("${r.reps}RM ${r.weight.compact()} kg${if (r.status == "competition") " 🎖️" else ""}")
|
Text("${r.reps}RM ${r.weight.compact()} kg${if (r.status == "competition") " 🎖️" else ""}")
|
||||||
@@ -459,7 +508,8 @@ fun StatsScreen(viewModel: StatsViewModel = viewModel(factory = StatsViewModel.F
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
"Tryck på ett rekord för detaljer, tävling eller räknas ej.",
|
if (pbLegacyApi) "Detaljer och rekordstatus kräver ett uppdaterat gym-API på servern."
|
||||||
|
else "Tryck på ett rekord för detaljer, tävling eller räknas ej.",
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.padding(top = 4.dp),
|
modifier = Modifier.padding(top = 4.dp),
|
||||||
|
|||||||
Reference in New Issue
Block a user