diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/imports/ImportScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/imports/ImportScreen.kt index cf72b82..0eb0140 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/imports/ImportScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/imports/ImportScreen.kt @@ -1,15 +1,27 @@ package de.jeanlucmakiola.calendula.ui.imports import android.net.Uri +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +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.RowScope +import androidx.compose.foundation.layout.Spacer 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.size import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Close import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator @@ -18,6 +30,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults @@ -30,6 +43,11 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment 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.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -139,41 +157,122 @@ private fun ManyContent(state: ImportUiState.Many, onImport: (Long) -> Unit) { @Composable private fun DoneContent(state: ImportUiState.Done, onClose: () -> Unit) { + // A little expressive pop on the success badge — springs in on first show. + val badgeScale = remember { Animatable(0.7f) } + LaunchedEffect(Unit) { + badgeScale.animateTo( + targetValue = 1f, + animationSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessLow, + ), + ) + } + Column( Modifier.fillMaxSize().padding(24.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { + Spacer(Modifier.weight(1f)) + Box( + Modifier + .size(112.dp) + .graphicsLayer { + scaleX = badgeScale.value + scaleY = badgeScale.value + } + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primaryContainer), + contentAlignment = Alignment.Center, + ) { + Icon( + Icons.Default.Check, + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier.size(56.dp), + ) + } + Spacer(Modifier.height(24.dp)) Text( stringResource(R.string.import_done_title), style = MaterialTheme.typography.headlineSmall, - modifier = Modifier.padding(top = 24.dp), + color = MaterialTheme.colorScheme.onSurface, ) - Text( - pluralStringResource( - R.plurals.import_done_imported, - state.summary.imported, - state.summary.imported, - ), - style = MaterialTheme.typography.bodyLarge, - ) - if (state.summary.skippedDuplicate > 0) { - Text( - pluralStringResource( - R.plurals.import_done_skipped, - state.summary.skippedDuplicate, - state.summary.skippedDuplicate, + Spacer(Modifier.height(24.dp)) + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + ImportStatCard( + count = state.summary.imported, + label = stringResource(R.string.import_done_added_label), + contentDescription = pluralStringResource( + R.plurals.import_done_imported, + state.summary.imported, + state.summary.imported, ), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, + container = MaterialTheme.colorScheme.secondaryContainer, + onContainer = MaterialTheme.colorScheme.onSecondaryContainer, ) + if (state.summary.skippedDuplicate > 0) { + ImportStatCard( + count = state.summary.skippedDuplicate, + label = stringResource(R.string.import_done_skipped_label), + contentDescription = pluralStringResource( + R.plurals.import_done_skipped, + state.summary.skippedDuplicate, + state.summary.skippedDuplicate, + ), + container = MaterialTheme.colorScheme.surfaceContainerHighest, + onContainer = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } } - Button(onClick = onClose, modifier = Modifier.padding(top = 12.dp)) { + Spacer(Modifier.weight(1f)) + Button( + onClick = onClose, + modifier = Modifier.fillMaxWidth(), + ) { Text(stringResource(R.string.import_close)) } } } +/** A big-number tonal tile summarising one import outcome (added / skipped). */ +@Composable +private fun RowScope.ImportStatCard( + count: Int, + label: String, + contentDescription: String, + container: Color, + onContainer: Color, +) { + Surface( + modifier = Modifier + .weight(1f) + .clearAndSetSemantics { this.contentDescription = contentDescription }, + shape = RoundedCornerShape(24.dp), + color = container, + ) { + Column( + Modifier.padding(vertical = 20.dp, horizontal = 12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + count.toString(), + style = MaterialTheme.typography.displaySmall, + color = onContainer, + ) + Text( + label, + style = MaterialTheme.typography.labelLarge, + color = onContainer.copy(alpha = 0.85f), + ) + } + } +} + @Composable private fun WarningText(warning: IcsParseWarning) { val text = when (warning) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d3272e7..c5ff303 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -462,6 +462,8 @@ Couldn\'t read this file. No writable calendar to import into. Create a local calendar first. Import complete + Added + Duplicates Close Some changed occurrences of recurring events were skipped. An event without a start time was skipped.