Implement a StageIndicator to visualize vocabulary learning progress and refine the VocabularyCard UI.
This commit is contained in:
@@ -101,7 +101,7 @@ fun DailyReviewScreen(
|
|||||||
state = listState,
|
state = listState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
contentPadding = PaddingValues(16.dp)
|
contentPadding = PaddingValues(16.dp)
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
|
|||||||
@@ -33,11 +33,13 @@ import androidx.compose.material.icons.filled.Add
|
|||||||
import androidx.compose.material.icons.filled.AddCircleOutline
|
import androidx.compose.material.icons.filled.AddCircleOutline
|
||||||
import androidx.compose.material.icons.filled.CheckCircle
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
import androidx.compose.material.icons.filled.LocalMall
|
import androidx.compose.material.icons.filled.LocalMall
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
|
||||||
import androidx.compose.material.icons.filled.Search
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.material.icons.filled.Tune
|
import androidx.compose.material.icons.filled.Tune
|
||||||
|
import androidx.compose.material.icons.rounded.Check
|
||||||
|
import androidx.compose.material.icons.rounded.Star
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@@ -58,6 +60,7 @@ import androidx.compose.ui.draw.drawBehind
|
|||||||
import androidx.compose.ui.geometry.CornerRadius
|
import androidx.compose.ui.geometry.CornerRadius
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.PathEffect
|
import androidx.compose.ui.graphics.PathEffect
|
||||||
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -70,6 +73,8 @@ import eu.gaudian.translator.R
|
|||||||
import eu.gaudian.translator.model.Language
|
import eu.gaudian.translator.model.Language
|
||||||
import eu.gaudian.translator.model.VocabularyCategory
|
import eu.gaudian.translator.model.VocabularyCategory
|
||||||
import eu.gaudian.translator.model.VocabularyItem
|
import eu.gaudian.translator.model.VocabularyItem
|
||||||
|
import eu.gaudian.translator.model.VocabularyStage
|
||||||
|
import eu.gaudian.translator.view.composable.AppCard
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
import eu.gaudian.translator.view.composable.AppIcons
|
||||||
import eu.gaudian.translator.view.composable.insertBreakOpportunities
|
import eu.gaudian.translator.view.composable.insertBreakOpportunities
|
||||||
|
|
||||||
@@ -127,22 +132,29 @@ fun SelectionTopBar(
|
|||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
// 1. Close Button
|
||||||
IconButton(onClick = onCloseClick) {
|
IconButton(onClick = onCloseClick) {
|
||||||
Icon(imageVector = AppIcons.Close, contentDescription = stringResource(R.string.label_close_selection_mode))
|
Icon(
|
||||||
}
|
imageVector = AppIcons.Close,
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
contentDescription = stringResource(R.string.label_close_selection_mode)
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.d_selected, selectionCount),
|
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
|
||||||
|
// 2. Title Text (Gets weight to prevent pushing icons off-screen)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.d_selected, selectionCount),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
|
||||||
|
// 3. Action Icons Group
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
IconButton(onClick = onSelectAllClick) {
|
IconButton(onClick = onSelectAllClick) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = AppIcons.SelectAll,
|
imageVector = AppIcons.SelectAll,
|
||||||
@@ -320,6 +332,7 @@ fun AllCardsView(
|
|||||||
vocabularyItems: List<VocabularyItem>,
|
vocabularyItems: List<VocabularyItem>,
|
||||||
allLanguages: List<Language>,
|
allLanguages: List<Language>,
|
||||||
selection: Set<Long>,
|
selection: Set<Long>,
|
||||||
|
stageMapping: Map<Int, VocabularyStage> = emptyMap(),
|
||||||
onItemClick: (VocabularyItem) -> Unit,
|
onItemClick: (VocabularyItem) -> Unit,
|
||||||
onItemLongClick: (VocabularyItem) -> Unit,
|
onItemLongClick: (VocabularyItem) -> Unit,
|
||||||
onDeleteClick: (VocabularyItem) -> Unit,
|
onDeleteClick: (VocabularyItem) -> Unit,
|
||||||
@@ -350,7 +363,7 @@ fun AllCardsView(
|
|||||||
} else {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = listState,
|
state = listState,
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize(),
|
||||||
contentPadding = PaddingValues(bottom = 100.dp)
|
contentPadding = PaddingValues(bottom = 100.dp)
|
||||||
) {
|
) {
|
||||||
@@ -359,10 +372,12 @@ fun AllCardsView(
|
|||||||
key = { it.id }
|
key = { it.id }
|
||||||
) { item ->
|
) { item ->
|
||||||
val isSelected = selection.contains(item.id.toLong())
|
val isSelected = selection.contains(item.id.toLong())
|
||||||
|
val stage = stageMapping[item.id] ?: VocabularyStage.NEW
|
||||||
VocabularyCard(
|
VocabularyCard(
|
||||||
item = item,
|
item = item,
|
||||||
allLanguages = allLanguages,
|
allLanguages = allLanguages,
|
||||||
isSelected = isSelected,
|
isSelected = isSelected,
|
||||||
|
stage = stage,
|
||||||
onItemClick = { onItemClick(item) },
|
onItemClick = { onItemClick(item) },
|
||||||
onItemLongClick = { onItemLongClick(item) },
|
onItemLongClick = { onItemLongClick(item) },
|
||||||
onDeleteClick = { onDeleteClick(item) }
|
onDeleteClick = { onDeleteClick(item) }
|
||||||
@@ -372,14 +387,12 @@ fun AllCardsView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Individual vocabulary card component
|
|
||||||
*/
|
|
||||||
@Composable
|
@Composable
|
||||||
fun VocabularyCard(
|
fun VocabularyCard(
|
||||||
item: VocabularyItem,
|
item: VocabularyItem,
|
||||||
allLanguages: List<Language>,
|
allLanguages: List<Language>,
|
||||||
isSelected: Boolean,
|
isSelected: Boolean,
|
||||||
|
stage: VocabularyStage = VocabularyStage.NEW,
|
||||||
onItemClick: () -> Unit,
|
onItemClick: () -> Unit,
|
||||||
onItemLongClick: () -> Unit,
|
onItemLongClick: () -> Unit,
|
||||||
onDeleteClick: () -> Unit,
|
onDeleteClick: () -> Unit,
|
||||||
@@ -392,14 +405,15 @@ fun VocabularyCard(
|
|||||||
Card(
|
Card(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(RoundedCornerShape(8.dp))
|
.clip(RoundedCornerShape(12.dp)) // Slightly rounder for a modern look
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onClick = onItemClick,
|
onClick = onItemClick,
|
||||||
onLongClick = onItemLongClick
|
onLongClick = onItemLongClick
|
||||||
),
|
),
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(
|
||||||
containerColor = if (isSelected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceContainer,
|
containerColor = if (isSelected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
||||||
contentColor = if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.surfaceContainer
|
// Fixed the contentColor bug here:
|
||||||
|
contentColor = if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurface
|
||||||
),
|
),
|
||||||
border = if (isSelected) BorderStroke(2.dp, MaterialTheme.colorScheme.primary) else null
|
border = if (isSelected) BorderStroke(2.dp, MaterialTheme.colorScheme.primary) else null
|
||||||
) {
|
) {
|
||||||
@@ -410,52 +424,48 @@ fun VocabularyCard(
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(end = 12.dp) // Ensures text doesn't bleed into the trailing icon
|
||||||
|
) {
|
||||||
// Top row: First word + Language Pill
|
// Top row: First word + Language Pill
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text(
|
Text(
|
||||||
text = insertBreakOpportunities(item.wordFirst),
|
text = insertBreakOpportunities(item.wordFirst),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
// This modifier allows the text to wrap without squishing the pill
|
||||||
|
modifier = Modifier.weight(1f, fill = false)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Surface(
|
LanguagePill(
|
||||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f),
|
|
||||||
shape = RoundedCornerShape(4.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = langFirst,
|
text = langFirst,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
backgroundColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f),
|
||||||
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
|
textColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(6.dp)) // Slightly more breathing room
|
||||||
|
|
||||||
// Bottom row: Second word + Language Pill
|
// Bottom row: Second word + Language Pill
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text(
|
Text(
|
||||||
text = insertBreakOpportunities(item.wordSecond),
|
text = insertBreakOpportunities(item.wordSecond),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
// Applied to the second text as well for consistency
|
||||||
|
modifier = Modifier.weight(1f, fill = false)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Surface(
|
LanguagePill(
|
||||||
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.15f),
|
|
||||||
shape = RoundedCornerShape(8.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = langSecond,
|
text = langSecond,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.15f),
|
||||||
color = MaterialTheme.colorScheme.primary,
|
textColor = MaterialTheme.colorScheme.primary
|
||||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -464,15 +474,121 @@ fun VocabularyCard(
|
|||||||
tint = MaterialTheme.colorScheme.primary
|
tint = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
IconButton(onClick = { /* Options menu could go here */ }) {
|
// Stage indicator showing the vocabulary item's learning stage
|
||||||
|
StageIndicator(stage = stage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StageIndicator(
|
||||||
|
stage: VocabularyStage,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
// Convert VocabularyStage to a step number (0-6)
|
||||||
|
val step = when (stage) {
|
||||||
|
VocabularyStage.NEW -> 0
|
||||||
|
VocabularyStage.STAGE_1 -> 1
|
||||||
|
VocabularyStage.STAGE_2 -> 2
|
||||||
|
VocabularyStage.STAGE_3 -> 3
|
||||||
|
VocabularyStage.STAGE_4 -> 4
|
||||||
|
VocabularyStage.STAGE_5 -> 5
|
||||||
|
VocabularyStage.LEARNED -> 6
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Calculate how full the ring should be (0.0 to 1.0)
|
||||||
|
val maxSteps = 6f
|
||||||
|
val progress = step / maxSteps
|
||||||
|
|
||||||
|
// 2. Determine the ring color based on the stage
|
||||||
|
val indicatorColor = when (stage) {
|
||||||
|
VocabularyStage.NEW -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.2f)
|
||||||
|
VocabularyStage.STAGE_1, VocabularyStage.STAGE_2 -> Color(0xFFE57373) // Soft Red
|
||||||
|
VocabularyStage.STAGE_3 -> Color(0xFFFFB74D) // Soft Orange
|
||||||
|
VocabularyStage.STAGE_4 -> Color(0xFFFFD54F) // Soft Yellow
|
||||||
|
VocabularyStage.STAGE_5 -> Color(0xFFAED581) // Light Green
|
||||||
|
VocabularyStage.LEARNED -> Color(0xFF81C784) // Solid Green
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = modifier.size(36.dp) // Keeps it neatly sized within the row
|
||||||
|
) {
|
||||||
|
// The background track (empty ring)
|
||||||
|
CircularProgressIndicator(
|
||||||
|
progress = { 1f },
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f),
|
||||||
|
strokeWidth = 3.dp,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
|
||||||
|
// The colored progress ring
|
||||||
|
if (stage != VocabularyStage.NEW) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
progress = { progress },
|
||||||
|
color = indicatorColor,
|
||||||
|
strokeWidth = 3.dp,
|
||||||
|
strokeCap = StrokeCap.Round, // Gives the progress bar nice rounded ends
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The center content (Number or Icon)
|
||||||
|
when (stage) {
|
||||||
|
VocabularyStage.NEW -> {
|
||||||
|
// An empty dot or small icon to denote it's untouched
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.MoreVert,
|
imageVector = Icons.Rounded.Star, // Or any generic 'new' icon
|
||||||
contentDescription = stringResource(R.string.cd_options),
|
contentDescription = "New Word",
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f),
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
VocabularyStage.LEARNED -> {
|
||||||
|
// A checkmark for mastery
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.Check,
|
||||||
|
contentDescription = "Learned",
|
||||||
|
tint = indicatorColor,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// Display the actual level number (1 through 5)
|
||||||
|
Text(
|
||||||
|
text = step.toString(),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// Extracted for consistency and cleaner code
|
||||||
|
@Composable
|
||||||
|
private fun LanguagePill(
|
||||||
|
text: String,
|
||||||
|
backgroundColor: Color,
|
||||||
|
textColor: Color
|
||||||
|
) {
|
||||||
|
if (text.isNotEmpty()) {
|
||||||
|
Surface(
|
||||||
|
color = backgroundColor,
|
||||||
|
shape = RoundedCornerShape(6.dp) // Consistent corner rounding for all pills
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = textColor,
|
||||||
|
// Guaranteed to never wrap awkwardly
|
||||||
|
maxLines = 1,
|
||||||
|
softWrap = false,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -515,13 +631,11 @@ fun CategoryCard(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Card(
|
AppCard(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(140.dp)
|
.height(140.dp)
|
||||||
.clickable(onClick = onClick),
|
.clickable(onClick = onClick),
|
||||||
shape = RoundedCornerShape(16.dp),
|
|
||||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -709,6 +823,7 @@ fun VocabularyCardPreview() {
|
|||||||
),
|
),
|
||||||
allLanguages = emptyList(),
|
allLanguages = emptyList(),
|
||||||
isSelected = false,
|
isSelected = false,
|
||||||
|
stage = VocabularyStage.NEW,
|
||||||
onItemClick = {},
|
onItemClick = {},
|
||||||
onItemLongClick = {},
|
onItemLongClick = {},
|
||||||
onDeleteClick = {}
|
onDeleteClick = {}
|
||||||
@@ -716,6 +831,154 @@ fun VocabularyCardPreview() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("HardCodedStringLiteral")
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun VocabularyCardStage1Preview() {
|
||||||
|
MaterialTheme {
|
||||||
|
VocabularyCard(
|
||||||
|
item = VocabularyItem(
|
||||||
|
id = 2,
|
||||||
|
wordFirst = "Goodbye",
|
||||||
|
wordSecond = "Adiós",
|
||||||
|
languageFirstId = 1,
|
||||||
|
languageSecondId = 2,
|
||||||
|
createdAt = null,
|
||||||
|
features = null,
|
||||||
|
zipfFrequencyFirst = null,
|
||||||
|
zipfFrequencySecond = null
|
||||||
|
),
|
||||||
|
allLanguages = emptyList(),
|
||||||
|
isSelected = false,
|
||||||
|
stage = VocabularyStage.STAGE_1,
|
||||||
|
onItemClick = {},
|
||||||
|
onItemLongClick = {},
|
||||||
|
onDeleteClick = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("HardCodedStringLiteral")
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun VocabularyCardStage3Preview() {
|
||||||
|
MaterialTheme {
|
||||||
|
VocabularyCard(
|
||||||
|
item = VocabularyItem(
|
||||||
|
id = 3,
|
||||||
|
wordFirst = "Thank you",
|
||||||
|
wordSecond = "Gracias",
|
||||||
|
languageFirstId = 1,
|
||||||
|
languageSecondId = 2,
|
||||||
|
createdAt = null,
|
||||||
|
features = null,
|
||||||
|
zipfFrequencyFirst = null,
|
||||||
|
zipfFrequencySecond = null
|
||||||
|
),
|
||||||
|
allLanguages = emptyList(),
|
||||||
|
isSelected = false,
|
||||||
|
stage = VocabularyStage.STAGE_3,
|
||||||
|
onItemClick = {},
|
||||||
|
onItemLongClick = {},
|
||||||
|
onDeleteClick = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("HardCodedStringLiteral")
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun VocabularyCardStage5Preview() {
|
||||||
|
MaterialTheme {
|
||||||
|
VocabularyCard(
|
||||||
|
item = VocabularyItem(
|
||||||
|
id = 4,
|
||||||
|
wordFirst = "Please",
|
||||||
|
wordSecond = "Por favor",
|
||||||
|
languageFirstId = 1,
|
||||||
|
languageSecondId = 2,
|
||||||
|
createdAt = null,
|
||||||
|
features = null,
|
||||||
|
zipfFrequencyFirst = null,
|
||||||
|
zipfFrequencySecond = null
|
||||||
|
),
|
||||||
|
allLanguages = emptyList(),
|
||||||
|
isSelected = false,
|
||||||
|
stage = VocabularyStage.STAGE_5,
|
||||||
|
onItemClick = {},
|
||||||
|
onItemLongClick = {},
|
||||||
|
onDeleteClick = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("HardCodedStringLiteral")
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun VocabularyCardLearnedPreview() {
|
||||||
|
MaterialTheme {
|
||||||
|
VocabularyCard(
|
||||||
|
item = VocabularyItem(
|
||||||
|
id = 5,
|
||||||
|
wordFirst = "Yes",
|
||||||
|
wordSecond = "Sí",
|
||||||
|
languageFirstId = 1,
|
||||||
|
languageSecondId = 2,
|
||||||
|
createdAt = null,
|
||||||
|
features = null,
|
||||||
|
zipfFrequencyFirst = null,
|
||||||
|
zipfFrequencySecond = null
|
||||||
|
),
|
||||||
|
allLanguages = emptyList(),
|
||||||
|
isSelected = false,
|
||||||
|
stage = VocabularyStage.LEARNED,
|
||||||
|
onItemClick = {},
|
||||||
|
onItemLongClick = {},
|
||||||
|
onDeleteClick = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun StageIndicatorNewPreview() {
|
||||||
|
MaterialTheme {
|
||||||
|
StageIndicator(stage = VocabularyStage.NEW)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun StageIndicatorStage1Preview() {
|
||||||
|
MaterialTheme {
|
||||||
|
StageIndicator(stage = VocabularyStage.STAGE_1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun StageIndicatorStage3Preview() {
|
||||||
|
MaterialTheme {
|
||||||
|
StageIndicator(stage = VocabularyStage.STAGE_3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun StageIndicatorStage5Preview() {
|
||||||
|
MaterialTheme {
|
||||||
|
StageIndicator(stage = VocabularyStage.STAGE_5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun StageIndicatorLearnedPreview() {
|
||||||
|
MaterialTheme {
|
||||||
|
StageIndicator(stage = VocabularyStage.LEARNED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("HardCodedStringLiteral")
|
@Suppress("HardCodedStringLiteral")
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ fun LibraryScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val vocabularyItems by vocabularyItemsFlow.collectAsStateWithLifecycle(initialValue = emptyList())
|
val vocabularyItems by vocabularyItemsFlow.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||||
|
val stageMapping by vocabularyViewModel.stageMapping.collectAsStateWithLifecycle(initialValue = emptyMap())
|
||||||
|
|
||||||
// Handle export state
|
// Handle export state
|
||||||
LaunchedEffect(exportState) {
|
LaunchedEffect(exportState) {
|
||||||
@@ -263,6 +264,7 @@ fun LibraryScreen(
|
|||||||
vocabularyItems = vocabularyItems,
|
vocabularyItems = vocabularyItems,
|
||||||
allLanguages = allLanguages,
|
allLanguages = allLanguages,
|
||||||
selection = selection,
|
selection = selection,
|
||||||
|
stageMapping = stageMapping,
|
||||||
listState = lazyListState,
|
listState = lazyListState,
|
||||||
onItemClick = { item ->
|
onItemClick = { item ->
|
||||||
if (isInSelectionMode) {
|
if (isInSelectionMode) {
|
||||||
|
|||||||
@@ -195,15 +195,15 @@ private fun InteractiveLineChart(weeklyStats: List<WeeklyActivityStat>) {
|
|||||||
Offset(i * xSpacing, height - ((stat.answeredRight * animationProgress) / yMax) * height)
|
Offset(i * xSpacing, height - ((stat.answeredRight * animationProgress) / yMax) * height)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path 1: Correct (Bottom, Dashed)
|
// Define Paths
|
||||||
val pathCorrect = Path().apply { smoothCurve(pointsCorrect) }
|
val pathCorrect = Path().apply { smoothCurve(pointsCorrect) }
|
||||||
drawPath(
|
val fillPathCorrect = Path().apply {
|
||||||
path = pathCorrect,
|
smoothCurve(pointsCorrect)
|
||||||
color = colorCorrect,
|
lineTo(width, height)
|
||||||
style = Stroke(width = 6f, pathEffect = PathEffect.dashPathEffect(floatArrayOf(12f, 12f), 0f))
|
lineTo(0f, height)
|
||||||
)
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
// Path 2: Completed (Top, Solid with Fill)
|
|
||||||
val pathCompleted = Path().apply { smoothCurve(pointsCompleted) }
|
val pathCompleted = Path().apply { smoothCurve(pointsCompleted) }
|
||||||
val fillPathCompleted = Path().apply {
|
val fillPathCompleted = Path().apply {
|
||||||
smoothCurve(pointsCompleted)
|
smoothCurve(pointsCompleted)
|
||||||
@@ -212,14 +212,32 @@ private fun InteractiveLineChart(weeklyStats: List<WeeklyActivityStat>) {
|
|||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw semi-transparent fills first
|
||||||
drawPath(
|
drawPath(
|
||||||
path = fillPathCompleted,
|
path = fillPathCompleted,
|
||||||
brush = Brush.verticalGradient(
|
brush = Brush.verticalGradient(
|
||||||
colors = listOf(colorCompleted.copy(alpha = 0.3f), Color.Transparent),
|
colors = listOf(colorCompleted.copy(alpha = 0.25f), Color.Transparent),
|
||||||
startY = 0f,
|
startY = 0f,
|
||||||
endY = height
|
endY = height
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
drawPath(
|
||||||
|
path = fillPathCorrect,
|
||||||
|
brush = Brush.verticalGradient(
|
||||||
|
colors = listOf(colorCorrect.copy(alpha = 0.25f), Color.Transparent),
|
||||||
|
startY = 0f,
|
||||||
|
endY = height
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw solid strokes on top of the fills
|
||||||
|
drawPath(
|
||||||
|
path = pathCorrect,
|
||||||
|
color = colorCorrect,
|
||||||
|
style = Stroke(width = 6f, pathEffect = PathEffect.dashPathEffect(floatArrayOf(12f, 12f), 0f))
|
||||||
|
)
|
||||||
|
|
||||||
drawPath(
|
drawPath(
|
||||||
path = pathCompleted,
|
path = pathCompleted,
|
||||||
color = colorCompleted,
|
color = colorCompleted,
|
||||||
|
|||||||
@@ -669,10 +669,20 @@ private fun PackCard(
|
|||||||
onGetClick: () -> Unit,
|
onGetClick: () -> Unit,
|
||||||
onAddToLibraryClick: () -> Unit,
|
onAddToLibraryClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
languageViewModel: LanguageViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
val info = packState.info
|
val info = packState.info
|
||||||
val gradient = gradientForId(info.id)
|
val gradient = gradientForId(info.id)
|
||||||
|
|
||||||
|
// Get language names from language IDs
|
||||||
|
val languageIds = info.languageIds
|
||||||
|
val firstLanguageName by languageViewModel.getLanguageByIdFlow(languageIds.getOrNull(0)).collectAsState(initial = null)
|
||||||
|
val secondLanguageName by languageViewModel.getLanguageByIdFlow(languageIds.getOrNull(1)).collectAsState(initial = null)
|
||||||
|
|
||||||
|
val languageDisplayText = listOfNotNull(firstLanguageName?.name, secondLanguageName?.name)
|
||||||
|
.joinToString(" ⇆ ")
|
||||||
|
.ifEmpty { info.category }
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -785,7 +795,7 @@ private fun PackCard(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
Text(
|
Text(
|
||||||
text = info.category,
|
text = languageDisplayText,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
maxLines = 1
|
maxLines = 1
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ fun AllCardsListScreen(
|
|||||||
val vocabularyItems: List<VocabularyItem> = itemsToShow.ifEmpty {
|
val vocabularyItems: List<VocabularyItem> = itemsToShow.ifEmpty {
|
||||||
vocabularyItemsFlow.collectAsStateWithLifecycle(initialValue = emptyList()).value
|
vocabularyItemsFlow.collectAsStateWithLifecycle(initialValue = emptyList()).value
|
||||||
}
|
}
|
||||||
|
val stageMapping by vocabularyViewModel.stageMapping.collectAsStateWithLifecycle(initialValue = emptyMap())
|
||||||
|
|
||||||
// Handle export state
|
// Handle export state
|
||||||
LaunchedEffect(exportState) {
|
LaunchedEffect(exportState) {
|
||||||
@@ -298,6 +299,7 @@ fun AllCardsListScreen(
|
|||||||
vocabularyItems = vocabularyItems,
|
vocabularyItems = vocabularyItems,
|
||||||
allLanguages = allLanguages,
|
allLanguages = allLanguages,
|
||||||
selection = selection,
|
selection = selection,
|
||||||
|
stageMapping = stageMapping,
|
||||||
listState = lazyListState,
|
listState = lazyListState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|||||||
Reference in New Issue
Block a user