implement CSV import for new words and refactor UI components to use AppCard

This commit is contained in:
jonasgaudian
2026-02-16 22:22:11 +01:00
parent 70e416d5e1
commit a7c83bb846
3 changed files with 338 additions and 75 deletions

View File

@@ -101,6 +101,7 @@ fun AppCard(
text: String? = null, text: String? = null,
expandable: Boolean = false, expandable: Boolean = false,
initiallyExpanded: Boolean = false, initiallyExpanded: Boolean = false,
onClick: (() -> Unit)? = null,
content: @Composable ColumnScope.() -> Unit, content: @Composable ColumnScope.() -> Unit,
) { ) {
var isExpanded by remember { mutableStateOf(initiallyExpanded) } var isExpanded by remember { mutableStateOf(initiallyExpanded) }
@@ -113,6 +114,7 @@ fun AppCard(
// Check if we need to render the header row // Check if we need to render the header row
// Updated to include icon in the check // Updated to include icon in the check
val hasHeader = title != null || text != null || expandable || icon != null val hasHeader = title != null || text != null || expandable || icon != null
val canClickHeader = expandable || onClick != null
Surface( Surface(
modifier = modifier modifier = modifier
@@ -133,7 +135,12 @@ fun AppCard(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable(enabled = expandable) { isExpanded = !isExpanded } .clickable(enabled = canClickHeader) {
if (expandable) {
isExpanded = !isExpanded
}
onClick?.invoke()
}
.padding(ComponentDefaults.CardPadding), .padding(ComponentDefaults.CardPadding),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@@ -186,17 +193,27 @@ fun AppCard(
// --- Content Area --- // --- Content Area ---
if (!expandable || isExpanded) { if (!expandable || isExpanded) {
Column( val contentModifier = Modifier
modifier = Modifier.padding( .padding(
start = ComponentDefaults.CardPadding, start = ComponentDefaults.CardPadding,
end = ComponentDefaults.CardPadding, end = ComponentDefaults.CardPadding,
bottom = ComponentDefaults.CardPadding, bottom = ComponentDefaults.CardPadding,
// If we have a header, remove the top padding so content sits closer to the title. // If we have a header, remove the top padding so content sits closer to the title.
// If no header (legacy behavior), keep the top padding. // If no header (legacy behavior), keep the top padding.
top = if (hasHeader) 0.dp else ComponentDefaults.CardPadding top = if (hasHeader) 0.dp else ComponentDefaults.CardPadding
), )
if (!hasHeader && onClick != null) {
Column(
modifier = contentModifier.clickable { onClick() },
content = content content = content
) )
} else {
Column(
modifier = contentModifier,
content = content
)
}
} }
} }
} }

View File

@@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AddCircle import androidx.compose.material.icons.filled.AddCircle
import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.CheckCircle
@@ -25,8 +24,6 @@ import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Psychology import androidx.compose.material.icons.filled.Psychology
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.TrendingUp import androidx.compose.material.icons.filled.TrendingUp
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@@ -47,6 +44,7 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import eu.gaudian.translator.utils.findActivity import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.AppCard
import eu.gaudian.translator.view.composable.Screen import eu.gaudian.translator.view.composable.Screen
import eu.gaudian.translator.view.vocabulary.widgets.WeeklyActivityChartWidget import eu.gaudian.translator.view.vocabulary.widgets.WeeklyActivityChartWidget
import eu.gaudian.translator.viewmodel.ProgressViewModel import eu.gaudian.translator.viewmodel.ProgressViewModel
@@ -177,10 +175,8 @@ fun StatCard(
title: String, title: String,
subtitle: String subtitle: String
) { ) {
Card( AppCard(
modifier = modifier, modifier = modifier,
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
) { ) {
Column( Column(
modifier = Modifier.padding(20.dp), modifier = Modifier.padding(20.dp),
@@ -208,10 +204,8 @@ fun GoalCard(
title: String, title: String,
subtitle: String subtitle: String
) { ) {
Card( AppCard(
modifier = modifier, modifier = modifier,
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
) { ) {
Column( Column(
modifier = Modifier.padding(20.dp), modifier = Modifier.padding(20.dp),
@@ -269,19 +263,15 @@ fun ActionCard(
} }
if (onClick != null) { if (onClick != null) {
Card( AppCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = containerColor, contentColor = contentColor),
onClick = onClick onClick = onClick
) { ) {
cardContent() cardContent()
} }
} else { } else {
Card( AppCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = containerColor, contentColor = contentColor)
) { ) {
cardContent() cardContent()
} }
@@ -311,10 +301,8 @@ fun WeeklyProgressSection(
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Card( AppCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
) { ) {
if (weeklyActivityStats.isEmpty()) { if (weeklyActivityStats.isEmpty()) {
Column( Column(
@@ -344,10 +332,8 @@ fun BottomStatsSection() {
horizontalArrangement = Arrangement.spacedBy(16.dp) horizontalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
// Total Words // Total Words
Card( AppCard(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
) { ) {
Column(modifier = Modifier.padding(20.dp)) { Column(modifier = Modifier.padding(20.dp)) {
Text(text = "TOTAL WORDS", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)) Text(text = "TOTAL WORDS", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f))
@@ -363,10 +349,8 @@ fun BottomStatsSection() {
} }
// Accuracy // Accuracy
Card( AppCard(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
) { ) {
Column(modifier = Modifier.padding(20.dp)) { Column(modifier = Modifier.padding(20.dp)) {
Text(text = "ACCURACY", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)) Text(text = "ACCURACY", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f))

View File

@@ -1,5 +1,7 @@
package eu.gaudian.translator.view.vocabulary package eu.gaudian.translator.view.vocabulary
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background 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
@@ -22,13 +24,14 @@ import androidx.compose.material.icons.filled.AutoAwesome
import androidx.compose.material.icons.filled.DriveFolderUpload import androidx.compose.material.icons.filled.DriveFolderUpload
import androidx.compose.material.icons.filled.EditNote import androidx.compose.material.icons.filled.EditNote
import androidx.compose.material.icons.filled.LibraryBooks import androidx.compose.material.icons.filled.LibraryBooks
import androidx.compose.material3.Card import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -36,12 +39,14 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue 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.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -52,12 +57,18 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import eu.gaudian.translator.R import eu.gaudian.translator.R
import eu.gaudian.translator.model.Language
import eu.gaudian.translator.model.VocabularyItem import eu.gaudian.translator.model.VocabularyItem
import eu.gaudian.translator.utils.StatusMessageId
import eu.gaudian.translator.utils.StatusMessageService
import eu.gaudian.translator.utils.findActivity import eu.gaudian.translator.utils.findActivity
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.AppOutlinedButton
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.SingleLanguageDropDown
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
import eu.gaudian.translator.viewmodel.LanguageViewModel import eu.gaudian.translator.viewmodel.LanguageViewModel
@@ -88,15 +99,122 @@ fun NewWordScreen(
} }
} }
val statusMessageService = StatusMessageService
val context = LocalContext.current
val showTableImportDialog = remember { mutableStateOf(false) }
var parsedTable by remember { mutableStateOf<List<List<String>>>(emptyList()) }
var selectedColFirst by remember { mutableIntStateOf(0) }
var selectedColSecond by remember { mutableIntStateOf(1) }
var skipHeader by remember { mutableStateOf(true) }
var selectedLangFirst by remember { mutableStateOf<Language?>(null) }
var selectedLangSecond by remember { mutableStateOf<Language?>(null) }
var parseError by remember { mutableStateOf<String?>(null) }
fun parseCsv(text: String): List<List<String>> {
if (text.isBlank()) return emptyList()
val candidates = listOf(',', ';', '\t')
val sampleLine = text.lineSequence().firstOrNull { it.isNotBlank() } ?: return emptyList()
val delimiter = candidates.maxBy { c -> sampleLine.count { it == c } }
val rows = mutableListOf<List<String>>()
var current = StringBuilder()
var inQuotes = false
val currentRow = mutableListOf<String>()
var i = 0
while (i < text.length) {
when (val ch = text[i]) {
'"' -> {
if (inQuotes && i + 1 < text.length && text[i + 1] == '"') {
current.append('"')
i++
} else {
inQuotes = !inQuotes
}
}
'\r' -> {
// ignore
}
'\n' -> {
val field = current.toString()
current = StringBuilder()
currentRow.add(field)
rows.add(currentRow.toList())
currentRow.clear()
inQuotes = false
}
else -> {
if (ch == delimiter && !inQuotes) {
val field = current.toString()
currentRow.add(field)
current = StringBuilder()
} else {
current.append(ch)
}
}
}
i++
}
if (current.isNotEmpty() || currentRow.isNotEmpty()) {
currentRow.add(current.toString())
rows.add(currentRow.toList())
}
return rows.map { row ->
row.map { it.trim().trim('"') }
}.filter { r -> r.any { it.isNotBlank() } }
}
val errorParsingTable = stringResource(R.string.error_parsing_table)
val errorParsingTableWithReason = stringResource(R.string.error_parsing_table_with_reason)
val importTableLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument(),
onResult = { uri ->
uri?.let { u ->
try {
context.contentResolver.takePersistableUriPermission(u, android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION)
} catch (_: Exception) {}
try {
val mime = context.contentResolver.getType(u)
val isExcel = mime == "application/vnd.ms-excel" ||
mime == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
if (isExcel) {
statusMessageService.showErrorById(StatusMessageId.ERROR_EXCEL_NOT_SUPPORTED)
return@let
}
context.contentResolver.openInputStream(u)?.use { inputStream ->
val text = inputStream.bufferedReader().use { it.readText() }
val rows = parseCsv(text)
if (rows.isNotEmpty() && rows.maxOf { it.size } >= 2) {
parsedTable = rows
selectedColFirst = 0
selectedColSecond = 1.coerceAtMost(rows.first().size - 1)
showTableImportDialog.value = true
parseError = null
} else {
parseError = errorParsingTable
statusMessageService.showErrorMessage(parseError!!)
}
}
} catch (e: Exception) {
parseError = e.message
statusMessageService.showErrorMessage(
(errorParsingTableWithReason + " " + e.message)
)
}
}
}
)
Box( Box(
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize().padding(16.dp),
contentAlignment = Alignment.TopCenter contentAlignment = Alignment.TopCenter
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.widthIn(max = 700.dp) // Perfect scaling for tablets/foldables .widthIn(max = 700.dp) // Perfect scaling for tablets/foldables
.fillMaxSize() .fillMaxSize()
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState()).padding(0.dp)
) { ) {
AppTopAppBar( AppTopAppBar(
title = "New Words", title = "New Words",
@@ -120,7 +238,6 @@ fun NewWordScreen(
} }
} }
}, },
modifier = Modifier.padding(horizontal = 24.dp)
) )
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
@@ -128,19 +245,151 @@ fun NewWordScreen(
AddManuallyCard( AddManuallyCard(
languageViewModel = languageViewModel, languageViewModel = languageViewModel,
vocabularyViewModel = vocabularyViewModel, vocabularyViewModel = vocabularyViewModel,
modifier = Modifier.padding(horizontal = 24.dp)
) )
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
BottomActionCardsRow( BottomActionCardsRow(
modifier = Modifier.padding(horizontal = 24.dp) onImportCsvClick = {
@Suppress("HardCodedStringLiteral")
importTableLauncher.launch(
arrayOf(
"text/csv",
"text/comma-separated-values",
"text/tab-separated-values",
"text/plain",
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
)
)
}
) )
// Extra padding at the bottom for scroll clearance // Extra padding at the bottom for scroll clearance
Spacer(modifier = Modifier.height(100.dp)) Spacer(modifier = Modifier.height(100.dp))
} }
} }
if (showTableImportDialog.value) {
AlertDialog(
onDismissRequest = { showTableImportDialog.value = false },
title = { Text(stringResource(R.string.label_import_table_csv_excel)) },
text = {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
val columnCount = parsedTable.maxOfOrNull { it.size } ?: 0
Row(verticalAlignment = Alignment.CenterVertically) {
Text(stringResource(R.string.label_first_column), modifier = Modifier.weight(1f))
var menu1Expanded by remember { mutableStateOf(false) }
AppOutlinedButton(onClick = { menu1Expanded = true }) {
Text(stringResource(R.string.label_column_n, selectedColFirst + 1))
}
DropdownMenu(expanded = menu1Expanded, onDismissRequest = { menu1Expanded = false }) {
(0 until columnCount).forEach { idx ->
val header = parsedTable.firstOrNull()?.getOrNull(idx).orEmpty()
DropdownMenuItem(
text = { Text("#${idx + 1}$header") },
onClick = { selectedColFirst = idx; menu1Expanded = false }
)
}
}
}
Row(verticalAlignment = Alignment.CenterVertically) {
Text(stringResource(R.string.label_second_column), modifier = Modifier.weight(1f))
var menu2Expanded by remember { mutableStateOf(false) }
AppOutlinedButton(onClick = { menu2Expanded = true }) {
Text(stringResource(R.string.label_column_n, selectedColSecond + 1))
}
DropdownMenu(expanded = menu2Expanded, onDismissRequest = { menu2Expanded = false }) {
(0 until columnCount).forEach { idx ->
val header = parsedTable.firstOrNull()?.getOrNull(idx).orEmpty()
DropdownMenuItem(
text = { Text("#${idx + 1}$header") },
onClick = { selectedColSecond = idx; menu2Expanded = false }
)
}
}
}
Text(stringResource(R.string.label_languages))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
Column(modifier = Modifier.weight(1f)) {
Text(stringResource(R.string.label_first_language))
SingleLanguageDropDown(
languageViewModel = languageViewModel,
selectedLanguage = selectedLangFirst,
onLanguageSelected = { selectedLangFirst = it }
)
}
Column(modifier = Modifier.weight(1f)) {
Text(stringResource(R.string.label_second_language))
SingleLanguageDropDown(
languageViewModel = languageViewModel,
selectedLanguage = selectedLangSecond,
onLanguageSelected = { selectedLangSecond = it }
)
}
}
Row(verticalAlignment = Alignment.CenterVertically) {
androidx.compose.material3.Checkbox(checked = skipHeader, onCheckedChange = { skipHeader = it })
Spacer(modifier = Modifier.width(4.dp))
Text(stringResource(R.string.label_header_row))
}
val startIdx = if (skipHeader) 1 else 0
val previewA = parsedTable.drop(startIdx).take(5).mapNotNull { it.getOrNull(selectedColFirst) }.joinToString(", ")
val previewB = parsedTable.drop(startIdx).take(5).mapNotNull { it.getOrNull(selectedColSecond) }.joinToString(", ")
Text(stringResource(R.string.label_preview_first, previewA))
Text(stringResource(R.string.label_preview_second, previewB))
val totalRows = parsedTable.drop(startIdx).count { row ->
val a = row.getOrNull(selectedColFirst).orEmpty().isNotBlank()
val b = row.getOrNull(selectedColSecond).orEmpty().isNotBlank()
a || b
}
Text(stringResource(R.string.text_rows_to_import_1d, totalRows))
}
},
confirmButton = {
val errorSelectTwoColumns = stringResource(R.string.error_select_two_columns)
val errorSelectLanguages = stringResource(R.string.error_select_languages)
val errorNoRowsToImport = stringResource(R.string.error_no_rows_to_import)
val infoImportedItemsFrom = stringResource(R.string.info_imported_items_from)
TextButton(onClick = {
if (selectedColFirst == selectedColSecond) {
statusMessageService.showErrorMessage(errorSelectTwoColumns)
return@TextButton
}
val langA = selectedLangFirst
val langB = selectedLangSecond
if (langA == null || langB == null) {
statusMessageService.showErrorMessage(errorSelectLanguages)
return@TextButton
}
val startIdx = if (skipHeader) 1 else 0
val items = parsedTable.drop(startIdx).mapNotNull { row ->
val a = row.getOrNull(selectedColFirst)?.trim().orEmpty()
val b = row.getOrNull(selectedColSecond)?.trim().orEmpty()
if (a.isBlank() && b.isBlank()) null else VocabularyItem(
id = 0,
languageFirstId = langA.nameResId,
languageSecondId = langB.nameResId,
wordFirst = a,
wordSecond = b
)
}
if (items.isEmpty()) {
statusMessageService.showErrorMessage(errorNoRowsToImport)
return@TextButton
}
vocabularyViewModel.addVocabularyItems(items)
statusMessageService.showSuccessMessage(infoImportedItemsFrom + " " + items.size)
showTableImportDialog.value = false
}) { Text(stringResource(R.string.label_import)) }
},
dismissButton = {
TextButton(onClick = { showTableImportDialog.value = false }) {
Text(stringResource(R.string.label_cancel))
}
}
)
}
} }
// --- AI GENERATOR CARD (From previous implementation) --- // --- AI GENERATOR CARD (From previous implementation) ---
@@ -157,12 +406,8 @@ fun AIGeneratorCard(
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val hints = stringArrayResource(R.array.vocabulary_hints) val hints = stringArrayResource(R.array.vocabulary_hints)
Card( AppCard(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
)
) { ) {
Column(modifier = Modifier.padding(24.dp)) { Column(modifier = Modifier.padding(24.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
@@ -293,12 +538,8 @@ fun AddManuallyCard(
val canAdd = wordText.isNotBlank() && translationText.isNotBlank() && val canAdd = wordText.isNotBlank() && translationText.isNotBlank() &&
selectedSourceLanguage != null && selectedTargetLanguage != null selectedSourceLanguage != null && selectedTargetLanguage != null
Card( AppCard(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
)
) { ) {
Column(modifier = Modifier.padding(24.dp)) { Column(modifier = Modifier.padding(24.dp)) {
// Header Row // Header Row
@@ -329,18 +570,6 @@ fun AddManuallyCard(
) )
} }
Surface(
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
shape = RoundedCornerShape(12.dp)
) {
Text(
text = languageLabel,
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp)
)
}
} }
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
@@ -378,6 +607,36 @@ fun AddManuallyCard(
singleLine = true singleLine = true
) )
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(R.string.text_select_languages),
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f))
.padding(12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
SourceLanguageDropdown(
modifier = Modifier.weight(1f),
languageViewModel = languageViewModel,
iconEnabled = false,
noBorder = true
)
TargetLanguageDropdown(
modifier = Modifier.weight(1f),
languageViewModel = languageViewModel,
iconEnabled = false,
noBorder = true
)
}
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
// Add to List Button (Darker variant) // Add to List Button (Darker variant)
@@ -410,22 +669,22 @@ fun AddManuallyCard(
} }
@Composable @Composable
fun BottomActionCardsRow(modifier: Modifier = Modifier) { fun BottomActionCardsRow(
modifier: Modifier = Modifier,
onImportCsvClick: () -> Unit
) {
Row( Row(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp) horizontalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
// Explore Packs Card // Explore Packs Card
Card( AppCard(
modifier = Modifier.weight(1f).height(120.dp), modifier = Modifier.weight(1f).height(120.dp),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
),
onClick = { /* TODO: Navigate to Explore */ }
) { ) {
Column( Column(
modifier = Modifier.fillMaxSize(), modifier = Modifier
.fillMaxSize()
.alpha(0.6f),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
@@ -446,19 +705,22 @@ fun BottomActionCardsRow(modifier: Modifier = Modifier) {
Text( Text(
text = "Explore Packs", text = "Explore Packs",
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(6.dp))
Text(
text = "Coming soon",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
) )
} }
} }
// Import CSV Card // Import CSV Card
Card( AppCard(
modifier = Modifier.weight(1f).height(120.dp), modifier = Modifier.weight(1f).height(120.dp),
shape = RoundedCornerShape(20.dp), onClick = onImportCsvClick
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
),
onClick = { /* TODO: Navigate to Import */ }
) { ) {
Column( Column(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),