Compare commits
5 Commits
b75f5f32a0
...
95dfd3c7eb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95dfd3c7eb | ||
|
|
d6a9ccf4e3 | ||
|
|
863920143d | ||
|
|
15d03ef57f | ||
|
|
f737657cdb |
@@ -1,4 +1,4 @@
|
|||||||
All vocabulary lists in this section were generated using AI. While I strive for accuracy and quality, please keep in mind that these are machine-generated collections.
|
All vocabulary lists in this section were generated automatically. While I strive for accuracy and quality, please keep in mind that these are machine-generated collections.
|
||||||
|
|
||||||
I'm a single developer building and maintaining this app in my spare time. I'm passionate about creating tools that help people learn languages, but I have limited resources and time.
|
I'm a single developer building and maintaining this app in my spare time. I'm passionate about creating tools that help people learn languages, but I have limited resources and time.
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,10 @@ data class VocabularyItem(
|
|||||||
features = switchedFeaturesJson
|
features = switchedFeaturesJson
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hasFeatures(): Boolean {
|
||||||
|
return !features.isNullOrBlank() && features != "{}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|||||||
@@ -55,7 +55,9 @@ class TranslationService(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun libreTranslate(text: String, source: String?, target: String, alternatives: Int = 1): Result<String> = withContext(Dispatchers.IO) {
|
// Public method to directly use LibreTranslate (bypasses AI)
|
||||||
|
suspend fun libreTranslate(text: String, source: String?, target: String, alternatives: Int = 1): Result<String> = withContext(Dispatchers.IO) {
|
||||||
|
Log.d("libreTranslate: $text, $source, $target")
|
||||||
try {
|
try {
|
||||||
val json = org.json.JSONObject().apply {
|
val json = org.json.JSONObject().apply {
|
||||||
put("q", text)
|
put("q", text)
|
||||||
|
|||||||
@@ -267,7 +267,8 @@ fun TranslatorApp(
|
|||||||
"new_word",
|
"new_word",
|
||||||
"new_word_review",
|
"new_word_review",
|
||||||
"vocabulary_detail/{itemId}",
|
"vocabulary_detail/{itemId}",
|
||||||
"daily_review"
|
"daily_review",
|
||||||
|
"explore_packs"
|
||||||
) || currentRoute?.startsWith("start_exercise") == true
|
) || currentRoute?.startsWith("start_exercise") == true
|
||||||
|| currentRoute?.startsWith("vocabulary_exercise") == true
|
|| currentRoute?.startsWith("vocabulary_exercise") == true
|
||||||
val showBottomNavLabels by settingsViewModel.showBottomNavLabels.collectAsStateWithLifecycle(initialValue = false)
|
val showBottomNavLabels by settingsViewModel.showBottomNavLabels.collectAsStateWithLifecycle(initialValue = false)
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package eu.gaudian.translator.view.composable
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
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.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A compact action card with an icon and label, designed for use in rows or grids.
|
||||||
|
* Used for quick action buttons like "Explore Packs", "Import CSV", etc.
|
||||||
|
*
|
||||||
|
* @param label The text label below the icon
|
||||||
|
* @param icon The icon to display
|
||||||
|
* @param onClick Callback when the card is clicked
|
||||||
|
* @param modifier Modifier for the card
|
||||||
|
* @param height The height of the card (default 120.dp)
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun AppActionCard(
|
||||||
|
label: String,
|
||||||
|
icon: ImageVector,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
height: Dp = 120.dp,
|
||||||
|
iconContainerSize: Dp = 48.dp,
|
||||||
|
iconSize: Dp = 24.dp
|
||||||
|
) {
|
||||||
|
AppCard(
|
||||||
|
modifier = modifier.height(height),
|
||||||
|
onClick = onClick
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
CircularIconContainer(
|
||||||
|
imageVector = icon,
|
||||||
|
size = iconContainerSize,
|
||||||
|
iconSize = iconSize
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A section header label with consistent styling.
|
||||||
|
* Used for section titles like "Recently Added", etc.
|
||||||
|
*
|
||||||
|
* @param text The section title text
|
||||||
|
* @param modifier Modifier for the text
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun SectionLabel(
|
||||||
|
text: String,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A labeled section with an optional action button.
|
||||||
|
* Provides consistent header styling for sections with a title and optional action.
|
||||||
|
*
|
||||||
|
* @param title The section title
|
||||||
|
* @param modifier Modifier for the section header
|
||||||
|
* @param actionLabel Optional label for the action button
|
||||||
|
* @param onActionClick Optional callback for the action button
|
||||||
|
* @param content The content below the header
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun LabeledSection(
|
||||||
|
title: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
actionLabel: String? = null,
|
||||||
|
onActionClick: (() -> Unit)? = null,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
// Header row with title and optional action
|
||||||
|
if (actionLabel != null && onActionClick != null) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
SectionLabel(text = title)
|
||||||
|
androidx.compose.material3.TextButton(onClick = onActionClick) {
|
||||||
|
Text(actionLabel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SectionLabel(text = title)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package eu.gaudian.translator.view.composable
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
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.MaterialTheme
|
||||||
|
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.vector.ImageVector
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A reusable icon container that displays an icon inside a shaped background.
|
||||||
|
* Used throughout the app for consistent icon presentation in cards, buttons, and action items.
|
||||||
|
*
|
||||||
|
* @param imageVector The icon to display
|
||||||
|
* @param modifier Modifier to be applied to the container
|
||||||
|
* @param size The size of the container (default 40.dp)
|
||||||
|
* @param iconSize The size of the icon itself (default 24.dp)
|
||||||
|
* @param shape The shape of the container (default RoundedCornerShape(12.dp))
|
||||||
|
* @param backgroundColor Background color of the container
|
||||||
|
* @param iconTint Tint color for the icon
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun AppIconContainer(
|
||||||
|
imageVector: ImageVector,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
size: Dp = 40.dp,
|
||||||
|
iconSize: Dp = 24.dp,
|
||||||
|
shape: androidx.compose.ui.graphics.Shape = RoundedCornerShape(12.dp),
|
||||||
|
backgroundColor: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
iconTint: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.primary,
|
||||||
|
contentDescription: String? = null
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.size(size)
|
||||||
|
.clip(shape)
|
||||||
|
.background(backgroundColor),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = imageVector,
|
||||||
|
contentDescription = contentDescription,
|
||||||
|
tint = iconTint,
|
||||||
|
modifier = Modifier.size(iconSize)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A circular variant of AppIconContainer.
|
||||||
|
* Convenience wrapper for circular icon containers.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun CircularIconContainer(
|
||||||
|
imageVector: ImageVector,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
size: Dp = 48.dp,
|
||||||
|
iconSize: Dp = 24.dp,
|
||||||
|
backgroundColor: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
iconTint: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.primary,
|
||||||
|
contentDescription: String? = null
|
||||||
|
) {
|
||||||
|
AppIconContainer(
|
||||||
|
imageVector = imageVector,
|
||||||
|
modifier = modifier,
|
||||||
|
size = size,
|
||||||
|
iconSize = iconSize,
|
||||||
|
shape = CircleShape,
|
||||||
|
backgroundColor = backgroundColor,
|
||||||
|
iconTint = iconTint,
|
||||||
|
contentDescription = contentDescription
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -115,7 +115,7 @@ fun AppOutlinedTextField(
|
|||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = value,
|
value = value,
|
||||||
onValueChange = onValueChange,
|
onValueChange = onValueChange,
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier,
|
||||||
label = label,
|
label = label,
|
||||||
trailingIcon = finalTrailingIcon,
|
trailingIcon = finalTrailingIcon,
|
||||||
shape = ComponentDefaults.DefaultShape,
|
shape = ComponentDefaults.DefaultShape,
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package eu.gaudian.translator.view.composable
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextField
|
||||||
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A styled filled text input field.
|
||||||
|
* Different from AppOutlinedTextField - this uses a filled background style.
|
||||||
|
*
|
||||||
|
* @param value The input text to be shown in the text field.
|
||||||
|
* @param onValueChange The callback that is triggered when the input service updates the text.
|
||||||
|
* @param modifier The modifier to be applied to the text field.
|
||||||
|
* @param placeholder The placeholder text to display when the field is empty.
|
||||||
|
* @param enabled Whether the text field is enabled.
|
||||||
|
* @param readOnly Whether the text field is read-only.
|
||||||
|
* @param singleLine Whether the text field is single line.
|
||||||
|
* @param minLines Minimum number of lines.
|
||||||
|
* @param maxLines Maximum number of lines.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun AppTextField(
|
||||||
|
value: String,
|
||||||
|
onValueChange: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
placeholder: String? = null,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
readOnly: Boolean = false,
|
||||||
|
singleLine: Boolean = true,
|
||||||
|
minLines: Int = 1,
|
||||||
|
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
|
||||||
|
leadingIcon: @Composable (() -> Unit)? = null,
|
||||||
|
trailingIcon: @Composable (() -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
val cornerRadius = 12.dp
|
||||||
|
|
||||||
|
TextField(
|
||||||
|
value = value,
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
placeholder = placeholder?.let {
|
||||||
|
{
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
shape = RoundedCornerShape(cornerRadius),
|
||||||
|
colors = TextFieldDefaults.colors(
|
||||||
|
focusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
focusedIndicatorColor = Color.Transparent,
|
||||||
|
unfocusedIndicatorColor = Color.Transparent,
|
||||||
|
cursorColor = MaterialTheme.colorScheme.primary,
|
||||||
|
focusedTextColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
unfocusedTextColor = MaterialTheme.colorScheme.onSurface
|
||||||
|
),
|
||||||
|
singleLine = singleLine,
|
||||||
|
minLines = minLines,
|
||||||
|
maxLines = maxLines,
|
||||||
|
enabled = enabled,
|
||||||
|
readOnly = readOnly,
|
||||||
|
leadingIcon = leadingIcon,
|
||||||
|
trailingIcon = trailingIcon
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -24,6 +23,7 @@ import androidx.core.net.toUri
|
|||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
import eu.gaudian.translator.ui.theme.ThemePreviews
|
||||||
import eu.gaudian.translator.view.composable.AppDialog
|
import eu.gaudian.translator.view.composable.AppDialog
|
||||||
|
import eu.gaudian.translator.view.composable.AppOutlinedTextField
|
||||||
import eu.gaudian.translator.view.composable.AppSlider
|
import eu.gaudian.translator.view.composable.AppSlider
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ fun RequestMorePackDialog(
|
|||||||
style = MaterialTheme.typography.labelLarge,
|
style = MaterialTheme.typography.labelLarge,
|
||||||
fontWeight = FontWeight.SemiBold
|
fontWeight = FontWeight.SemiBold
|
||||||
)
|
)
|
||||||
OutlinedTextField(
|
AppOutlinedTextField(
|
||||||
value = topic,
|
value = topic,
|
||||||
onValueChange = { topic = it },
|
onValueChange = { topic = it },
|
||||||
placeholder = { Text("e.g. Travel, Business, Cooking…") },
|
placeholder = { Text("e.g. Travel, Business, Cooking…") },
|
||||||
@@ -78,20 +78,21 @@ fun RequestMorePackDialog(
|
|||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Row(
|
||||||
OutlinedTextField(
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
AppOutlinedTextField(
|
||||||
value = langFrom,
|
value = langFrom,
|
||||||
onValueChange = { langFrom = it },
|
onValueChange = { langFrom = it },
|
||||||
placeholder = { Text(stringResource(R.string.label_from)) },
|
placeholder = { Text(stringResource(R.string.label_from)) },
|
||||||
label = { Text(stringResource(R.string.label_from)) },
|
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
OutlinedTextField(
|
AppOutlinedTextField(
|
||||||
value = langTo,
|
value = langTo,
|
||||||
onValueChange = { langTo = it },
|
onValueChange = { langTo = it },
|
||||||
placeholder = { Text(stringResource(R.string.label_to)) },
|
placeholder = { Text(stringResource(R.string.label_to)) },
|
||||||
label = { Text(stringResource(R.string.label_to)) },
|
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
@@ -27,7 +29,6 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -47,6 +48,7 @@ import eu.gaudian.translator.R
|
|||||||
import eu.gaudian.translator.utils.findActivity
|
import eu.gaudian.translator.utils.findActivity
|
||||||
import eu.gaudian.translator.view.NavigationRoutes
|
import eu.gaudian.translator.view.NavigationRoutes
|
||||||
import eu.gaudian.translator.view.composable.AppCard
|
import eu.gaudian.translator.view.composable.AppCard
|
||||||
|
import eu.gaudian.translator.view.composable.LabeledSection
|
||||||
import eu.gaudian.translator.view.composable.Screen
|
import eu.gaudian.translator.view.composable.Screen
|
||||||
import eu.gaudian.translator.view.settings.SettingsRoutes
|
import eu.gaudian.translator.view.settings.SettingsRoutes
|
||||||
import eu.gaudian.translator.view.stats.widgets.WeeklyActivityChartWidget
|
import eu.gaudian.translator.view.stats.widgets.WeeklyActivityChartWidget
|
||||||
@@ -356,20 +358,12 @@ fun WeeklyProgressSection(
|
|||||||
val viewModel: ProgressViewModel = hiltViewModel(activity)
|
val viewModel: ProgressViewModel = hiltViewModel(activity)
|
||||||
val weeklyActivityStats by viewModel.weeklyActivityStats.collectAsState()
|
val weeklyActivityStats by viewModel.weeklyActivityStats.collectAsState()
|
||||||
|
|
||||||
Column(modifier = modifier) {
|
LabeledSection(
|
||||||
Row(
|
title = stringResource(R.string.label_weekly_progress),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = modifier,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
actionLabel = stringResource(R.string.label_see_history),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
onActionClick = { navController.navigate(NavigationRoutes.STATS_VOCABULARY_HEATMAP) }
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(R.string.label_weekly_progress), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
|
||||||
TextButton(onClick = { navController.navigate(NavigationRoutes.STATS_VOCABULARY_HEATMAP) }) {
|
|
||||||
Text(stringResource(R.string.label_see_history), softWrap = false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
AppCard(
|
AppCard(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
onClick = { navController.navigate(NavigationRoutes.STATS_VOCABULARY_HEATMAP) }
|
onClick = { navController.navigate(NavigationRoutes.STATS_VOCABULARY_HEATMAP) }
|
||||||
@@ -405,12 +399,16 @@ fun BottomStatsSection(
|
|||||||
val learnedWords by viewModel.totalWordsCompleted.collectAsState()
|
val learnedWords by viewModel.totalWordsCompleted.collectAsState()
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(IntrinsicSize.Min),
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
// Total Words
|
// Total Words
|
||||||
AppCard(
|
AppCard(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxHeight(),
|
||||||
onClick = { navController.navigate(Screen.Library.route) }
|
onClick = { navController.navigate(Screen.Library.route) }
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(20.dp)) {
|
Column(modifier = Modifier.padding(20.dp)) {
|
||||||
@@ -423,7 +421,9 @@ fun BottomStatsSection(
|
|||||||
|
|
||||||
// Learned
|
// Learned
|
||||||
AppCard(
|
AppCard(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxHeight(),
|
||||||
onClick = { navController.navigate(NavigationRoutes.STATS_LANGUAGE_PROGRESS) }
|
onClick = { navController.navigate(NavigationRoutes.STATS_LANGUAGE_PROGRESS) }
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(20.dp)) {
|
Column(modifier = Modifier.padding(20.dp)) {
|
||||||
|
|||||||
@@ -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,50 +424,46 @@ 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),
|
text = langFirst,
|
||||||
shape = RoundedCornerShape(4.dp)
|
backgroundColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f),
|
||||||
) {
|
textColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
Text(
|
)
|
||||||
text = langFirst,
|
|
||||||
style = MaterialTheme.typography.labelSmall,
|
|
||||||
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
|
|
||||||
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),
|
text = langSecond,
|
||||||
shape = RoundedCornerShape(8.dp)
|
backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.15f),
|
||||||
) {
|
textColor = MaterialTheme.colorScheme.primary
|
||||||
Text(
|
)
|
||||||
text = langSecond,
|
|
||||||
style = MaterialTheme.typography.labelSmall,
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,18 +474,124 @@ 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
|
||||||
Icon(
|
StageIndicator(stage = stage)
|
||||||
imageVector = Icons.Default.MoreVert,
|
|
||||||
contentDescription = stringResource(R.string.cd_options),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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(
|
||||||
|
imageVector = Icons.Rounded.Star, // Or any generic 'new' icon
|
||||||
|
contentDescription = "New Word",
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Grid view of categories
|
* Grid view of categories
|
||||||
*/
|
*/
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ fun StatusWidget(
|
|||||||
if (itemsWithoutGrammarCount > 0) {
|
if (itemsWithoutGrammarCount > 0) {
|
||||||
StatusItem(
|
StatusItem(
|
||||||
icon = AppIcons.Error,
|
icon = AppIcons.Error,
|
||||||
text = stringResource(R.string.items_without_grammar_infos),
|
text = stringResource(R.string.label_items_without_grammar),
|
||||||
count = itemsWithoutGrammarCount,
|
count = itemsWithoutGrammarCount,
|
||||||
onClick = onNavigateToNoGrammar,
|
onClick = onNavigateToNoGrammar,
|
||||||
color = MaterialTheme.colorScheme.error
|
color = MaterialTheme.colorScheme.error
|
||||||
|
|||||||
@@ -4,14 +4,15 @@ package eu.gaudian.translator.view.stats.widgets
|
|||||||
|
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.RowScope
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
@@ -31,19 +32,31 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.geometry.CornerRadius
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.Path
|
||||||
|
import androidx.compose.ui.graphics.PathEffect
|
||||||
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.drawText
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.rememberTextMeasurer
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
import eu.gaudian.translator.ui.theme.ThemePreviews
|
||||||
import eu.gaudian.translator.ui.theme.semanticColors
|
|
||||||
import eu.gaudian.translator.viewmodel.WeeklyActivityStat
|
import eu.gaudian.translator.viewmodel.WeeklyActivityStat
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A widget that displays weekly activity statistics in a visually appealing bar chart.
|
* A widget that displays weekly activity statistics in a visually appealing smooth line chart.
|
||||||
* It's designed to be consistent with the app's modern, floating UI style.
|
* It's designed to be consistent with the app's modern UI style using the theme's colors.
|
||||||
*
|
*
|
||||||
* @param weeklyStats A list of [WeeklyActivityStat] for the last 7 days.
|
* @param weeklyStats A list of [WeeklyActivityStat] for the last 7 days.
|
||||||
*/
|
*/
|
||||||
@@ -51,20 +64,15 @@ import kotlinx.coroutines.delay
|
|||||||
fun WeeklyActivityChartWidget(
|
fun WeeklyActivityChartWidget(
|
||||||
weeklyStats: List<WeeklyActivityStat>
|
weeklyStats: List<WeeklyActivityStat>
|
||||||
) {
|
) {
|
||||||
val maxValue = remember(weeklyStats) {
|
|
||||||
(weeklyStats.flatMap { listOf(it.newlyAdded, it.completed, it.answeredRight) }.maxOrNull() ?: 0).let {
|
|
||||||
if (it < 10) 10 else ((it / 5) + 1) * 5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val hasNoData = remember(weeklyStats) {
|
val hasNoData = remember(weeklyStats) {
|
||||||
weeklyStats.all { it.newlyAdded == 0 && it.completed == 0 && it.answeredRight == 0 }
|
weeklyStats.all { it.completed == 0 && it.answeredRight == 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasNoData) {
|
if (hasNoData) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
@@ -77,60 +85,293 @@ fun WeeklyActivityChartWidget(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(8.dp)
|
// Reduced horizontal padding to give the chart more space
|
||||||
|
.padding(vertical = 24.dp, horizontal = 12.dp)
|
||||||
) {
|
) {
|
||||||
WeeklyChartLegend()
|
WeeklyChartLegend()
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
Row(
|
InteractiveLineChart(weeklyStats = weeklyStats)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
ChartFooter(weeklyStats = weeklyStats)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun InteractiveLineChart(weeklyStats: List<WeeklyActivityStat>) {
|
||||||
|
var selectedIndex by remember { mutableStateOf<Int?>(3) } // Default selection
|
||||||
|
val textMeasurer = rememberTextMeasurer()
|
||||||
|
|
||||||
|
val colorCompleted = MaterialTheme.colorScheme.primary
|
||||||
|
val colorCorrect = MaterialTheme.colorScheme.tertiary
|
||||||
|
|
||||||
|
val gridColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
|
||||||
|
val tooltipLineColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.2f)
|
||||||
|
val dotCenterColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
val tooltipBgColor = MaterialTheme.colorScheme.inverseSurface
|
||||||
|
val tooltipTextColor = MaterialTheme.colorScheme.inverseOnSurface
|
||||||
|
|
||||||
|
var startAnimation by remember { mutableStateOf(false) }
|
||||||
|
val animationProgress by animateFloatAsState(
|
||||||
|
targetValue = if (startAnimation) 1f else 0f,
|
||||||
|
animationSpec = tween(durationMillis = 1000),
|
||||||
|
label = "chartAnimation"
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
delay(100)
|
||||||
|
startAnimation = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val yAxisMax = remember(weeklyStats) {
|
||||||
|
val max = weeklyStats.flatMap { listOf(it.completed, it.answeredRight) }.maxOrNull() ?: 0
|
||||||
|
if (max < 10) 10 else ((max / 10) + 1) * 10
|
||||||
|
}
|
||||||
|
val yMax = yAxisMax.toFloat()
|
||||||
|
|
||||||
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
// Left Side: Y-Axis Amounts
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(180.dp)
|
||||||
|
// Reduced end padding to save space
|
||||||
|
.padding(end = 8.dp, top = 2.dp, bottom = 2.dp),
|
||||||
|
verticalArrangement = Arrangement.SpaceBetween,
|
||||||
|
horizontalAlignment = Alignment.End
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = yAxisMax.toString(),
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = (yAxisMax / 2).toString(),
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "0",
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right Side: Chart Area
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(220.dp),
|
.height(180.dp)
|
||||||
verticalAlignment = Alignment.Bottom
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures { offset ->
|
||||||
|
val itemWidth = size.width / (weeklyStats.size - 1).coerceAtLeast(1)
|
||||||
|
selectedIndex = (offset.x / itemWidth).roundToInt().coerceIn(0, weeklyStats.lastIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectHorizontalDragGestures { change, _ ->
|
||||||
|
val itemWidth = size.width / (weeklyStats.size - 1).coerceAtLeast(1)
|
||||||
|
selectedIndex = (change.position.x / itemWidth).roundToInt().coerceIn(0, weeklyStats.lastIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
// Y-Axis Labels
|
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||||
Column(
|
val width = size.width
|
||||||
modifier = Modifier
|
val height = size.height
|
||||||
.fillMaxHeight()
|
val xSpacing = width / (weeklyStats.size - 1).coerceAtLeast(1)
|
||||||
.padding(end = 8.dp),
|
|
||||||
verticalArrangement = Arrangement.SpaceBetween,
|
|
||||||
horizontalAlignment = Alignment.End
|
|
||||||
) {
|
|
||||||
Text(maxValue.toString(), style = MaterialTheme.typography.labelSmall)
|
|
||||||
Text((maxValue / 2).toString(), style = MaterialTheme.typography.labelSmall)
|
|
||||||
Text("0", style = MaterialTheme.typography.labelSmall)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chart Bars
|
drawLine(gridColor, Offset(0f, 0f), Offset(width, 0f), strokeWidth = 2f, pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f)))
|
||||||
Row(
|
drawLine(gridColor, Offset(0f, height / 2f), Offset(width, height / 2f), strokeWidth = 2f, pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f)))
|
||||||
modifier = Modifier
|
drawLine(gridColor, Offset(0f, height), Offset(width, height), strokeWidth = 2f, pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f)))
|
||||||
.weight(1f)
|
|
||||||
.fillMaxHeight(),
|
if (animationProgress == 0f) return@Canvas
|
||||||
horizontalArrangement = Arrangement.SpaceAround,
|
|
||||||
verticalAlignment = Alignment.Bottom
|
val pointsCompleted = weeklyStats.mapIndexed { i, stat ->
|
||||||
) {
|
Offset(i * xSpacing, height - ((stat.completed * animationProgress) / yMax) * height)
|
||||||
weeklyStats.forEach { stat ->
|
}
|
||||||
Column(
|
val pointsCorrect = weeklyStats.mapIndexed { i, stat ->
|
||||||
modifier = Modifier.weight(1f),
|
Offset(i * xSpacing, height - ((stat.answeredRight * animationProgress) / yMax) * height)
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
}
|
||||||
verticalArrangement = Arrangement.Bottom
|
|
||||||
) {
|
// Define Paths
|
||||||
Row(
|
val pathCorrect = Path().apply { smoothCurve(pointsCorrect) }
|
||||||
verticalAlignment = Alignment.Bottom,
|
val fillPathCorrect = Path().apply {
|
||||||
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
smoothCurve(pointsCorrect)
|
||||||
modifier = Modifier
|
lineTo(width, height)
|
||||||
.weight(1f)
|
lineTo(0f, height)
|
||||||
.fillMaxWidth(0.8f)
|
close()
|
||||||
) {
|
}
|
||||||
Bar(value = stat.newlyAdded, maxValue = maxValue, color = MaterialTheme.semanticColors.stageGradient1)
|
|
||||||
Bar(value = stat.completed, maxValue = maxValue, color = MaterialTheme.semanticColors.stageGradient3)
|
val pathCompleted = Path().apply { smoothCurve(pointsCompleted) }
|
||||||
Bar(value = stat.answeredRight, maxValue = maxValue, color = MaterialTheme.semanticColors.stageGradient5)
|
val fillPathCompleted = Path().apply {
|
||||||
|
smoothCurve(pointsCompleted)
|
||||||
|
lineTo(width, height)
|
||||||
|
lineTo(0f, height)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw semi-transparent fills first
|
||||||
|
drawPath(
|
||||||
|
path = fillPathCompleted,
|
||||||
|
brush = Brush.verticalGradient(
|
||||||
|
colors = listOf(colorCompleted.copy(alpha = 0.25f), Color.Transparent),
|
||||||
|
startY = 0f,
|
||||||
|
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(
|
||||||
|
path = pathCompleted,
|
||||||
|
color = colorCompleted,
|
||||||
|
style = Stroke(width = 8f)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Interactive Highlights & Dual Separated Tooltips
|
||||||
|
selectedIndex?.let { index ->
|
||||||
|
val stat = weeklyStats[index]
|
||||||
|
val x = index * xSpacing
|
||||||
|
val yCompleted = height - ((stat.completed * animationProgress) / yMax) * height
|
||||||
|
val yCorrect = height - ((stat.answeredRight * animationProgress) / yMax) * height
|
||||||
|
|
||||||
|
// Vertical line marker
|
||||||
|
drawLine(
|
||||||
|
color = tooltipLineColor,
|
||||||
|
start = Offset(x, 0f),
|
||||||
|
end = Offset(x, height),
|
||||||
|
strokeWidth = 3f
|
||||||
|
)
|
||||||
|
|
||||||
|
// Dots on lines
|
||||||
|
drawCircle(color = dotCenterColor, radius = 12f, center = Offset(x, yCompleted))
|
||||||
|
drawCircle(color = colorCompleted, radius = 7f, center = Offset(x, yCompleted))
|
||||||
|
|
||||||
|
drawCircle(color = dotCenterColor, radius = 12f, center = Offset(x, yCorrect))
|
||||||
|
drawCircle(color = colorCorrect, radius = 7f, center = Offset(x, yCorrect))
|
||||||
|
|
||||||
|
// Measure text
|
||||||
|
val textStyle = TextStyle(color = tooltipTextColor, fontWeight = FontWeight.Bold, fontSize = 13.sp)
|
||||||
|
val textResCompleted = textMeasurer.measure(stat.completed.toString(), textStyle)
|
||||||
|
val textResCorrect = textMeasurer.measure(stat.answeredRight.toString(), textStyle)
|
||||||
|
|
||||||
|
val dotRadius = 5f
|
||||||
|
val gap = 6f
|
||||||
|
val padX = 12f
|
||||||
|
val padY = 8f
|
||||||
|
|
||||||
|
val w1 = padX * 2 + dotRadius * 2 + gap + textResCompleted.size.width
|
||||||
|
val h1 = padY * 2 + textResCompleted.size.height
|
||||||
|
|
||||||
|
val w2 = padX * 2 + dotRadius * 2 + gap + textResCorrect.size.width
|
||||||
|
val h2 = padY * 2 + textResCorrect.size.height
|
||||||
|
|
||||||
|
// Tooltip Overlap Prevention Logic
|
||||||
|
val completedIsHigher = yCompleted <= yCorrect
|
||||||
|
var yPosCompleted = if (completedIsHigher) yCompleted - h1 - 12f else yCompleted + 12f
|
||||||
|
var yPosCorrect = if (completedIsHigher) yCorrect + 12f else yCorrect - h2 - 12f
|
||||||
|
|
||||||
|
// Prevent clipping out of canvas bounds natively first
|
||||||
|
if (yPosCompleted < 0f && completedIsHigher) yPosCompleted = 0f
|
||||||
|
if (yPosCorrect < 0f && !completedIsHigher) yPosCorrect = 0f
|
||||||
|
if (yPosCompleted + h1 > height && !completedIsHigher) yPosCompleted = height - h1
|
||||||
|
if (yPosCorrect + h2 > height && completedIsHigher) yPosCorrect = height - h2
|
||||||
|
|
||||||
|
// Overlap resolution
|
||||||
|
val topRectY = minOf(yPosCompleted, yPosCorrect)
|
||||||
|
val topRectH = if (topRectY == yPosCompleted) h1 else h2
|
||||||
|
val bottomRectY = maxOf(yPosCompleted, yPosCorrect)
|
||||||
|
|
||||||
|
val gapBetweenTooltips = 8f
|
||||||
|
if (topRectY + topRectH + gapBetweenTooltips > bottomRectY) {
|
||||||
|
val midPointY = (yCompleted + yCorrect) / 2f
|
||||||
|
val adjustedTopY = midPointY - (topRectH + gapBetweenTooltips / 2f)
|
||||||
|
val adjustedBottomY = midPointY + (gapBetweenTooltips / 2f)
|
||||||
|
|
||||||
|
if (topRectY == yPosCompleted) {
|
||||||
|
yPosCompleted = adjustedTopY
|
||||||
|
yPosCorrect = adjustedBottomY
|
||||||
|
} else {
|
||||||
|
yPosCorrect = adjustedTopY
|
||||||
|
yPosCompleted = adjustedBottomY
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
}
|
||||||
Text(
|
|
||||||
text = stat.day,
|
// Final Canvas Bounds Check post-resolution
|
||||||
style = MaterialTheme.typography.bodySmall
|
val finalMinY = minOf(yPosCompleted, yPosCorrect)
|
||||||
|
if (finalMinY < 0f) {
|
||||||
|
yPosCompleted -= finalMinY
|
||||||
|
yPosCorrect -= finalMinY
|
||||||
|
}
|
||||||
|
val finalMaxY = maxOf(yPosCompleted + h1, yPosCorrect + h2)
|
||||||
|
if (finalMaxY > height) {
|
||||||
|
val shift = finalMaxY - height
|
||||||
|
yPosCompleted -= shift
|
||||||
|
yPosCorrect -= shift
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw Completed Tooltip
|
||||||
|
val t1X = (x - w1 / 2f).coerceIn(0f, width - w1)
|
||||||
|
drawRoundRect(color = tooltipBgColor, topLeft = Offset(t1X, yPosCompleted), size = Size(w1, h1), cornerRadius = CornerRadius(16f, 16f))
|
||||||
|
drawCircle(color = colorCompleted, radius = dotRadius, center = Offset(t1X + padX + dotRadius, yPosCompleted + h1 / 2f))
|
||||||
|
drawText(textLayoutResult = textResCompleted, topLeft = Offset(t1X + padX + dotRadius * 2 + gap, yPosCompleted + padY))
|
||||||
|
|
||||||
|
// Draw Correct Tooltip
|
||||||
|
val t2X = (x - w2 / 2f).coerceIn(0f, width - w2)
|
||||||
|
drawRoundRect(color = tooltipBgColor, topLeft = Offset(t2X, yPosCorrect), size = Size(w2, h2), cornerRadius = CornerRadius(16f, 16f))
|
||||||
|
drawCircle(color = colorCorrect, radius = dotRadius, center = Offset(t2X + padX + dotRadius, yPosCorrect + h2 / 2f))
|
||||||
|
drawText(textLayoutResult = textResCorrect, topLeft = Offset(t2X + padX + dotRadius * 2 + gap, yPosCorrect + padY))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// X-Axis Labels (Freed from fixed widths, prevented from wrapping)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
weeklyStats.forEachIndexed { index, stat ->
|
||||||
|
val isSelected = index == selectedIndex
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stat.day.uppercase().take(3) + ".",
|
||||||
|
color = if (isSelected) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
fontSize = 11.sp, // Slightly smaller to ensure fit across all devices
|
||||||
|
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
|
||||||
|
maxLines = 1,
|
||||||
|
softWrap = false // Prevents the text from splitting into multiple lines
|
||||||
|
)
|
||||||
|
if (isSelected) {
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(2.dp)
|
||||||
|
.width(20.dp)
|
||||||
|
.background(MaterialTheme.colorScheme.primary)
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
// Invisible spacer to prevent layout jumping when line appears
|
||||||
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,41 +380,29 @@ fun WeeklyActivityChartWidget(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
private fun Path.smoothCurve(points: List<Offset>) {
|
||||||
private fun RowScope.Bar(value: Int, maxValue: Int, color: Color) {
|
if (points.isEmpty()) return
|
||||||
var startAnimation by remember { mutableStateOf(false) }
|
moveTo(points.first().x, points.first().y)
|
||||||
val barHeight by animateFloatAsState(
|
for (i in 1 until points.size) {
|
||||||
targetValue = if (startAnimation) value.toFloat() / maxValue.toFloat() else 0f,
|
val prev = points[i - 1]
|
||||||
animationSpec = tween(durationMillis = 1000),
|
val curr = points[i]
|
||||||
label = "barHeightAnimation"
|
val controlX = (prev.x + curr.x) / 2f
|
||||||
)
|
cubicTo(
|
||||||
|
controlX, prev.y,
|
||||||
LaunchedEffect(Unit) {
|
controlX, curr.y,
|
||||||
delay(200) // Small delay to ensure the UI is ready before animating
|
curr.x, curr.y
|
||||||
@Suppress("AssignedValueIsNeverRead")
|
)
|
||||||
startAnimation = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.fillMaxHeight(barHeight)
|
|
||||||
.clip(RoundedCornerShape(topStart = 4.dp, topEnd = 4.dp))
|
|
||||||
.background(color)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun WeeklyChartLegend() {
|
private fun WeeklyChartLegend() {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth(),
|
||||||
.fillMaxWidth()
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
.padding(horizontal = 8.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
) {
|
||||||
LegendItem(color = MaterialTheme.semanticColors.stageGradient1, label = stringResource(R.string.label_added))
|
LegendItem(color = MaterialTheme.colorScheme.primary, label = stringResource(R.string.label_completed).uppercase())
|
||||||
LegendItem(color = MaterialTheme.semanticColors.stageGradient3, label = stringResource(R.string.label_completed))
|
LegendItem(color = MaterialTheme.colorScheme.tertiary, label = stringResource(R.string.label_correct).uppercase())
|
||||||
LegendItem(color = MaterialTheme.semanticColors.stageGradient5, label = stringResource(R.string.label_correct))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,8 +414,51 @@ private fun LegendItem(color: Color, label: String) {
|
|||||||
.size(10.dp)
|
.size(10.dp)
|
||||||
.background(color, shape = CircleShape)
|
.background(color, shape = CircleShape)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Text(text = label, style = MaterialTheme.typography.labelMedium, fontSize = 12.sp)
|
Text(
|
||||||
|
text = label,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
letterSpacing = 0.5.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ChartFooter(weeklyStats: List<WeeklyActivityStat>) {
|
||||||
|
val bestDay = remember(weeklyStats) {
|
||||||
|
weeklyStats.maxByOrNull { it.completed + it.answeredRight }?.day?.uppercase() ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
text = "Melhor Dia:",
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
fontSize = 13.sp
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f))
|
||||||
|
.padding(horizontal = 10.dp, vertical = 6.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = bestDay,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 12.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,13 +466,13 @@ private fun LegendItem(color: Color, label: String) {
|
|||||||
@Composable
|
@Composable
|
||||||
fun WeeklyActivityChartWidgetPreview() {
|
fun WeeklyActivityChartWidgetPreview() {
|
||||||
val sampleStats = listOf(
|
val sampleStats = listOf(
|
||||||
WeeklyActivityStat("Mon", 10, 5, 20),
|
WeeklyActivityStat("Seg", 30, 15, 10),
|
||||||
WeeklyActivityStat("Tue", 12, 3, 15),
|
WeeklyActivityStat("Ter", 45, 20, 12),
|
||||||
WeeklyActivityStat("Wed", 8, 8, 25),
|
WeeklyActivityStat("Qua", 80, 25, 15),
|
||||||
WeeklyActivityStat("Thu", 15, 2, 18),
|
WeeklyActivityStat("Qui", 84, 35, 18),
|
||||||
WeeklyActivityStat("Fri", 5, 10, 30),
|
WeeklyActivityStat("Sex", 50, 40, 22),
|
||||||
WeeklyActivityStat("Sat", 7, 6, 22),
|
WeeklyActivityStat("Sáb", 70, 30, 20),
|
||||||
WeeklyActivityStat("Sun", 9, 4, 17)
|
WeeklyActivityStat("Dom", 60, 25, 18)
|
||||||
)
|
)
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ import eu.gaudian.translator.utils.findActivity
|
|||||||
import eu.gaudian.translator.view.composable.AppAlertDialog
|
import eu.gaudian.translator.view.composable.AppAlertDialog
|
||||||
import eu.gaudian.translator.view.composable.AppDialog
|
import eu.gaudian.translator.view.composable.AppDialog
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
|
import eu.gaudian.translator.view.composable.SectionLabel
|
||||||
import eu.gaudian.translator.view.dialogs.RequestMorePackDialog
|
import eu.gaudian.translator.view.dialogs.RequestMorePackDialog
|
||||||
import eu.gaudian.translator.view.hints.HintDefinition
|
import eu.gaudian.translator.view.hints.HintDefinition
|
||||||
import eu.gaudian.translator.view.translation.LanguageSelectorBar
|
import eu.gaudian.translator.view.translation.LanguageSelectorBar
|
||||||
@@ -88,6 +89,7 @@ import eu.gaudian.translator.viewmodel.ImportState
|
|||||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||||
import eu.gaudian.translator.viewmodel.PackDownloadState
|
import eu.gaudian.translator.viewmodel.PackDownloadState
|
||||||
import eu.gaudian.translator.viewmodel.PackUiState
|
import eu.gaudian.translator.viewmodel.PackUiState
|
||||||
|
import eu.gaudian.translator.viewmodel.TranslationViewModel
|
||||||
import eu.gaudian.translator.viewmodel.VocabPacksViewModel
|
import eu.gaudian.translator.viewmodel.VocabPacksViewModel
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
@@ -124,19 +126,168 @@ enum class PackFilter {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
private val gradientPalette = listOf(
|
private val gradientPalette = listOf(
|
||||||
listOf(Color(0xFF1565C0), Color(0xFF42A5F5)),
|
// Original Gradients
|
||||||
listOf(Color(0xFF00695C), Color(0xFF26A69A)),
|
listOf(Color(0xFF1565C0), Color(0xFF42A5F5)), // Blue
|
||||||
listOf(Color(0xFF6A1B9A), Color(0xFFAB47BC)),
|
listOf(Color(0xFF00695C), Color(0xFF26A69A)), // Teal
|
||||||
listOf(Color(0xFFE65100), Color(0xFFFFA726)),
|
listOf(Color(0xFF6A1B9A), Color(0xFFAB47BC)), // Purple
|
||||||
listOf(Color(0xFF212121), Color(0xFF546E7A)),
|
listOf(Color(0xFFE65100), Color(0xFFFFA726)), // Orange
|
||||||
listOf(Color(0xFFC62828), Color(0xFFEF9A9A)),
|
listOf(Color(0xFF212121), Color(0xFF546E7A)), // Dark Grey to Blue Grey
|
||||||
listOf(Color(0xFF1B5E20), Color(0xFF66BB6A)),
|
listOf(Color(0xFFC62828), Color(0xFFEF9A9A)), // Red
|
||||||
listOf(Color(0xFF0D47A1), Color(0xFF90CAF9)),
|
listOf(Color(0xFF1B5E20), Color(0xFF66BB6A)), // Green
|
||||||
|
listOf(Color(0xFF0D47A1), Color(0xFF90CAF9)), // Deep Blue
|
||||||
|
|
||||||
|
// New Monochromatic / Material Shades
|
||||||
|
listOf(Color(0xFFAD1457), Color(0xFFF06292)), // Pink
|
||||||
|
listOf(Color(0xFF283593), Color(0xFF7986CB)), // Indigo
|
||||||
|
listOf(Color(0xFF00838F), Color(0xFF4DD0E1)), // Cyan
|
||||||
|
listOf(Color(0xFFFF8F00), Color(0xFFFFD54F)), // Amber
|
||||||
|
listOf(Color(0xFFD84315), Color(0xFFFF8A65)), // Deep Orange
|
||||||
|
listOf(Color(0xFF4E342E), Color(0xFFA1887F)), // Brown
|
||||||
|
listOf(Color(0xFF4527A0), Color(0xFF9575CD)), // Deep Purple
|
||||||
|
listOf(Color(0xFF9E9D24), Color(0xFFDCE775)), // Lime
|
||||||
|
listOf(Color(0xFF37474F), Color(0xFF90A4AE)), // Cool Grey
|
||||||
|
listOf(Color(0xFFF57F17), Color(0xFFFFF176)), // Yellow
|
||||||
|
|
||||||
|
// New Multi-Hue / Vibrant Gradients
|
||||||
|
listOf(Color(0xFF1A237E), Color(0xFF880E4F)), // Deep Blue to Deep Pink (Midnight)
|
||||||
|
listOf(Color(0xFFE65100), Color(0xFFE91E63)), // Dark Orange to Pink (Sunset)
|
||||||
|
listOf(Color(0xFF0277BD), Color(0xFF00897B)), // Light Blue to Teal (Ocean)
|
||||||
|
listOf(Color(0xFF303F9F), Color(0xFF7B1FA2)), // Indigo to Purple (Galaxy)
|
||||||
|
listOf(Color(0xFFBF360C), Color(0xFFFFCA28)), // Deep Red to Amber (Fire)
|
||||||
|
listOf(Color(0xFF004D40), Color(0xFF64FFDA)), // Dark Teal to Mint (Aqua)
|
||||||
|
listOf(Color(0xFF4A148C), Color(0xFFF50057)), // Dark Purple to Neon Pink (Cyberpunk)
|
||||||
|
listOf(Color(0xFF1B5E20), Color(0xFFC0CA33)), // Dark Green to Lime (Forest)
|
||||||
|
listOf(Color(0xFF827717), Color(0xFFFF9800)), // Olive to Orange (Autumn)
|
||||||
|
listOf(Color(0xFF01579B), Color(0xFF00E5FF)), // Navy to Neon Cyan (Electric Blue)
|
||||||
|
|
||||||
|
// Pastel / Soft Gradients
|
||||||
|
listOf(Color(0xFF80DEEA), Color(0xFFE0F7FA)), // Soft Cyan
|
||||||
|
listOf(Color(0xFFF48FB1), Color(0xFFFCE4EC)), // Soft Pink
|
||||||
|
listOf(Color(0xFFCE93D8), Color(0xFFF3E5F5)), // Soft Purple
|
||||||
|
listOf(Color(0xFFA5D6A7), Color(0xFFE8F5E9)), // Soft Green
|
||||||
|
|
||||||
|
listOf(Color(0xFF81D4FA), Color(0xFFE1F5FE)), // Light Sky Blue
|
||||||
|
listOf(Color(0xFFB39DDB), Color(0xFFEDE7F6)), // Soft Lavender
|
||||||
|
listOf(Color(0xFFFFCC80), Color(0xFFFFF3E0)), // Peach / Warm Sand
|
||||||
|
listOf(Color(0xFFA5D6A7), Color(0xFFF1F8E9)), // Pale Mint
|
||||||
|
listOf(Color(0xFFFFF59D), Color(0xFFFFFDE7)), // Soft Lemon
|
||||||
|
listOf(Color(0xFFFFAB91), Color(0xFFFBE9E7)), // Pale Coral
|
||||||
|
listOf(Color(0xFFCE93D8), Color(0xFFF3E5F5)), // Light Orchid
|
||||||
|
listOf(Color(0xFFBCAAA4), Color(0xFFEFEBE9)), // Light Taupe / Oat
|
||||||
|
listOf(Color(0xFF90CAF9), Color(0xFFE3F2FD)), // Baby Blue
|
||||||
|
listOf(Color(0xFFF48FB1), Color(0xFFFCE4EC)), // Rosewater
|
||||||
|
|
||||||
|
// Soft Multi-Hue (Two-tone Pastels)
|
||||||
|
listOf(Color(0xFFE1BEE7), Color(0xFFBBDEFB)), // Light Purple to Light Blue (Cotton Candy)
|
||||||
|
listOf(Color(0xFFFFF9C4), Color(0xFFFFCCBC)), // Pale Yellow to Pale Peach (Morning Light)
|
||||||
|
listOf(Color(0xFFB2EBF2), Color(0xFFC8E6C9)), // Pale Cyan to Pale Green (Seafoam)
|
||||||
|
listOf(Color(0xFFFFD54F), Color(0xFFFF8A65)), // Warm Sun to Soft Coral (Soft Sunset)
|
||||||
|
listOf(Color(0xFFD1C4E9), Color(0xFFF8BBD0)), // Periwinkle to Blush Pink (Twilight)
|
||||||
|
listOf(Color(0xFFC5E1A5), Color(0xFFFFF59D)), // Spring Green to Pale Yellow (Meadow)
|
||||||
|
listOf(Color(0xFF80CBC4), Color(0xFF81D4FA)), // Soft Teal to Light Blue (Glacier)
|
||||||
|
listOf(Color(0xFFF8BBD0), Color(0xFFFFE0B2)), // Soft Pink to Cream (Sorbet)
|
||||||
|
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun gradientForId(id: String): List<Color> =
|
private fun gradientForId(id: String): List<Color> =
|
||||||
gradientPalette[abs(id.hashCode()) % gradientPalette.size]
|
gradientPalette[abs(id.hashCode()) % gradientPalette.size]
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Translation cache - shared between PackCard and PackPreviewDialog, cleared on screen exit
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
private class TranslationCache {
|
||||||
|
// Cache key: pack ID, value: Pair(translatedName, translatedDescription)
|
||||||
|
private val cache = mutableMapOf<String, Pair<String, String>>()
|
||||||
|
|
||||||
|
fun get(packId: String): Pair<String, String>? = cache[packId]
|
||||||
|
|
||||||
|
fun put(packId: String, translated: Pair<String, String>) {
|
||||||
|
cache[packId] = translated
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
cache.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun rememberTranslationCache(): TranslationCache {
|
||||||
|
val cache = remember { TranslationCache() }
|
||||||
|
|
||||||
|
// Clear cache when leaving the screen
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
cache.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cache
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Translation helper - translates pack name and description from English to device's locale using LibreTranslate
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun rememberTranslatedPackInfo(
|
||||||
|
info: eu.gaudian.translator.model.communication.files_download.VocabCollectionInfo,
|
||||||
|
translationViewModel: TranslationViewModel,
|
||||||
|
cache: TranslationCache
|
||||||
|
): Pair<String, String> {
|
||||||
|
// Check cache first
|
||||||
|
val cached = cache.get(info.id)
|
||||||
|
if (cached != null) {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
|
var translatedName by remember(info.id) { mutableStateOf(info.name) }
|
||||||
|
var translatedDescription by remember(info.id) { mutableStateOf(info.description) }
|
||||||
|
|
||||||
|
// Get device locale using Locale.getDefault() which is more reliable
|
||||||
|
val deviceLocale = remember { java.util.Locale.getDefault().language }
|
||||||
|
|
||||||
|
// Launch translation when language or content changes
|
||||||
|
LaunchedEffect(info.name, info.description, deviceLocale) {
|
||||||
|
try {
|
||||||
|
// Always translate from English to device locale
|
||||||
|
val targetCode = deviceLocale
|
||||||
|
|
||||||
|
var finalName = info.name
|
||||||
|
var finalDescription = info.description
|
||||||
|
|
||||||
|
// Translate name if not empty using LibreTranslate directly
|
||||||
|
if (info.name.isNotBlank()) {
|
||||||
|
val nameResult = translationViewModel.translateWithLibreTranslate(info.name, targetCode, "en")
|
||||||
|
if (nameResult.isSuccess) {
|
||||||
|
finalName = nameResult.getOrNull() ?: info.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translate description if not empty using LibreTranslate directly
|
||||||
|
if (info.description.isNotBlank()) {
|
||||||
|
val descResult = translationViewModel.translateWithLibreTranslate(info.description, targetCode, "en")
|
||||||
|
if (descResult.isSuccess) {
|
||||||
|
finalDescription = descResult.getOrNull() ?: info.description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
translatedName = finalName
|
||||||
|
translatedDescription = finalDescription
|
||||||
|
|
||||||
|
// Store in cache
|
||||||
|
cache.put(info.id, Pair(finalName, finalDescription))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Translation failed for pack ${info.id}: ${e.message}")
|
||||||
|
// Keep original text on failure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Pair(translatedName, translatedDescription)
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Screen
|
// Screen
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -153,6 +304,7 @@ fun ExplorePacksScreen(
|
|||||||
val vocabPacksViewModel: VocabPacksViewModel = hiltViewModel()
|
val vocabPacksViewModel: VocabPacksViewModel = hiltViewModel()
|
||||||
val exportImportViewModel: ExportImportViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
val exportImportViewModel: ExportImportViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||||
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||||
|
val translationViewModel: TranslationViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||||
|
|
||||||
val packs by vocabPacksViewModel.packs.collectAsState()
|
val packs by vocabPacksViewModel.packs.collectAsState()
|
||||||
val isLoadingManifest by vocabPacksViewModel.isLoadingManifest.collectAsState()
|
val isLoadingManifest by vocabPacksViewModel.isLoadingManifest.collectAsState()
|
||||||
@@ -183,6 +335,9 @@ fun ExplorePacksScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Translation cache - shared between PackCard and PackPreviewDialog
|
||||||
|
val translationCache = rememberTranslationCache()
|
||||||
|
|
||||||
// Auto-open conflict dialog once a queued download finishes
|
// Auto-open conflict dialog once a queued download finishes
|
||||||
LaunchedEffect(packs, pendingImportPackId) {
|
LaunchedEffect(packs, pendingImportPackId) {
|
||||||
val id = pendingImportPackId ?: return@LaunchedEffect
|
val id = pendingImportPackId ?: return@LaunchedEffect
|
||||||
@@ -227,10 +382,10 @@ fun ExplorePacksScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filtered + sorted pack list
|
// Filtered + sorted pack list - also search through translated names
|
||||||
val filteredPacks = remember(
|
val filteredPacks = remember(
|
||||||
packs, selectedFilter, searchQuery,
|
packs, selectedFilter, searchQuery,
|
||||||
selectedSourceLanguage, selectedTargetLanguage
|
selectedSourceLanguage, selectedTargetLanguage, translationCache
|
||||||
) {
|
) {
|
||||||
val srcId = selectedSourceLanguage?.nameResId
|
val srcId = selectedSourceLanguage?.nameResId
|
||||||
val tgtId = selectedTargetLanguage?.nameResId
|
val tgtId = selectedTargetLanguage?.nameResId
|
||||||
@@ -247,9 +402,16 @@ fun ExplorePacksScreen(
|
|||||||
(tgtId == null || ids.contains(tgtId))
|
(tgtId == null || ids.contains(tgtId))
|
||||||
}
|
}
|
||||||
|
|
||||||
val matchSearch = searchQuery.isBlank() ||
|
// Search in both original English and translated names
|
||||||
|
val translated = translationCache.get(info.id)
|
||||||
|
val matchSearch = if (searchQuery.isBlank()) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
info.name.contains(searchQuery, ignoreCase = true) ||
|
info.name.contains(searchQuery, ignoreCase = true) ||
|
||||||
info.category.contains(searchQuery, ignoreCase = true)
|
info.category.contains(searchQuery, ignoreCase = true) ||
|
||||||
|
(translated?.first?.contains(searchQuery, ignoreCase = true) == true) ||
|
||||||
|
(translated?.second?.contains(searchQuery, ignoreCase = true) == true)
|
||||||
|
}
|
||||||
|
|
||||||
val matchFilter = when (val code = selectedFilter.cefrCode) {
|
val matchFilter = when (val code = selectedFilter.cefrCode) {
|
||||||
null -> true // All or Newest – handled by sort below
|
null -> true // All or Newest – handled by sort below
|
||||||
@@ -365,11 +527,7 @@ fun ExplorePacksScreen(
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Text(
|
SectionLabel(text = stringResource(R.string.label_available_collections))
|
||||||
stringResource(R.string.label_available_collections),
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
|
||||||
if (!isLoadingManifest && packs.isNotEmpty()) {
|
if (!isLoadingManifest && packs.isNotEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.label_d_packs, filteredPacks.size),
|
stringResource(R.string.label_d_packs, filteredPacks.size),
|
||||||
@@ -453,6 +611,9 @@ fun ExplorePacksScreen(
|
|||||||
items(filteredPacks, key = { it.info.id }) { packState ->
|
items(filteredPacks, key = { it.info.id }) { packState ->
|
||||||
PackCard(
|
PackCard(
|
||||||
packState = packState,
|
packState = packState,
|
||||||
|
languageViewModel = languageViewModel,
|
||||||
|
translationViewModel = translationViewModel,
|
||||||
|
translationCache = translationCache,
|
||||||
onCardClick = {
|
onCardClick = {
|
||||||
previewPack = packState
|
previewPack = packState
|
||||||
when (packState.downloadState) {
|
when (packState.downloadState) {
|
||||||
@@ -512,6 +673,9 @@ fun ExplorePacksScreen(
|
|||||||
if (preview != null) {
|
if (preview != null) {
|
||||||
PackPreviewDialog(
|
PackPreviewDialog(
|
||||||
packState = preview,
|
packState = preview,
|
||||||
|
languageViewModel = languageViewModel,
|
||||||
|
translationViewModel = translationViewModel,
|
||||||
|
translationCache = translationCache,
|
||||||
onDismiss = { previewPack = null },
|
onDismiss = { previewPack = null },
|
||||||
onGetClick = {
|
onGetClick = {
|
||||||
pendingImportPackId = preview.info.id
|
pendingImportPackId = preview.info.id
|
||||||
@@ -668,14 +832,30 @@ private fun PackConflictStrategyOption(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun PackCard(
|
private fun PackCard(
|
||||||
packState: PackUiState,
|
packState: PackUiState,
|
||||||
|
languageViewModel: LanguageViewModel,
|
||||||
|
translationViewModel: TranslationViewModel,
|
||||||
|
translationCache: TranslationCache,
|
||||||
onCardClick: () -> Unit,
|
onCardClick: () -> Unit,
|
||||||
onGetClick: () -> Unit,
|
onGetClick: () -> Unit,
|
||||||
onAddToLibraryClick: () -> Unit,
|
onAddToLibraryClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
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 }
|
||||||
|
|
||||||
|
// Get translated name and description
|
||||||
|
val (translatedName, translatedDescription) = rememberTranslatedPackInfo(info, translationViewModel, translationCache)
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -781,14 +961,14 @@ private fun PackCard(
|
|||||||
// ── Pack info ─────────────────────────────────────────────────────
|
// ── Pack info ─────────────────────────────────────────────────────
|
||||||
Column(modifier = Modifier.padding(10.dp)) {
|
Column(modifier = Modifier.padding(10.dp)) {
|
||||||
Text(
|
Text(
|
||||||
text = info.name,
|
text = translatedName,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
maxLines = 2
|
maxLines = 2
|
||||||
)
|
)
|
||||||
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
|
||||||
@@ -886,6 +1066,9 @@ private fun PackCard(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun PackPreviewDialog(
|
private fun PackPreviewDialog(
|
||||||
packState: PackUiState,
|
packState: PackUiState,
|
||||||
|
languageViewModel: LanguageViewModel,
|
||||||
|
translationViewModel: TranslationViewModel,
|
||||||
|
translationCache: TranslationCache,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onGetClick: () -> Unit,
|
onGetClick: () -> Unit,
|
||||||
onAddToLibraryClick: () -> Unit,
|
onAddToLibraryClick: () -> Unit,
|
||||||
@@ -895,9 +1078,12 @@ private fun PackPreviewDialog(
|
|||||||
val gradient = gradientForId(info.id)
|
val gradient = gradientForId(info.id)
|
||||||
var selectedItem by remember { mutableStateOf<VocabularyItem?>(null) }
|
var selectedItem by remember { mutableStateOf<VocabularyItem?>(null) }
|
||||||
|
|
||||||
|
// Get translated name and description
|
||||||
|
val (translatedName, translatedDescription) = rememberTranslatedPackInfo(info, translationViewModel, translationCache)
|
||||||
|
|
||||||
AppDialog(
|
AppDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
title = { Text(info.name, fontWeight = FontWeight.Bold) },
|
title = { Text(translatedName, fontWeight = FontWeight.Bold) },
|
||||||
) {
|
) {
|
||||||
// ── Gradient banner ───────────────────────────────────────────
|
// ── Gradient banner ───────────────────────────────────────────
|
||||||
Box(
|
Box(
|
||||||
@@ -925,18 +1111,18 @@ private fun PackPreviewDialog(
|
|||||||
Column(modifier = Modifier.align(Alignment.BottomStart).padding(12.dp)) {
|
Column(modifier = Modifier.align(Alignment.BottomStart).padding(12.dp)) {
|
||||||
Text(info.emoji, fontSize = 32.sp)
|
Text(info.emoji, fontSize = 32.sp)
|
||||||
Text(
|
Text(
|
||||||
"${info.category} · ${stringResource(R.string.text_d_cards, info.itemCount)}",
|
"$translatedName · ${stringResource(R.string.text_d_cards, info.itemCount)}",
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelLarge,
|
||||||
color = Color.White.copy(alpha = 0.85f)
|
color = Color.White.copy(alpha = 0.85f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Description ───────────────────────────────────────────────
|
// ── Description ───────────────────────────────────────────────
|
||||||
if (info.description.isNotBlank()) {
|
if (translatedDescription.isNotBlank()) {
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
Text(
|
Text(
|
||||||
text = info.description,
|
text = translatedDescription,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.size
|
|||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@@ -31,8 +30,6 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TextField
|
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
@@ -46,7 +43,6 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringArrayResource
|
import androidx.compose.ui.res.stringArrayResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -63,8 +59,10 @@ import eu.gaudian.translator.utils.findActivity
|
|||||||
import eu.gaudian.translator.view.NavigationRoutes
|
import eu.gaudian.translator.view.NavigationRoutes
|
||||||
import eu.gaudian.translator.view.composable.AppButton
|
import eu.gaudian.translator.view.composable.AppButton
|
||||||
import eu.gaudian.translator.view.composable.AppCard
|
import eu.gaudian.translator.view.composable.AppCard
|
||||||
|
import eu.gaudian.translator.view.composable.AppIconContainer
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
import eu.gaudian.translator.view.composable.AppIcons
|
||||||
import eu.gaudian.translator.view.composable.AppOutlinedButton
|
import eu.gaudian.translator.view.composable.AppOutlinedButton
|
||||||
|
import eu.gaudian.translator.view.composable.AppOutlinedTextField
|
||||||
import eu.gaudian.translator.view.composable.AppSlider
|
import eu.gaudian.translator.view.composable.AppSlider
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
import eu.gaudian.translator.view.composable.InspiringSearchField
|
import eu.gaudian.translator.view.composable.InspiringSearchField
|
||||||
@@ -226,6 +224,14 @@ fun NewWordScreen(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// Explore Packs - Prominent full-width card at top
|
||||||
|
ExplorePacksProminentCard(
|
||||||
|
onClick = { navController.navigate(NavigationRoutes.EXPLORE_PACKS) }
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// AI Generator Card
|
||||||
AIGeneratorCard(
|
AIGeneratorCard(
|
||||||
category = category,
|
category = category,
|
||||||
onCategoryChange = { category = it },
|
onCategoryChange = { category = it },
|
||||||
@@ -245,6 +251,7 @@ fun NewWordScreen(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// Add Manually Card
|
||||||
AddManuallyCard(
|
AddManuallyCard(
|
||||||
languageViewModel = languageViewModel,
|
languageViewModel = languageViewModel,
|
||||||
vocabularyViewModel = vocabularyViewModel,
|
vocabularyViewModel = vocabularyViewModel,
|
||||||
@@ -252,11 +259,9 @@ fun NewWordScreen(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
BottomActionCardsRow(
|
// Import CSV - Full width card at bottom
|
||||||
onExplorePsClick = {
|
ImportCsvCard(
|
||||||
navController.navigate(NavigationRoutes.EXPLORE_PACKS)
|
onClick = {
|
||||||
},
|
|
||||||
onImportCsvClick = {
|
|
||||||
navController.navigate("settings_vocabulary_repository_options")
|
navController.navigate("settings_vocabulary_repository_options")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -464,7 +469,11 @@ fun AIGeneratorCard(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f))
|
||||||
|
.padding(12.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
) {
|
) {
|
||||||
SourceLanguageDropdown(
|
SourceLanguageDropdown(
|
||||||
@@ -556,26 +565,16 @@ fun AddManuallyCard(
|
|||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(24.dp)) {
|
Column(modifier = Modifier.padding(24.dp)) {
|
||||||
// Header Row
|
// Header Row - Using reusable AppIconContainer
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Box(
|
AppIconContainer(
|
||||||
modifier = Modifier
|
imageVector = Icons.Default.EditNote
|
||||||
.size(40.dp)
|
)
|
||||||
.clip(RoundedCornerShape(12.dp))
|
|
||||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.EditNote,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.label_add_vocabulary),
|
text = stringResource(R.string.label_add_vocabulary),
|
||||||
@@ -588,37 +587,19 @@ fun AddManuallyCard(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
// Input Fields
|
// Input Fields - Using AppOutlinedTextField
|
||||||
TextField(
|
AppOutlinedTextField(
|
||||||
value = wordText,
|
value = wordText,
|
||||||
onValueChange = { wordText = it },
|
onValueChange = { wordText = it },
|
||||||
placeholder = { Text(stringResource(R.string.text_label_word), color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)) },
|
placeholder = { Text(stringResource(R.string.text_label_word)) }
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
shape = RoundedCornerShape(12.dp),
|
|
||||||
colors = TextFieldDefaults.colors(
|
|
||||||
focusedContainerColor = MaterialTheme.colorScheme.surface, // Very dark background
|
|
||||||
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
|
|
||||||
focusedIndicatorColor = Color.Transparent,
|
|
||||||
unfocusedIndicatorColor = Color.Transparent
|
|
||||||
),
|
|
||||||
singleLine = true
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
TextField(
|
AppOutlinedTextField(
|
||||||
value = translationText,
|
value = translationText,
|
||||||
onValueChange = { translationText = it },
|
onValueChange = { translationText = it },
|
||||||
placeholder = { Text(stringResource(R.string.text_translation), color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)) },
|
placeholder = { Text(stringResource(R.string.text_translation)) }
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
shape = RoundedCornerShape(12.dp),
|
|
||||||
colors = TextFieldDefaults.colors(
|
|
||||||
focusedContainerColor = MaterialTheme.colorScheme.surface,
|
|
||||||
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
|
|
||||||
focusedIndicatorColor = Color.Transparent,
|
|
||||||
unfocusedIndicatorColor = Color.Transparent
|
|
||||||
),
|
|
||||||
singleLine = true
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
@@ -653,7 +634,7 @@ fun AddManuallyCard(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
// Add to List Button (Darker variant)
|
// Add to List Button
|
||||||
AppButton(
|
AppButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
val newItem = VocabularyItem(
|
val newItem = VocabularyItem(
|
||||||
@@ -682,84 +663,80 @@ fun AddManuallyCard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Explore Packs Prominent Card (Full width at top) ---
|
||||||
@Composable
|
@Composable
|
||||||
fun BottomActionCardsRow(
|
fun ExplorePacksProminentCard(
|
||||||
modifier: Modifier = Modifier,
|
onClick: () -> Unit,
|
||||||
onExplorePsClick: () -> Unit,
|
modifier: Modifier = Modifier
|
||||||
onImportCsvClick: () -> Unit
|
|
||||||
) {
|
) {
|
||||||
Row(
|
AppCard(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
onClick = onClick
|
||||||
) {
|
) {
|
||||||
// Explore Packs Card
|
Row(
|
||||||
AppCard(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.fillMaxWidth()
|
||||||
.height(120.dp),
|
.padding(20.dp),
|
||||||
onClick = onExplorePsClick
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Column(
|
AppIconContainer(
|
||||||
modifier = Modifier.fillMaxSize(),
|
imageVector = AppIcons.Vocabulary,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
size = 56.dp,
|
||||||
verticalArrangement = Arrangement.Center
|
iconSize = 28.dp
|
||||||
) {
|
)
|
||||||
Box(
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
modifier = Modifier
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
.size(48.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = AppIcons.Vocabulary,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
@Suppress("HardCodedStringLiteral")
|
|
||||||
Text(
|
Text(
|
||||||
text = "Explore Packs",
|
text = stringResource(R.string.title_explore_packs),
|
||||||
style = MaterialTheme.typography.labelLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Import CSV Card
|
|
||||||
AppCard(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.height(120.dp),
|
|
||||||
onClick = onImportCsvClick
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Center
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(48.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.DriveFolderUpload,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
Text(
|
|
||||||
text = "Import Lists or CSV",
|
|
||||||
style = MaterialTheme.typography.labelLarge,
|
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.desc_explore_packs),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Import CSV Card (Full width at bottom) ---
|
||||||
|
@Composable
|
||||||
|
fun ImportCsvCard(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
AppCard(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
onClick = onClick
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(20.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
AppIconContainer(
|
||||||
|
imageVector = Icons.Default.DriveFolderUpload,
|
||||||
|
size = 56.dp,
|
||||||
|
iconSize = 28.dp
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.label_import_csv_or_lists),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.desc_import_csv),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -435,7 +435,7 @@ private fun VocabularyCardContent(
|
|||||||
onMoveToStageClick = onMoveToStageClick,
|
onMoveToStageClick = onMoveToStageClick,
|
||||||
onDeleteClick = onDeleteClick,
|
onDeleteClick = onDeleteClick,
|
||||||
|
|
||||||
showAnalyzeGrammarButton = item.features.isNullOrBlank(),
|
showAnalyzeGrammarButton = !item.hasFeatures(),
|
||||||
onAnalyzeGrammarClick = {
|
onAnalyzeGrammarClick = {
|
||||||
vocabularyViewModel.fetchAndApplyGrammaticalDetailsForList(listOf(item))
|
vocabularyViewModel.fetchAndApplyGrammaticalDetailsForList(listOf(item))
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ class ProgressViewModel @Inject constructor(
|
|||||||
|
|
||||||
// Calculate localized day name
|
// Calculate localized day name
|
||||||
val calendarDay = ((date.dayOfWeek.ordinal + 1) % 7) + 1
|
val calendarDay = ((date.dayOfWeek.ordinal + 1) % 7) + 1
|
||||||
val localizedDay = DateFormatSymbols.getInstance(Locale.getDefault()).shortWeekdays[calendarDay].uppercase()
|
val localizedDay = DateFormatSymbols.getInstance(Locale.getDefault()).weekdays[calendarDay]
|
||||||
|
|
||||||
WeeklyActivityStat(
|
WeeklyActivityStat(
|
||||||
// 3. Get the actual day name from the date and take the first 3 letters.
|
// 3. Get the actual day name from the date and take the first 3 letters.
|
||||||
|
|||||||
@@ -177,6 +177,17 @@ class TranslationViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Direct LibreTranslate without AI - for translating pack names/descriptions
|
||||||
|
suspend fun translateWithLibreTranslate(text: String, targetLanguageCode: String, sourceLanguageCode: String?): Result<String> {
|
||||||
|
// If source and target are the same, return the original text without calling the API
|
||||||
|
val sourceCode = sourceLanguageCode?.lowercase() ?: "en"
|
||||||
|
val targetCode = targetLanguageCode.lowercase()
|
||||||
|
if (sourceCode == targetCode) {
|
||||||
|
return Result.success(text)
|
||||||
|
}
|
||||||
|
return translationService.libreTranslate(text, sourceLanguageCode, targetLanguageCode, 1)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun getMultipleTranslations(sentence: String, contextPhrase: String? = null): Result<List<String>> {
|
suspend fun getMultipleTranslations(sentence: String, contextPhrase: String? = null): Result<List<String>> {
|
||||||
return translationService.getMultipleSynonyms(sentence, contextPhrase)
|
return translationService.getMultipleSynonyms(sentence, contextPhrase)
|
||||||
.also { result ->
|
.also { result ->
|
||||||
|
|||||||
@@ -1327,7 +1327,7 @@ class VocabularyViewModel @Inject constructor(
|
|||||||
|
|
||||||
val itemsWithoutGrammarCount: StateFlow<Int> = vocabularyItems
|
val itemsWithoutGrammarCount: StateFlow<Int> = vocabularyItems
|
||||||
.map { items ->
|
.map { items ->
|
||||||
items.count { it.features.isNullOrEmpty() }
|
items.count { it.hasFeatures() }
|
||||||
}
|
}
|
||||||
.stateIn(
|
.stateIn(
|
||||||
scope = viewModelScope,
|
scope = viewModelScope,
|
||||||
|
|||||||
@@ -419,7 +419,6 @@
|
|||||||
<string name="label_all_types">Alle Typen</string>
|
<string name="label_all_types">Alle Typen</string>
|
||||||
<string name="filter_and_sort">Filtern und Sortieren</string>
|
<string name="filter_and_sort">Filtern und Sortieren</string>
|
||||||
<string name="language_with_id_d_not_found">Sprache mit ID %1$d nicht gefunden</string>
|
<string name="language_with_id_d_not_found">Sprache mit ID %1$d nicht gefunden</string>
|
||||||
<string name="items_without_grammar_infos">Einträge ohne Grammatikinfos</string>
|
|
||||||
<string name="resolve_missing_language_id">Fehlende Sprach-ID auflösen: %1$d</string>
|
<string name="resolve_missing_language_id">Fehlende Sprach-ID auflösen: %1$d</string>
|
||||||
<string name="found_d_items_using_this_missing_language_id">%1$d Einträge mit dieser fehlenden Sprach-ID gefunden.</string>
|
<string name="found_d_items_using_this_missing_language_id">%1$d Einträge mit dieser fehlenden Sprach-ID gefunden.</string>
|
||||||
<string name="hide_affected_items">Betroffene Einträge ausblenden</string>
|
<string name="hide_affected_items">Betroffene Einträge ausblenden</string>
|
||||||
@@ -903,7 +902,6 @@
|
|||||||
<string name="text_add_new_word_to_list">Extrahiere ein neues Wort in deine Liste</string>
|
<string name="text_add_new_word_to_list">Extrahiere ein neues Wort in deine Liste</string>
|
||||||
<string name="cd_scroll_to_top">Nach oben scrollen</string>
|
<string name="cd_scroll_to_top">Nach oben scrollen</string>
|
||||||
<string name="cd_settings">Einstellungen</string>
|
<string name="cd_settings">Einstellungen</string>
|
||||||
<string name="label_import_csv">CSV importieren</string>
|
|
||||||
<string name="label_ai_generator">KI-Generator</string>
|
<string name="label_ai_generator">KI-Generator</string>
|
||||||
<string name="label_new_wordss">Neue Wörter</string>
|
<string name="label_new_wordss">Neue Wörter</string>
|
||||||
<string name="label_recently_added">Kürzlich hinzugefügt</string>
|
<string name="label_recently_added">Kürzlich hinzugefügt</string>
|
||||||
|
|||||||
@@ -415,7 +415,6 @@
|
|||||||
<string name="label_all_types">Todos os Tipos</string>
|
<string name="label_all_types">Todos os Tipos</string>
|
||||||
<string name="filter_and_sort">Filtrar e Ordenar</string>
|
<string name="filter_and_sort">Filtrar e Ordenar</string>
|
||||||
<string name="language_with_id_d_not_found">Idioma com id %1$d não encontrado</string>
|
<string name="language_with_id_d_not_found">Idioma com id %1$d não encontrado</string>
|
||||||
<string name="items_without_grammar_infos">Itens sem infos de gramática</string>
|
|
||||||
<string name="resolve_missing_language_id">Resolver ID de Idioma Ausente: %1$d</string>
|
<string name="resolve_missing_language_id">Resolver ID de Idioma Ausente: %1$d</string>
|
||||||
<string name="found_d_items_using_this_missing_language_id">Encontrados %1$d itens usando este ID de idioma ausente.</string>
|
<string name="found_d_items_using_this_missing_language_id">Encontrados %1$d itens usando este ID de idioma ausente.</string>
|
||||||
<string name="hide_affected_items">Ocultar Itens Afetados</string>
|
<string name="hide_affected_items">Ocultar Itens Afetados</string>
|
||||||
@@ -899,7 +898,6 @@
|
|||||||
<string name="text_add_new_word_to_list">Extrair uma nova palavra para a sua lista</string>
|
<string name="text_add_new_word_to_list">Extrair uma nova palavra para a sua lista</string>
|
||||||
<string name="cd_scroll_to_top">Rolar para o topo</string>
|
<string name="cd_scroll_to_top">Rolar para o topo</string>
|
||||||
<string name="cd_settings">Configurações</string>
|
<string name="cd_settings">Configurações</string>
|
||||||
<string name="label_import_csv">Importar CSV</string>
|
|
||||||
<string name="label_ai_generator">Gerador de IA</string>
|
<string name="label_ai_generator">Gerador de IA</string>
|
||||||
<string name="label_new_wordss">Novas Palavras</string>
|
<string name="label_new_wordss">Novas Palavras</string>
|
||||||
<string name="label_recently_added">Adicionados recentemente</string>
|
<string name="label_recently_added">Adicionados recentemente</string>
|
||||||
|
|||||||
@@ -77,6 +77,8 @@
|
|||||||
|
|
||||||
<string name="desc_daily_review_due">%1$d words need attention</string>
|
<string name="desc_daily_review_due">%1$d words need attention</string>
|
||||||
<string name="desc_expand_your_vocabulary">Expand your vocabulary</string>
|
<string name="desc_expand_your_vocabulary">Expand your vocabulary</string>
|
||||||
|
<string name="desc_explore_packs">Discover lists to download</string>
|
||||||
|
<string name="desc_import_csv">Import words from CSV or lists</string>
|
||||||
|
|
||||||
<string name="description">Description</string>
|
<string name="description">Description</string>
|
||||||
|
|
||||||
@@ -208,7 +210,7 @@
|
|||||||
<string name="item_id">Item ID: %1$d</string>
|
<string name="item_id">Item ID: %1$d</string>
|
||||||
|
|
||||||
<string name="items">%1$d items</string>
|
<string name="items">%1$d items</string>
|
||||||
<string name="items_without_grammar_infos">Items without grammar infos</string>
|
<string name="label_items_without_grammar">Items without grammar infos</string>
|
||||||
|
|
||||||
<string name="keep_both">Keep Both</string>
|
<string name="keep_both">Keep Both</string>
|
||||||
|
|
||||||
@@ -1166,4 +1168,5 @@
|
|||||||
|
|
||||||
<!-- Explore Packs Hint -->
|
<!-- Explore Packs Hint -->
|
||||||
<string name="hint_explore_packs_title">About Vocabulary Packs</string>
|
<string name="hint_explore_packs_title">About Vocabulary Packs</string>
|
||||||
|
<string name="label_import_csv_or_lists">Import Lists or CSV</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user