From 34c2a6c4da52e8af4d350564e17633ad765d7d8b Mon Sep 17 00:00:00 2001 From: claude Date: Sat, 25 Jul 2026 16:42:45 +0200 Subject: [PATCH] =?UTF-8?q?Statistik:=20scrollbar=20trendgraf=20med=20l?= =?UTF-8?q?=C3=A4sbara=20etiketter=20+=20Totalt-period;=20auth-luckor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Trendstaplarna i en horisontellt scrollbar rad (38dp per stapel) med volymsiffra ovanför och hel etikett under — startar på senaste perioden. Fixar etikettröran på månad/halvår - Ny period 'Totalt' (period=all, stöds redan av API:t; delta saknas per design) - Tyst återinloggning provas nu även när tokens saknas helt och när JWT:n hunnit gå ut vid nätfel under appstart - Inställningstext: förtydligar att auto-återinloggning aktiveras vid nästa inloggning (uppgifter sparas bara vid login) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Kf4MAbZ3c3B4Xy5XfuGsCP --- .../fitnessdroid/data/AuthRepository.kt | 17 ++++--- .../ui/settings/SettingsScreen.kt | 3 +- .../fitnessdroid/ui/stats/StatsScreen.kt | 50 +++++++++++++++---- 3 files changed, 53 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/eu/brassepc/fitnessdroid/data/AuthRepository.kt b/app/src/main/java/eu/brassepc/fitnessdroid/data/AuthRepository.kt index f5a52e8..84bceb5 100644 --- a/app/src/main/java/eu/brassepc/fitnessdroid/data/AuthRepository.kt +++ b/app/src/main/java/eu/brassepc/fitnessdroid/data/AuthRepository.kt @@ -92,7 +92,10 @@ class AuthRepository( suspend fun restoreSession() { val saved = store.read() if (saved.sessionId.isNullOrEmpty() || saved.refreshToken.isNullOrEmpty()) { - _state.value = AuthState.LoggedOut + // Inga tokens alls — men kanske sparade uppgifter (t.ex. efter + // att servern rensat sessioner). Prova tyst innan login-skärmen. + val relogged = runCatching { trySilentRelogin() }.getOrNull() + if (relogged == null) _state.value = AuthState.LoggedOut return } try { @@ -112,12 +115,14 @@ class AuthRepository( } } catch (e: Exception) { // Nätverksfel vid uppstart: släpp in användaren om JWT:n inte hunnit - // gå ut, annars visa inloggningen igen. + // gå ut. Har den gått ut: prova tyst återinloggning (kan också + // faila på nätet — då login-skärmen, synken tar det sen). val stillValid = (saved.expirationEpochMs ?: 0) > System.currentTimeMillis() - _state.value = if (stillValid) { - AuthState.LoggedIn(saved.username.orEmpty()) - } else { - AuthState.LoggedOut + _state.value = when { + stillValid -> AuthState.LoggedIn(saved.username.orEmpty()) + runCatching { trySilentRelogin() }.getOrNull() != null -> + AuthState.LoggedIn(store.read().username.orEmpty()) + else -> AuthState.LoggedOut } } } diff --git a/app/src/main/java/eu/brassepc/fitnessdroid/ui/settings/SettingsScreen.kt b/app/src/main/java/eu/brassepc/fitnessdroid/ui/settings/SettingsScreen.kt index 59ead04..be82d2f 100644 --- a/app/src/main/java/eu/brassepc/fitnessdroid/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/eu/brassepc/fitnessdroid/ui/settings/SettingsScreen.kt @@ -188,7 +188,8 @@ fun SettingsScreen( Text("Automatisk återinloggning", style = MaterialTheme.typography.bodyLarge) Text( "Sparar inloggningen krypterat (Android Keystore) och loggar " + - "in igen tyst om servern kastat ut dig", + "in igen tyst om servern kastat ut dig. " + + "Aktiveras vid nästa inloggning.", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) diff --git a/app/src/main/java/eu/brassepc/fitnessdroid/ui/stats/StatsScreen.kt b/app/src/main/java/eu/brassepc/fitnessdroid/ui/stats/StatsScreen.kt index 1a14890..89f3f06 100644 --- a/app/src/main/java/eu/brassepc/fitnessdroid/ui/stats/StatsScreen.kt +++ b/app/src/main/java/eu/brassepc/fitnessdroid/ui/stats/StatsScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.Button @@ -44,6 +45,7 @@ val PERIODS = listOf( PeriodOption("month", "Månad"), PeriodOption("halfyear", "Halvår"), PeriodOption("year", "År"), + PeriodOption("all", "Totalt"), ) sealed interface StatsUiState { @@ -96,8 +98,8 @@ fun StatsScreen(viewModel: StatsViewModel = viewModel(factory = StatsViewModel.F Text("Statistik", style = MaterialTheme.typography.headlineSmall) } item { - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - PERIODS.forEach { p -> + androidx.compose.foundation.lazy.LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + items(PERIODS, key = { it.id }) { p -> FilterChip( selected = period == p.id, onClick = { viewModel.setPeriod(p.id) }, @@ -151,7 +153,14 @@ fun StatsScreen(viewModel: StatsViewModel = viewModel(factory = StatsViewModel.F item { Card(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.padding(14.dp)) { - Text("Volym per ${if (period == "week") "dag" else "period"}", style = MaterialTheme.typography.titleSmall) + Text( + "Volym per " + when (period) { + "week", "month" -> "dag" + "halfyear" -> "vecka" + else -> "månad" + }, + style = MaterialTheme.typography.titleSmall, + ) TrendBars(stats) } } @@ -278,23 +287,44 @@ private fun KpiCard(label: String, value: String, deltaText: String?, modifier: @Composable private fun TrendBars(stats: StatsBundle) { val max = stats.trend.maxOf { it.volumeKg }.takeIf { it > 0 } ?: 1.0 - Row( + val listState = androidx.compose.foundation.lazy.rememberLazyListState() + + // Börja längst till höger — senaste perioden är den intressanta. + androidx.compose.runtime.LaunchedEffect(stats.trend.size) { + if (stats.trend.isNotEmpty()) listState.scrollToItem(stats.trend.size - 1) + } + + androidx.compose.foundation.lazy.LazyRow( + state = listState, modifier = Modifier .fillMaxWidth() - .height(96.dp) + .height(112.dp) .padding(top = 10.dp), - horizontalArrangement = Arrangement.spacedBy(4.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.Bottom, ) { - stats.trend.forEach { bucket -> + items(stats.trend.size) { i -> + val bucket = stats.trend[i] Column( - modifier = Modifier.weight(1f), + modifier = Modifier.width(38.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { + if (bucket.volumeKg > 0) { + Text( + if (bucket.volumeKg >= 1000) { + "${(bucket.volumeKg / 1000).toInt()}t" + } else { + "${bucket.volumeKg.toInt()}" + }, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + ) + } Box( modifier = Modifier .fillMaxWidth() - .height(((bucket.volumeKg / max) * 72).dp.coerceAtLeast(2.dp)) + .height(((bucket.volumeKg / max) * 64).dp.coerceAtLeast(2.dp)) .background( color = MaterialTheme.colorScheme.primary.copy( alpha = if (bucket.volumeKg > 0) 0.85f else 0.25f, @@ -303,7 +333,7 @@ private fun TrendBars(stats: StatsBundle) { ), ) Text( - bucket.label.takeLast(5), + bucket.label, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1,