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
Original file line number Diff line number Diff line change
@@ -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…")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
)
}
}
}
}

Expand All @@ -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)
Expand Down Expand Up @@ -290,18 +298,6 @@ private fun DeckGrid(decks: List<DeckTileModel>, 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
)
}
}
}
}

Expand All @@ -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()) {
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading