refactor UI components and layout in NewWordScreen and HomeScreen using new reusable composables: AppActionCard, AppIconContainer, AppTextField, and LabeledSection.

This commit is contained in:
jonasgaudian
2026-02-19 15:24:27 +01:00
parent b75f5f32a0
commit f737657cdb
10 changed files with 408 additions and 146 deletions

View File

@@ -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.

View File

@@ -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()
}
}

View File

@@ -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
)
}

View File

@@ -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,

View File

@@ -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
)
}

View File

@@ -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)
) )

View File

@@ -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) }

View File

@@ -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),

View File

@@ -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
) { ) {
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(),
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
)
} }
} }
}
} }

View File

@@ -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>