From a7c83bb84645fd17d9e42fb278a404e3aea3c49a Mon Sep 17 00:00:00 2001 From: jonasgaudian <43753916+jonasgaudian@users.noreply.github.com> Date: Mon, 16 Feb 2026 22:22:11 +0100 Subject: [PATCH] implement CSV import for new words and refactor UI components to use `AppCard` --- .../view/composable/ComponentLibrary.kt | 29 +- .../translator/view/home/HomeScreen.kt | 32 +- .../view/vocabulary/NewWordScreen.kt | 352 +++++++++++++++--- 3 files changed, 338 insertions(+), 75 deletions(-) diff --git a/app/src/main/java/eu/gaudian/translator/view/composable/ComponentLibrary.kt b/app/src/main/java/eu/gaudian/translator/view/composable/ComponentLibrary.kt index 3fed94a..1a14588 100644 --- a/app/src/main/java/eu/gaudian/translator/view/composable/ComponentLibrary.kt +++ b/app/src/main/java/eu/gaudian/translator/view/composable/ComponentLibrary.kt @@ -101,6 +101,7 @@ fun AppCard( text: String? = null, expandable: Boolean = false, initiallyExpanded: Boolean = false, + onClick: (() -> Unit)? = null, content: @Composable ColumnScope.() -> Unit, ) { var isExpanded by remember { mutableStateOf(initiallyExpanded) } @@ -113,6 +114,7 @@ fun AppCard( // Check if we need to render the header row // Updated to include icon in the check val hasHeader = title != null || text != null || expandable || icon != null + val canClickHeader = expandable || onClick != null Surface( modifier = modifier @@ -133,7 +135,12 @@ fun AppCard( Row( modifier = Modifier .fillMaxWidth() - .clickable(enabled = expandable) { isExpanded = !isExpanded } + .clickable(enabled = canClickHeader) { + if (expandable) { + isExpanded = !isExpanded + } + onClick?.invoke() + } .padding(ComponentDefaults.CardPadding), verticalAlignment = Alignment.CenterVertically ) { @@ -186,17 +193,27 @@ fun AppCard( // --- Content Area --- if (!expandable || isExpanded) { - Column( - modifier = Modifier.padding( + val contentModifier = Modifier + .padding( start = ComponentDefaults.CardPadding, end = ComponentDefaults.CardPadding, bottom = ComponentDefaults.CardPadding, // 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. top = if (hasHeader) 0.dp else ComponentDefaults.CardPadding - ), - content = content - ) + ) + + if (!hasHeader && onClick != null) { + Column( + modifier = contentModifier.clickable { onClick() }, + content = content + ) + } else { + Column( + modifier = contentModifier, + content = content + ) + } } } } diff --git a/app/src/main/java/eu/gaudian/translator/view/home/HomeScreen.kt b/app/src/main/java/eu/gaudian/translator/view/home/HomeScreen.kt index 719881f..cea118a 100644 --- a/app/src/main/java/eu/gaudian/translator/view/home/HomeScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/home/HomeScreen.kt @@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AddCircle 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.Settings 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.Icon import androidx.compose.material3.IconButton @@ -47,6 +44,7 @@ import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation.NavHostController 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.vocabulary.widgets.WeeklyActivityChartWidget import eu.gaudian.translator.viewmodel.ProgressViewModel @@ -177,10 +175,8 @@ fun StatCard( title: String, subtitle: String ) { - Card( + AppCard( modifier = modifier, - shape = RoundedCornerShape(20.dp), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant) ) { Column( modifier = Modifier.padding(20.dp), @@ -208,10 +204,8 @@ fun GoalCard( title: String, subtitle: String ) { - Card( + AppCard( modifier = modifier, - shape = RoundedCornerShape(20.dp), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant) ) { Column( modifier = Modifier.padding(20.dp), @@ -269,19 +263,15 @@ fun ActionCard( } if (onClick != null) { - Card( + AppCard( modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(20.dp), - colors = CardDefaults.cardColors(containerColor = containerColor, contentColor = contentColor), onClick = onClick ) { cardContent() } } else { - Card( + AppCard( modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(20.dp), - colors = CardDefaults.cardColors(containerColor = containerColor, contentColor = contentColor) ) { cardContent() } @@ -311,10 +301,8 @@ fun WeeklyProgressSection( Spacer(modifier = Modifier.height(8.dp)) - Card( + AppCard( modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(20.dp), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant) ) { if (weeklyActivityStats.isEmpty()) { Column( @@ -344,10 +332,8 @@ fun BottomStatsSection() { horizontalArrangement = Arrangement.spacedBy(16.dp) ) { // Total Words - Card( + AppCard( modifier = Modifier.weight(1f), - shape = RoundedCornerShape(20.dp), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant) ) { Column(modifier = Modifier.padding(20.dp)) { Text(text = "TOTAL WORDS", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)) @@ -363,10 +349,8 @@ fun BottomStatsSection() { } // Accuracy - Card( + AppCard( modifier = Modifier.weight(1f), - shape = RoundedCornerShape(20.dp), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant) ) { Column(modifier = Modifier.padding(20.dp)) { Text(text = "ACCURACY", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)) diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/NewWordScreen.kt b/app/src/main/java/eu/gaudian/translator/view/vocabulary/NewWordScreen.kt index 59c7c84..73c0b8e 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/NewWordScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/vocabulary/NewWordScreen.kt @@ -1,5 +1,7 @@ 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.layout.Arrangement 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.EditNote import androidx.compose.material.icons.filled.LibraryBooks -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults +import androidx.compose.material3.AlertDialog import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable @@ -36,12 +39,14 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color 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.navigation.NavHostController import eu.gaudian.translator.R +import eu.gaudian.translator.model.Language 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.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.AppTopAppBar 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.TargetLanguageDropdown 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>>(emptyList()) } + var selectedColFirst by remember { mutableIntStateOf(0) } + var selectedColSecond by remember { mutableIntStateOf(1) } + var skipHeader by remember { mutableStateOf(true) } + var selectedLangFirst by remember { mutableStateOf(null) } + var selectedLangSecond by remember { mutableStateOf(null) } + var parseError by remember { mutableStateOf(null) } + + fun parseCsv(text: String): List> { + 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>() + var current = StringBuilder() + var inQuotes = false + val currentRow = mutableListOf() + + 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( - modifier = modifier.fillMaxSize(), + modifier = modifier.fillMaxSize().padding(16.dp), contentAlignment = Alignment.TopCenter ) { Column( modifier = Modifier .widthIn(max = 700.dp) // Perfect scaling for tablets/foldables .fillMaxSize() - .verticalScroll(rememberScrollState()) + .verticalScroll(rememberScrollState()).padding(0.dp) ) { AppTopAppBar( title = "New Words", @@ -120,7 +238,6 @@ fun NewWordScreen( } } }, - modifier = Modifier.padding(horizontal = 24.dp) ) Spacer(modifier = Modifier.height(24.dp)) @@ -128,19 +245,151 @@ fun NewWordScreen( AddManuallyCard( languageViewModel = languageViewModel, vocabularyViewModel = vocabularyViewModel, - modifier = Modifier.padding(horizontal = 24.dp) ) Spacer(modifier = Modifier.height(24.dp)) 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 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) --- @@ -157,12 +406,8 @@ fun AIGeneratorCard( modifier: Modifier = Modifier ) { val hints = stringArrayResource(R.array.vocabulary_hints) - Card( + AppCard( modifier = modifier.fillMaxWidth(), - shape = RoundedCornerShape(24.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) - ) ) { Column(modifier = Modifier.padding(24.dp)) { Row(verticalAlignment = Alignment.CenterVertically) { @@ -293,12 +538,8 @@ fun AddManuallyCard( val canAdd = wordText.isNotBlank() && translationText.isNotBlank() && selectedSourceLanguage != null && selectedTargetLanguage != null - Card( + AppCard( modifier = modifier.fillMaxWidth(), - shape = RoundedCornerShape(24.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) - ) ) { Column(modifier = Modifier.padding(24.dp)) { // 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)) @@ -378,6 +607,36 @@ fun AddManuallyCard( 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)) // Add to List Button (Darker variant) @@ -410,22 +669,22 @@ fun AddManuallyCard( } @Composable -fun BottomActionCardsRow(modifier: Modifier = Modifier) { +fun BottomActionCardsRow( + modifier: Modifier = Modifier, + onImportCsvClick: () -> Unit +) { Row( modifier = modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp) ) { // Explore Packs Card - Card( + AppCard( 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( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .alpha(0.6f), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { @@ -446,19 +705,22 @@ fun BottomActionCardsRow(modifier: Modifier = Modifier) { Text( text = "Explore Packs", 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 - Card( + AppCard( 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 Import */ } + onClick = onImportCsvClick ) { Column( modifier = Modifier.fillMaxSize(),