refactor UI components and layout in NewWordScreen and HomeScreen using new reusable composables: AppActionCard, AppIconContainer, AppTextField, and LabeledSection.
This commit is contained in:
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -27,7 +27,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 +46,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 +356,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) }
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -365,11 +366,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),
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -61,13 +57,17 @@ import eu.gaudian.translator.utils.StatusMessageId
|
|||||||
import eu.gaudian.translator.utils.StatusMessageService
|
import eu.gaudian.translator.utils.StatusMessageService
|
||||||
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.AppActionCard
|
||||||
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
|
||||||
|
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.composable.SourceLanguageDropdown
|
import eu.gaudian.translator.view.composable.SourceLanguageDropdown
|
||||||
import eu.gaudian.translator.view.composable.TargetLanguageDropdown
|
import eu.gaudian.translator.view.composable.TargetLanguageDropdown
|
||||||
@@ -226,6 +226,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 +253,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 +261,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 +471,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 +567,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 +589,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 +636,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 +665,92 @@ 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
|
) {
|
||||||
|
AppCard(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
onClick = onClick
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
|
||||||
) {
|
|
||||||
// Explore Packs Card
|
|
||||||
AppCard(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.fillMaxWidth()
|
||||||
.height(120.dp),
|
.padding(20.dp),
|
||||||
onClick = onExplorePsClick
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Column(
|
AppIconContainer(
|
||||||
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 = AppIcons.Vocabulary,
|
imageVector = AppIcons.Vocabulary,
|
||||||
contentDescription = null,
|
size = 56.dp,
|
||||||
tint = MaterialTheme.colorScheme.primary
|
iconSize = 28.dp
|
||||||
)
|
)
|
||||||
}
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
@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,
|
fontWeight = FontWeight.Bold
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
)
|
||||||
|
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
|
|
||||||
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(
|
Icon(
|
||||||
imageVector = Icons.Default.DriveFolderUpload,
|
imageVector = Icons.Default.DriveFolderUpload,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
tint = MaterialTheme.colorScheme.primary
|
tint = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 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(
|
||||||
text = "Import Lists or CSV",
|
text = stringResource(R.string.label_import_csv),
|
||||||
style = MaterialTheme.typography.labelLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
fontWeight = FontWeight.Bold
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.DriveFolderUpload,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
@@ -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 curated vocabulary packs</string>
|
||||||
|
<string name="desc_import_csv">Import words from CSV or lists</string>
|
||||||
|
|
||||||
<string name="description">Description</string>
|
<string name="description">Description</string>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user