Coverage Summary for Class: TransactionCreationCategoryGridKt (com.georgeci.moneysurfer.feature.transaction.creation)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| TransactionCreationCategoryGridKt |
100%
(1/1)
|
100%
(3/3)
|
79.2%
(38/48)
|
100%
(120/120)
|
99%
(1136/1148)
|
package com.georgeci.moneysurfer.feature.transaction.creation
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
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.Spacer
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.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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.vector.ImageVector
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.georgeci.moneysurfer.domain.model.Category
import com.georgeci.moneysurfer.uikit.components.SurferCategoryBubble
import com.georgeci.moneysurfer.uikit.components.SurferCategoryPalette
import com.georgeci.moneysurfer.uikit.icons.SurferIcons
import com.georgeci.moneysurfer.uikit.semantics.SurferSemantics
import com.georgeci.moneysurfer.uikit.theme.AppTheme
import moneysurfer.feature.transaction.generated.resources.Res
import moneysurfer.feature.transaction.generated.resources.transaction_creation_category_all
import moneysurfer.feature.transaction.generated.resources.transaction_creation_category_label
import moneysurfer.feature.transaction.generated.resources.transaction_creation_category_more
import org.jetbrains.compose.resources.stringResource
private const val CATEGORY_PREVIEW_SIZE = 7
/** Columns in the tile grid; the last cell of the last row is always "More". */
private const val CATEGORY_GRID_COLUMNS = 4
@Composable
internal fun CategoryGridSection(
categories: List<Category>,
selected: Category?,
onSelect: (Category) -> Unit,
onAllClick: () -> Unit,
onMoreClick: () -> Unit,
) {
Column {
Row(
modifier = Modifier.padding(horizontal = 4.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = stringResource(Res.string.transaction_creation_category_label),
style = AppTheme.typography.labelLarge,
color = AppTheme.materialColors.onSurfaceVariant,
modifier = Modifier.weight(1f),
)
Text(
text = stringResource(Res.string.transaction_creation_category_all),
style = AppTheme.typography.labelMedium,
color = AppTheme.materialColors.primary,
modifier = Modifier.clickable(onClick = onAllClick),
)
}
Spacer(Modifier.height(8.dp))
val preview = categories.take(CATEGORY_PREVIEW_SIZE)
val rows = (preview + listOf<Category?>(null)).chunked(CATEGORY_GRID_COLUMNS)
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
rows.forEach { row ->
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
row.forEach { cat ->
Box(modifier = Modifier.weight(1f)) {
if (cat != null) {
CategoryTile(
category = cat,
selected = cat.id == selected?.id,
onClick = { onSelect(cat) },
)
} else {
MoreTile(onClick = onMoreClick)
}
}
}
// Pad row if last row is shorter than the grid.
if (row.size < CATEGORY_GRID_COLUMNS) {
repeat(CATEGORY_GRID_COLUMNS - row.size) {
Box(modifier = Modifier.weight(1f))
}
}
}
}
}
}
}
@Composable
private fun CategoryTile(
category: Category,
selected: Boolean,
onClick: () -> Unit,
) {
val bg = if (selected) AppTheme.materialColors.secondaryContainer else Color.Transparent
val labelColor =
if (selected) AppTheme.materialColors.onSecondaryContainer else AppTheme.materialColors.onSurface
val visual = SurferCategoryPalette.visualFor(
id = category.id.value,
iconKey = category.iconKey,
hue = category.hue,
systemKind = category.systemKind?.name,
)
val tint = visual.tint
val icon: ImageVector = visual.icon
Column(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(14.dp))
.background(bg)
.clickable(onClick = onClick)
.padding(vertical = 6.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(6.dp),
) {
Box(contentAlignment = Alignment.Center) {
SurferCategoryBubble(icon = icon, tint = tint, size = 44.dp)
if (selected) {
Box(
modifier = Modifier
.align(Alignment.BottomEnd)
.size(18.dp)
.clip(CircleShape)
.background(AppTheme.materialColors.primary)
.border(2.dp, AppTheme.materialColors.surface, CircleShape),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = SurferIcons.Check,
contentDescription = SurferSemantics.Decorative,
tint = AppTheme.materialColors.onPrimary,
modifier = Modifier.size(10.dp),
)
}
}
}
Text(
text = category.name,
style = AppTheme.typography.labelMedium,
color = labelColor,
textAlign = TextAlign.Center,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(horizontal = 2.dp),
)
}
}
@Composable
private fun MoreTile(onClick: () -> Unit) {
val outline = AppTheme.materialColors.outline
Column(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(vertical = 6.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(6.dp),
) {
Box(
modifier = Modifier
.size(44.dp)
.clip(CircleShape)
.border(1.5.dp, outline, CircleShape),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = SurferIcons.Add,
contentDescription = SurferSemantics.Decorative,
tint = AppTheme.materialColors.primary,
modifier = Modifier.size(20.dp),
)
}
Text(
text = stringResource(Res.string.transaction_creation_category_more),
style = AppTheme.typography.labelMedium,
color = AppTheme.materialColors.primary,
textAlign = TextAlign.Center,
)
}
}