From 625c16d3da419228bc747481dec0ea1ed76d2cdb Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 15 Jun 2026 20:56:30 -0300 Subject: [PATCH] fix: cozy, centered loading UI across screens Replace broken loading states with a shared EchoLoadingScreen (bouncing accent dots + caption, centered, respects Reduce Motion). - Fix spurious pull-to-refresh spinner on Home/Decks/Discover: the PullToRefreshBox bound isRefreshing to the initial Loading state, so the swipe indicator showed with no swipe. Initial load now renders the loader outside PullToRefreshBox. - Fix off-center spinners: old LoadingBlock used fillMaxSize() inside a verticalScroll column (unbounded height -> collapsed under the header); Discover also hardcoded a top padding. The loader now centers on the full viewport. - Apply the same loader to DeckDetail, StudySession, Settings, Profile and FriendProfile for consistency. Button-level spinners left unchanged. - Share rememberReduceMotion() via ui/components; add loading captions. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../echo/ui/components/EchoLoadingScreen.kt | 133 ++++++++++++++++++ .../echo/ui/decks/DeckDetailScreen.kt | 5 +- .../jvsena42/echo/ui/decks/DecksScreen.kt | 44 +++--- .../echo/ui/discover/DiscoverScreen.kt | 45 +++--- .../jvsena42/echo/ui/home/HomeScreen.kt | 47 +++---- .../echo/ui/profile/FriendProfileScreen.kt | 5 +- .../jvsena42/echo/ui/profile/ProfileScreen.kt | 5 +- .../echo/ui/settings/SettingsScreen.kt | 5 +- .../echo/ui/study/StudySessionScreen.kt | 26 +--- .../src/androidMain/res/values/strings.xml | 7 + 10 files changed, 215 insertions(+), 107 deletions(-) create mode 100644 composeApp/src/androidMain/kotlin/com/github/jvsena42/echo/ui/components/EchoLoadingScreen.kt diff --git a/composeApp/src/androidMain/kotlin/com/github/jvsena42/echo/ui/components/EchoLoadingScreen.kt b/composeApp/src/androidMain/kotlin/com/github/jvsena42/echo/ui/components/EchoLoadingScreen.kt new file mode 100644 index 0000000..ab90ab3 --- /dev/null +++ b/composeApp/src/androidMain/kotlin/com/github/jvsena42/echo/ui/components/EchoLoadingScreen.kt @@ -0,0 +1,133 @@ +package com.github.jvsena42.echo.ui.components + +import android.provider.Settings +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.StartOffset +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.github.jvsena42.echo.ui.theme.EchoTheme + +private const val DOT_COUNT = 3 +private const val DOT_SIZE_DP = 11 +private const val DOT_GAP_DP = 8 +private const val BOUNCE_HEIGHT_DP = 10 +private const val BOUNCE_DURATION_MS = 480 +private const val DOT_STAGGER_MS = 140 + +/** + * Cozy full-screen loader: three bouncing accent dots with an optional caption, + * centered on the whole viewport. Render this as a direct child of a fill-size + * container — never inside a `verticalScroll` column, or it will collapse and + * lose vertical centering. + */ +@Composable +fun EchoLoadingScreen( + modifier: Modifier = Modifier, + message: String? = null, +) { + val colors = EchoTheme.colors + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(20.dp), + ) { + EchoBouncingDots() + if (!message.isNullOrBlank()) { + Text( + text = message, + color = colors.foregroundMuted, + fontSize = 14.sp, + fontWeight = FontWeight.W600, + ) + } + } + } +} + +@Composable +private fun EchoBouncingDots() { + val colors = EchoTheme.colors + val reduceMotion = rememberReduceMotion() + val transition = rememberInfiniteTransition(label = "dots") + + Row( + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.spacedBy(DOT_GAP_DP.dp), + ) { + repeat(DOT_COUNT) { index -> + val offset by transition.animateFloat( + initialValue = 0f, + targetValue = if (reduceMotion) 0f else 1f, + animationSpec = infiniteRepeatable( + animation = tween( + durationMillis = BOUNCE_DURATION_MS, + easing = FastOutSlowInEasing, + ), + repeatMode = RepeatMode.Reverse, + initialStartOffset = StartOffset(index * DOT_STAGGER_MS), + ), + label = "dot$index", + ) + Box( + modifier = Modifier + .size(DOT_SIZE_DP.dp) + .graphicsLayer { translationY = -offset * BOUNCE_HEIGHT_DP.dp.toPx() } + .clip(CircleShape) + .background(colors.accentPrimary), + ) + } + } +} + +/** + * Reads the OS animation scale; returns true when animations are disabled + * ("Remove animations" / Reduce Motion), so animated UI stays static. + */ +@Composable +fun rememberReduceMotion(): Boolean { + val context = LocalContext.current + return remember(context) { + Settings.Global.getFloat( + context.contentResolver, + Settings.Global.ANIMATOR_DURATION_SCALE, + 1f, + ) == 0f + } +} + +@Preview +@Composable +private fun EchoLoadingScreenPreview() { + EchoTheme { + Box(modifier = Modifier.background(EchoTheme.colors.surfacePrimary)) { + EchoLoadingScreen(message = "Shuffling your decks…") + } + } +} diff --git a/composeApp/src/androidMain/kotlin/com/github/jvsena42/echo/ui/decks/DeckDetailScreen.kt b/composeApp/src/androidMain/kotlin/com/github/jvsena42/echo/ui/decks/DeckDetailScreen.kt index 8e13afa..5648864 100644 --- a/composeApp/src/androidMain/kotlin/com/github/jvsena42/echo/ui/decks/DeckDetailScreen.kt +++ b/composeApp/src/androidMain/kotlin/com/github/jvsena42/echo/ui/decks/DeckDetailScreen.kt @@ -24,7 +24,6 @@ import androidx.compose.material.icons.filled.Share import androidx.compose.material3.AlertDialog import androidx.compose.material3.AssistChip import androidx.compose.material3.AssistChipDefaults -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.FilledIconButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButtonDefaults @@ -56,6 +55,7 @@ import com.github.jvsena42.echo.presentation.decks.DeckDetailUiState import com.github.jvsena42.echo.presentation.decks.DeckDetailViewModel import com.github.jvsena42.echo.ui.components.AuthorRow import com.github.jvsena42.echo.ui.components.CardPreviewRow +import com.github.jvsena42.echo.ui.components.EchoLoadingScreen import com.github.jvsena42.echo.ui.components.EchoPrimaryButton import com.github.jvsena42.echo.ui.components.StatsBar import com.github.jvsena42.echo.ui.components.TagChip @@ -127,9 +127,8 @@ fun DeckDetailScreen( modifier = Modifier .fillMaxSize() .background(colors.surfacePrimary), - contentAlignment = Alignment.Center, ) { - CircularProgressIndicator(color = colors.accentPrimary) + EchoLoadingScreen(message = stringResource(R.string.deck_detail_loading)) } } diff --git a/composeApp/src/androidMain/kotlin/com/github/jvsena42/echo/ui/decks/DecksScreen.kt b/composeApp/src/androidMain/kotlin/com/github/jvsena42/echo/ui/decks/DecksScreen.kt index 750b327..cf291aa 100644 --- a/composeApp/src/androidMain/kotlin/com/github/jvsena42/echo/ui/decks/DecksScreen.kt +++ b/composeApp/src/androidMain/kotlin/com/github/jvsena42/echo/ui/decks/DecksScreen.kt @@ -21,7 +21,6 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.filled.Search -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -50,6 +49,7 @@ import com.github.jvsena42.echo.presentation.decks.DecksLibraryEffect import com.github.jvsena42.echo.presentation.decks.DecksLibraryUiState import com.github.jvsena42.echo.presentation.decks.DecksLibraryViewModel import com.github.jvsena42.echo.ui.components.DeckTile +import com.github.jvsena42.echo.ui.components.EchoLoadingScreen import com.github.jvsena42.echo.ui.components.EchoPrimaryButton import com.github.jvsena42.echo.ui.theme.EchoTheme import kotlinx.coroutines.flow.collectLatest @@ -100,21 +100,29 @@ fun DecksScreen( onRetry: () -> Unit, ) { val colors = EchoTheme.colors - PullToRefreshBox( - isRefreshing = state is DecksLibraryUiState.Loading, - onRefresh = onRetry, + Box( modifier = Modifier .fillMaxSize() .background(colors.surfacePrimary) .windowInsetsPadding(WindowInsets.statusBars), ) { - DecksScreenContent( - state = state, - onDeckClick = onDeckClick, - onImportClick = onImportClick, - onCreateDeckClick = onCreateDeckClick, - onRetry = onRetry, - ) + if (state is DecksLibraryUiState.Loading) { + EchoLoadingScreen(message = stringResource(R.string.decks_loading)) + } else { + PullToRefreshBox( + isRefreshing = false, + onRefresh = onRetry, + modifier = Modifier.fillMaxSize(), + ) { + DecksScreenContent( + state = state, + onDeckClick = onDeckClick, + onImportClick = onImportClick, + onCreateDeckClick = onCreateDeckClick, + onRetry = onRetry, + ) + } + } } } @@ -137,7 +145,7 @@ private fun DecksScreenContent( PasteCtaCard(onClick = onImportClick) when (state) { - DecksLibraryUiState.Loading -> LoadingBlock() + DecksLibraryUiState.Loading -> Unit DecksLibraryUiState.Empty -> EmptyBlock(onCreateDeckClick = onCreateDeckClick) is DecksLibraryUiState.Content -> { SectionHeader(deckCount = state.deckCount) @@ -290,18 +298,6 @@ private fun DeckGrid(decks: List, onDeckClick: (String) -> Unit) } } -@Composable -private fun LoadingBlock() { - val colors = EchoTheme.colors - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - CircularProgressIndicator(color = colors.accentPrimary) - } -} - @Composable private fun EmptyBlock(onCreateDeckClick: () -> Unit) { val colors = EchoTheme.colors diff --git a/composeApp/src/androidMain/kotlin/com/github/jvsena42/echo/ui/discover/DiscoverScreen.kt b/composeApp/src/androidMain/kotlin/com/github/jvsena42/echo/ui/discover/DiscoverScreen.kt index 60a8993..0ac3155 100644 --- a/composeApp/src/androidMain/kotlin/com/github/jvsena42/echo/ui/discover/DiscoverScreen.kt +++ b/composeApp/src/androidMain/kotlin/com/github/jvsena42/echo/ui/discover/DiscoverScreen.kt @@ -24,7 +24,6 @@ import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.ModalBottomSheet @@ -60,6 +59,7 @@ import com.github.jvsena42.echo.presentation.discover.DiscoverEffect import com.github.jvsena42.echo.presentation.discover.DiscoverUiState import com.github.jvsena42.echo.presentation.discover.DiscoverViewModel import com.github.jvsena42.echo.ui.components.DeckTile +import com.github.jvsena42.echo.ui.components.EchoLoadingScreen import com.github.jvsena42.echo.ui.components.TagChip import com.github.jvsena42.echo.ui.theme.EchoTheme import kotlinx.coroutines.flow.collectLatest @@ -119,22 +119,30 @@ private fun DiscoverScreen( onRetry: () -> Unit, ) { val colors = EchoTheme.colors - PullToRefreshBox( - isRefreshing = state is DiscoverUiState.Loading, - onRefresh = onRetry, + Box( modifier = Modifier .fillMaxSize() .background(colors.surfacePrimary) .windowInsetsPadding(WindowInsets.statusBars), ) { - DiscoverScreenContent( - state = state, - onTagSelected = onTagSelected, - onAddFriend = onAddFriend, - onOpenAuthor = onOpenAuthor, - onOpenDeck = onOpenDeck, - onRetry = onRetry, - ) + if (state is DiscoverUiState.Loading) { + EchoLoadingScreen(message = stringResource(R.string.discover_loading)) + } else { + PullToRefreshBox( + isRefreshing = false, + onRefresh = onRetry, + modifier = Modifier.fillMaxSize(), + ) { + DiscoverScreenContent( + state = state, + onTagSelected = onTagSelected, + onAddFriend = onAddFriend, + onOpenAuthor = onOpenAuthor, + onOpenDeck = onOpenDeck, + onRetry = onRetry, + ) + } + } } } @@ -157,7 +165,7 @@ private fun DiscoverScreenContent( HeaderRow(onAddFriend = onAddFriend) when (state) { - DiscoverUiState.Loading -> LoadingBlock() + DiscoverUiState.Loading -> Unit DiscoverUiState.Empty -> EmptyBlock(onAddFriend = onAddFriend) is DiscoverUiState.Content -> { if (state.tags.isNotEmpty()) { @@ -329,17 +337,6 @@ private fun DeckGrid( } } -@Composable -private fun LoadingBlock() { - val colors = EchoTheme.colors - Column( - modifier = Modifier.fillMaxSize().padding(top = 80.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - CircularProgressIndicator(color = colors.accentPrimary) - } -} - @Composable private fun EmptyBlock(onAddFriend: () -> Unit) { val colors = EchoTheme.colors diff --git a/composeApp/src/androidMain/kotlin/com/github/jvsena42/echo/ui/home/HomeScreen.kt b/composeApp/src/androidMain/kotlin/com/github/jvsena42/echo/ui/home/HomeScreen.kt index 9173d5e..7b77ca0 100644 --- a/composeApp/src/androidMain/kotlin/com/github/jvsena42/echo/ui/home/HomeScreen.kt +++ b/composeApp/src/androidMain/kotlin/com/github/jvsena42/echo/ui/home/HomeScreen.kt @@ -2,6 +2,7 @@ package com.github.jvsena42.echo.ui.home import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets @@ -11,7 +12,6 @@ import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text import androidx.compose.material3.pulltorefresh.PullToRefreshBox @@ -33,6 +33,7 @@ import com.github.jvsena42.echo.presentation.home.DeckSummary import com.github.jvsena42.echo.presentation.home.HomeEffect import com.github.jvsena42.echo.presentation.home.HomeUiState import com.github.jvsena42.echo.presentation.home.HomeViewModel +import com.github.jvsena42.echo.ui.components.EchoLoadingScreen import com.github.jvsena42.echo.ui.theme.EchoTheme import kotlinx.coroutines.flow.collectLatest import org.koin.compose.koinInject @@ -90,22 +91,30 @@ fun HomeScreen( onRetry: () -> Unit, ) { val colors = EchoTheme.colors - PullToRefreshBox( - isRefreshing = state is HomeUiState.Loading, - onRefresh = onRetry, + Box( modifier = Modifier .fillMaxSize() .background(colors.surfacePrimary) .windowInsetsPadding(WindowInsets.statusBars), ) { - HomeScreenContent( - state = state, - onStartStudyClick = onStartStudyClick, - onCreateDeckClick = onCreateDeckClick, - onBrowseExamplesClick = onBrowseExamplesClick, - onDeckClick = onDeckClick, - onRetry = onRetry, - ) + if (state is HomeUiState.Loading) { + EchoLoadingScreen(message = stringResource(R.string.home_loading)) + } else { + PullToRefreshBox( + isRefreshing = false, + onRefresh = onRetry, + modifier = Modifier.fillMaxSize(), + ) { + HomeScreenContent( + state = state, + onStartStudyClick = onStartStudyClick, + onCreateDeckClick = onCreateDeckClick, + onBrowseExamplesClick = onBrowseExamplesClick, + onDeckClick = onDeckClick, + onRetry = onRetry, + ) + } + } } } @@ -126,7 +135,7 @@ private fun HomeScreenContent( verticalArrangement = Arrangement.spacedBy(24.dp), ) { when (state) { - HomeUiState.Loading -> LoadingBlock() + HomeUiState.Loading -> Unit is HomeUiState.Empty -> { GreetingHeader(name = state.greetingName) HomeEmptyContent( @@ -150,18 +159,6 @@ private fun HomeScreenContent( } } -@Composable -private fun LoadingBlock() { - val colors = EchoTheme.colors - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - CircularProgressIndicator(color = colors.accentPrimary) - } -} - @Composable private fun ErrorBlock(message: String, onRetry: () -> Unit) { val colors = EchoTheme.colors diff --git a/composeApp/src/androidMain/kotlin/com/github/jvsena42/echo/ui/profile/FriendProfileScreen.kt b/composeApp/src/androidMain/kotlin/com/github/jvsena42/echo/ui/profile/FriendProfileScreen.kt index 399623d..07d1307 100644 --- a/composeApp/src/androidMain/kotlin/com/github/jvsena42/echo/ui/profile/FriendProfileScreen.kt +++ b/composeApp/src/androidMain/kotlin/com/github/jvsena42/echo/ui/profile/FriendProfileScreen.kt @@ -21,7 +21,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -48,6 +47,7 @@ import com.github.jvsena42.echo.presentation.profile.FriendProfileUiState import com.github.jvsena42.echo.presentation.profile.FriendProfileViewModel import com.github.jvsena42.echo.ui.components.AuthorRow import com.github.jvsena42.echo.ui.components.DeckTile +import com.github.jvsena42.echo.ui.components.EchoLoadingScreen import com.github.jvsena42.echo.ui.theme.EchoTheme import kotlinx.coroutines.flow.collectLatest import org.koin.compose.koinInject @@ -98,9 +98,8 @@ private fun FriendProfileScreen( if (state.isLoading) { Box( modifier = Modifier.fillMaxSize().background(colors.surfacePrimary), - contentAlignment = Alignment.Center, ) { - CircularProgressIndicator(color = colors.accentPrimary) + EchoLoadingScreen(message = stringResource(R.string.profile_loading)) } return } diff --git a/composeApp/src/androidMain/kotlin/com/github/jvsena42/echo/ui/profile/ProfileScreen.kt b/composeApp/src/androidMain/kotlin/com/github/jvsena42/echo/ui/profile/ProfileScreen.kt index f1532a8..f3547d6 100644 --- a/composeApp/src/androidMain/kotlin/com/github/jvsena42/echo/ui/profile/ProfileScreen.kt +++ b/composeApp/src/androidMain/kotlin/com/github/jvsena42/echo/ui/profile/ProfileScreen.kt @@ -26,7 +26,6 @@ import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.AssistChip import androidx.compose.material3.AssistChipDefaults import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon @@ -66,6 +65,7 @@ import com.github.jvsena42.echo.R import com.github.jvsena42.echo.presentation.profile.ProfileEffect import com.github.jvsena42.echo.presentation.profile.ProfileUiState import com.github.jvsena42.echo.presentation.profile.ProfileViewModel +import com.github.jvsena42.echo.ui.components.EchoLoadingScreen import com.github.jvsena42.echo.ui.components.EchoPrimaryButton import com.github.jvsena42.echo.ui.theme.EchoTheme import kotlinx.coroutines.flow.collectLatest @@ -130,9 +130,8 @@ private fun ProfileScreen( modifier = Modifier .fillMaxSize() .background(colors.surfacePrimary), - contentAlignment = Alignment.Center, ) { - CircularProgressIndicator(color = colors.accentPrimary) + EchoLoadingScreen(message = stringResource(R.string.profile_loading)) } return } diff --git a/composeApp/src/androidMain/kotlin/com/github/jvsena42/echo/ui/settings/SettingsScreen.kt b/composeApp/src/androidMain/kotlin/com/github/jvsena42/echo/ui/settings/SettingsScreen.kt index 784dd2e..3d1c15b 100644 --- a/composeApp/src/androidMain/kotlin/com/github/jvsena42/echo/ui/settings/SettingsScreen.kt +++ b/composeApp/src/androidMain/kotlin/com/github/jvsena42/echo/ui/settings/SettingsScreen.kt @@ -23,7 +23,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.AlertDialog import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.FilledIconButton import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.HorizontalDivider @@ -57,6 +56,7 @@ import com.github.jvsena42.echo.R import com.github.jvsena42.echo.presentation.settings.SettingsEffect import com.github.jvsena42.echo.presentation.settings.SettingsUiState import com.github.jvsena42.echo.presentation.settings.SettingsViewModel +import com.github.jvsena42.echo.ui.components.EchoLoadingScreen import com.github.jvsena42.echo.ui.theme.EchoTheme import kotlinx.coroutines.flow.collectLatest import org.koin.compose.koinInject @@ -112,9 +112,8 @@ private fun SettingsScreen( modifier = Modifier .fillMaxSize() .background(colors.surfacePrimary), - contentAlignment = Alignment.Center, ) { - CircularProgressIndicator(color = colors.accentPrimary) + EchoLoadingScreen(message = stringResource(R.string.settings_loading)) } return } diff --git a/composeApp/src/androidMain/kotlin/com/github/jvsena42/echo/ui/study/StudySessionScreen.kt b/composeApp/src/androidMain/kotlin/com/github/jvsena42/echo/ui/study/StudySessionScreen.kt index 051e650..63f363a 100644 --- a/composeApp/src/androidMain/kotlin/com/github/jvsena42/echo/ui/study/StudySessionScreen.kt +++ b/composeApp/src/androidMain/kotlin/com/github/jvsena42/echo/ui/study/StudySessionScreen.kt @@ -1,6 +1,5 @@ package com.github.jvsena42.echo.ui.study -import android.provider.Settings import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.snap @@ -32,7 +31,6 @@ import androidx.compose.material.icons.filled.Autorenew import androidx.compose.material.icons.filled.Close import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.FilledIconButton import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon @@ -52,7 +50,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -69,6 +66,8 @@ import com.github.jvsena42.echo.platform.Speaker import com.github.jvsena42.echo.presentation.study.StudySessionEffect import com.github.jvsena42.echo.presentation.study.StudySessionUiState import com.github.jvsena42.echo.presentation.study.StudySessionViewModel +import com.github.jvsena42.echo.ui.components.EchoLoadingScreen +import com.github.jvsena42.echo.ui.components.rememberReduceMotion import com.github.jvsena42.echo.ui.theme.EchoTheme import kotlinx.coroutines.flow.collectLatest import org.koin.compose.koinInject @@ -126,9 +125,8 @@ fun StudySessionScreen( .padding(horizontal = 20.dp, vertical = 8.dp), ) { when (state) { - StudySessionUiState.Loading -> CircularProgressIndicator( - color = colors.accentPrimary, - modifier = Modifier.align(Alignment.Center), + StudySessionUiState.Loading -> EchoLoadingScreen( + message = stringResource(R.string.study_loading), ) is StudySessionUiState.Error -> CenteredMessage( @@ -316,22 +314,6 @@ private fun ReviewingContent( } } -/** - * Reads the OS animation scale; returns true when animations are disabled - * ("Remove animations" / Reduce Motion), so the flip swaps faces instantly. - */ -@Composable -private fun rememberReduceMotion(): Boolean { - val context = LocalContext.current - return remember(context) { - Settings.Global.getFloat( - context.contentResolver, - Settings.Global.ANIMATOR_DURATION_SCALE, - 1f, - ) == 0f - } -} - @Composable private fun CardFace( label: String?, diff --git a/composeApp/src/androidMain/res/values/strings.xml b/composeApp/src/androidMain/res/values/strings.xml index 9c835e6..54157aa 100644 --- a/composeApp/src/androidMain/res/values/strings.xml +++ b/composeApp/src/androidMain/res/values/strings.xml @@ -6,6 +6,7 @@ %1$s 👋 Something went wrong Retry + Warming up… DUE TODAY cards to review @@ -31,11 +32,13 @@ Create your first deck or paste a list to get started. Create a deck Something went wrong + Shuffling your decks… Retry Something went wrong Retry + Opening deck… Start studying · %1$d due Study this deck Back @@ -92,6 +95,7 @@ Something went wrong Close + Dealing your cards… You\'re all caught up 🎉 No cards due right now. No cards due in %1$s right now. @@ -116,6 +120,7 @@ Add a friend by their pubky to follow them and discover their public decks. Add a friend Something went wrong + Finding decks to explore… Retry Paste a pubky or scan their QR code to view their profile and follow them. Paste a pubky @@ -185,6 +190,7 @@ Profile + Loading profile… Settings pk:%1$s pk:%1$s…%2$s @@ -212,6 +218,7 @@ Back Settings + Loading settings… IDENTITY Pubky pk:%1$s…%2$s