Compare commits
6 Commits
47d7e01f7f
...
059e5d9d3f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
059e5d9d3f | ||
|
|
3e3d6d9cd1 | ||
|
|
a7c83bb846 | ||
|
|
70e416d5e1 | ||
|
|
84cad31810 | ||
|
|
89ac7cd9eb |
@@ -41,6 +41,8 @@ import eu.gaudian.translator.view.vocabulary.CategoryDetailScreen
|
||||
import eu.gaudian.translator.view.vocabulary.CategoryListScreen
|
||||
import eu.gaudian.translator.view.vocabulary.LanguageProgressScreen
|
||||
import eu.gaudian.translator.view.vocabulary.MainVocabularyScreen
|
||||
import eu.gaudian.translator.view.vocabulary.NewWordReviewScreen
|
||||
import eu.gaudian.translator.view.vocabulary.NewWordScreen
|
||||
import eu.gaudian.translator.view.vocabulary.NoGrammarItemsScreen
|
||||
import eu.gaudian.translator.view.vocabulary.StageDetailScreen
|
||||
import eu.gaudian.translator.view.vocabulary.VocabularyCardHost
|
||||
@@ -157,6 +159,12 @@ fun NavGraphBuilder.homeGraph(navController: NavHostController) {
|
||||
composable("main_home") {
|
||||
HomeScreen(navController = navController)
|
||||
}
|
||||
composable("new_word") {
|
||||
NewWordScreen(navController = navController)
|
||||
}
|
||||
composable("new_word_review") {
|
||||
NewWordReviewScreen(navController = navController)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,13 @@ import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBackIosNew
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
@@ -94,17 +96,15 @@ fun AppTopAppBar(
|
||||
if (onNavigateBack != null) {
|
||||
IconButton(
|
||||
onClick = onNavigateBack,
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
// This tells the button to paint its own circular background natively
|
||||
colors = IconButtonDefaults.iconButtonColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
||||
contentColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
|
||||
) {
|
||||
Icon(
|
||||
imageVector = AppIcons.ArrowBack,
|
||||
contentDescription = stringResource(R.string.cd_navigate_back)
|
||||
// Notice we removed the 'tint' here, as contentColor handles it perfectly now!
|
||||
imageVector = Icons.Default.ArrowBackIosNew,
|
||||
contentDescription = "Back",
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
} else if (navigationIcon != null) {
|
||||
|
||||
@@ -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
|
||||
@@ -125,7 +127,7 @@ fun AppCard(
|
||||
// Animate height changes when expanding/collapsing
|
||||
.animateContentSize(),
|
||||
shape = ComponentDefaults.CardShape,
|
||||
color = MaterialTheme.colorScheme.surfaceContainer
|
||||
color = MaterialTheme.colorScheme.surfaceContainer,
|
||||
) {
|
||||
Column {
|
||||
// --- Header Row ---
|
||||
@@ -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
|
||||
),
|
||||
)
|
||||
|
||||
if (!hasHeader && onClick != null) {
|
||||
Column(
|
||||
modifier = contentModifier.clickable { onClick() },
|
||||
content = content
|
||||
)
|
||||
} else {
|
||||
Column(
|
||||
modifier = contentModifier,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -34,15 +31,23 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
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
|
||||
|
||||
@Composable
|
||||
fun HomeScreen(
|
||||
@@ -58,9 +63,10 @@ fun HomeScreen(
|
||||
modifier = Modifier
|
||||
.widthIn(max = 700.dp) // Prevents extreme stretching on tablets
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 20.dp, vertical = 24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp)
|
||||
.padding(horizontal = 16.dp, vertical = 0.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(15.dp)
|
||||
) {
|
||||
item { Spacer(modifier = Modifier.height(16.dp)) }
|
||||
item { TopProfileSection(navController = navController) }
|
||||
item { StreakAndGoalSection() }
|
||||
item {
|
||||
@@ -78,10 +84,11 @@ fun HomeScreen(
|
||||
subtitle = "Expand your vocabulary",
|
||||
icon = Icons.Default.AddCircle,
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
contentColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
onClick = { navController.navigate("new_word") }
|
||||
)
|
||||
}
|
||||
item { WeeklyProgressSection() }
|
||||
item { WeeklyProgressSection(navController = navController) }
|
||||
item { BottomStatsSection() }
|
||||
|
||||
// Bottom padding for edge-to-edge screens
|
||||
@@ -168,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),
|
||||
@@ -199,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),
|
||||
@@ -233,13 +236,10 @@ fun ActionCard(
|
||||
subtitle: String,
|
||||
icon: ImageVector,
|
||||
containerColor: Color,
|
||||
contentColor: Color
|
||||
contentColor: Color,
|
||||
onClick: (() -> Unit)? = null
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = containerColor, contentColor = contentColor)
|
||||
) {
|
||||
val cardContent: @Composable () -> Unit = {
|
||||
Row(
|
||||
modifier = Modifier.padding(20.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
@@ -261,53 +261,65 @@ fun ActionCard(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (onClick != null) {
|
||||
AppCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = onClick
|
||||
) {
|
||||
cardContent()
|
||||
}
|
||||
} else {
|
||||
AppCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
cardContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WeeklyProgressSection() {
|
||||
Column {
|
||||
fun WeeklyProgressSection(
|
||||
navController: NavHostController,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val viewModel: ProgressViewModel = hiltViewModel(activity)
|
||||
val weeklyActivityStats by viewModel.weeklyActivityStats.collectAsState()
|
||||
|
||||
Column(modifier = modifier) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(text = "Weekly Progress", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||
TextButton(onClick = { /* TODO */ }) {
|
||||
TextButton(onClick = { navController.navigate("vocabulary_heatmap") }) {
|
||||
Text("See History")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(200.dp), // Fixed height for dummy chart area
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
|
||||
AppCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
if (weeklyActivityStats.isEmpty()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.Bottom
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
// Dummy Chart Graph Space
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
// Days row
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
listOf("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun").forEach { day ->
|
||||
Text(
|
||||
text = day,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||
text = "No activity data available",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
WeeklyActivityChartWidget(weeklyStats = weeklyActivityStats)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -320,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))
|
||||
@@ -339,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))
|
||||
|
||||
@@ -385,14 +385,14 @@ fun VocabularyCard(
|
||||
Card(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.combinedClickable(
|
||||
onClick = onItemClick,
|
||||
onLongClick = onItemLongClick
|
||||
),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = if (isSelected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant
|
||||
containerColor = if (isSelected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceContainer,
|
||||
contentColor = if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.surfaceContainer
|
||||
),
|
||||
border = if (isSelected) BorderStroke(2.dp, MaterialTheme.colorScheme.primary) else null
|
||||
) {
|
||||
|
||||
@@ -60,9 +60,12 @@ import androidx.navigation.NavHostController
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.model.VocabularyStage
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.composable.AppDropDownMenu
|
||||
import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppSwitch
|
||||
import eu.gaudian.translator.view.composable.LargeDropdownMenuItem
|
||||
import eu.gaudian.translator.view.composable.MultipleLanguageDropdown
|
||||
import eu.gaudian.translator.view.dialogs.AddCategoryDialog
|
||||
import eu.gaudian.translator.view.dialogs.CategorySelectionDialog
|
||||
import eu.gaudian.translator.view.dialogs.StageSelectionDialog
|
||||
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
||||
@@ -106,6 +109,8 @@ fun LibraryScreen(
|
||||
|
||||
var showCategoryDialog by remember { mutableStateOf(false) }
|
||||
var showStageDialog by remember { mutableStateOf(false) }
|
||||
var showAddMenu by remember { mutableStateOf(false) }
|
||||
var showAddCategoryDialog by remember { mutableStateOf(false) }
|
||||
|
||||
var isCategoriesView by remember { mutableStateOf(false) }
|
||||
|
||||
@@ -164,7 +169,7 @@ fun LibraryScreen(
|
||||
modifier = Modifier
|
||||
.widthIn(max = 700.dp)
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 24.dp),
|
||||
.padding(horizontal = 16.dp),
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = isHeaderVisible,
|
||||
@@ -193,7 +198,7 @@ fun LibraryScreen(
|
||||
)
|
||||
} else {
|
||||
LibraryTopBar(
|
||||
onAddClick = { /* TODO: Add new card/category */ }
|
||||
onAddClick = { showAddMenu = true }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -323,6 +328,36 @@ fun LibraryScreen(
|
||||
)
|
||||
}
|
||||
|
||||
if (showAddMenu) {
|
||||
AppDropDownMenu(
|
||||
expanded = showAddMenu,
|
||||
onDismissRequest = { showAddMenu = false }
|
||||
) {
|
||||
LargeDropdownMenuItem(
|
||||
text = stringResource(R.string.label_add_vocabulary),
|
||||
selected = false,
|
||||
enabled = true,
|
||||
onClick = {
|
||||
showAddMenu = false
|
||||
navController.navigate("new_word")
|
||||
}
|
||||
)
|
||||
LargeDropdownMenuItem(
|
||||
text = stringResource(R.string.label_add_category),
|
||||
selected = false,
|
||||
enabled = true,
|
||||
onClick = {
|
||||
showAddMenu = false
|
||||
showAddCategoryDialog = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showAddCategoryDialog) {
|
||||
AddCategoryDialog(onDismiss = { showAddCategoryDialog = false })
|
||||
}
|
||||
|
||||
if (showStageDialog) {
|
||||
val selectedItems = vocabularyItems.filter { selection.contains(it.id.toLong()) }
|
||||
StageSelectionDialog(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,288 @@
|
||||
package eu.gaudian.translator.view.vocabulary
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.WarningAmber
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
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.VocabularyCategory
|
||||
import eu.gaudian.translator.model.VocabularyItem
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.composable.AppButton
|
||||
import eu.gaudian.translator.view.composable.AppCheckbox
|
||||
import eu.gaudian.translator.view.composable.AppScaffold
|
||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||
import eu.gaudian.translator.view.dialogs.CategoryDropdown
|
||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||
|
||||
@Composable
|
||||
fun NewWordReviewScreen(
|
||||
navController: NavHostController,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(activity)
|
||||
|
||||
val generatedItems by vocabularyViewModel.generatedVocabularyItems.collectAsState()
|
||||
val selectedItems = remember { mutableStateListOf<VocabularyItem>() }
|
||||
val duplicates = remember { mutableStateListOf<Boolean>() }
|
||||
var selectedCategories by remember { mutableStateOf<List<VocabularyCategory?>>(emptyList()) }
|
||||
|
||||
LaunchedEffect(generatedItems) {
|
||||
val duplicateResults = vocabularyViewModel.findDuplicates(generatedItems)
|
||||
duplicates.clear()
|
||||
duplicates.addAll(duplicateResults)
|
||||
selectedItems.clear()
|
||||
selectedItems.addAll(generatedItems.filterIndexed { index, _ -> !duplicateResults[index] })
|
||||
}
|
||||
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
AppTopAppBar(
|
||||
title = stringResource(R.string.found_items),
|
||||
onNavigateBack = { navController.popBackStack() }
|
||||
)
|
||||
},
|
||||
modifier = modifier.fillMaxSize()
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
SummaryHeader(
|
||||
totalCount = generatedItems.size,
|
||||
selectedCount = selectedItems.size,
|
||||
modifier = Modifier.padding(horizontal = 20.dp, vertical = 12.dp)
|
||||
)
|
||||
|
||||
if (generatedItems.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(24.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.text_no_data_available),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
} else {
|
||||
ReviewList(
|
||||
generatedItems = generatedItems,
|
||||
selectedItems = selectedItems,
|
||||
duplicates = duplicates,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = 16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.select_list_optional),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
modifier = Modifier.padding(horizontal = 20.dp, vertical = 8.dp)
|
||||
)
|
||||
CategoryDropdown(
|
||||
onCategorySelected = { selectedCategories = it },
|
||||
noneSelectable = false,
|
||||
multipleSelectable = true,
|
||||
onlyLists = true,
|
||||
addCategory = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp)
|
||||
)
|
||||
|
||||
ActionRow(
|
||||
selectedCount = selectedItems.size,
|
||||
onCancel = { navController.popBackStack() },
|
||||
onConfirm = {
|
||||
val selectedCategoryIds = selectedCategories.filterNotNull().map { it.id }
|
||||
vocabularyViewModel.addVocabularyItems(selectedItems.toList(), selectedCategoryIds)
|
||||
navController.popBackStack("new_word", inclusive = false)
|
||||
},
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SummaryHeader(
|
||||
totalCount: Int,
|
||||
selectedCount: Int,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f))
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.found_items),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.text_amount_2d, totalCount),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
Text(
|
||||
text = stringResource(R.string.label_add_, selectedCount),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.select_items_to_add),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ReviewList(
|
||||
generatedItems: List<VocabularyItem>,
|
||||
selectedItems: MutableList<VocabularyItem>,
|
||||
duplicates: List<Boolean>,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val duplicateLabel = stringResource(R.string.duplicate)
|
||||
LazyColumn(modifier = modifier) {
|
||||
itemsIndexed(generatedItems) { index, item ->
|
||||
val isDuplicate = duplicates.getOrNull(index) == true
|
||||
val isSelected = selectedItems.contains(item)
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 6.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = if (isDuplicate) {
|
||||
MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.25f)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.25f)
|
||||
}
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
AppCheckbox(
|
||||
checked = isSelected,
|
||||
onCheckedChange = { checked ->
|
||||
if (checked) {
|
||||
selectedItems.add(item)
|
||||
} else {
|
||||
selectedItems.remove(item)
|
||||
}
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.size(12.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(text = item.wordFirst, style = MaterialTheme.typography.titleMedium)
|
||||
Text(text = item.wordSecond, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
if (isDuplicate) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(MaterialTheme.colorScheme.error.copy(alpha = 0.15f))
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.WarningAmber,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Spacer(modifier = Modifier.size(4.dp))
|
||||
Text(
|
||||
text = duplicateLabel,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ActionRow(
|
||||
selectedCount: Int,
|
||||
onCancel: () -> Unit,
|
||||
onConfirm: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
TextButton(onClick = onCancel) {
|
||||
Text(stringResource(R.string.label_cancel))
|
||||
}
|
||||
AppButton(
|
||||
onClick = onConfirm,
|
||||
enabled = selectedCount > 0
|
||||
) {
|
||||
Text(stringResource(R.string.label_add_, selectedCount))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,792 @@
|
||||
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
|
||||
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.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
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.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.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
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
|
||||
import androidx.compose.ui.res.stringArrayResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
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.SourceLanguageDropdown
|
||||
import eu.gaudian.translator.view.composable.TargetLanguageDropdown
|
||||
import eu.gaudian.translator.view.library.VocabularyCard
|
||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun NewWordScreen(
|
||||
navController: NavHostController,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val isGenerating by vocabularyViewModel.isGenerating.collectAsState()
|
||||
val generatedItems by vocabularyViewModel.generatedVocabularyItems.collectAsState()
|
||||
val allLanguages by languageViewModel.allLanguages.collectAsState()
|
||||
val recentItems by vocabularyViewModel.vocabularyItems.collectAsState()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var category by remember { mutableStateOf("") }
|
||||
var amount by remember { mutableFloatStateOf(8f) }
|
||||
var navigateToReview by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(isGenerating, generatedItems, navigateToReview) {
|
||||
if (navigateToReview && !isGenerating) {
|
||||
if (generatedItems.isNotEmpty()) {
|
||||
navController.navigate("new_word_review")
|
||||
}
|
||||
navigateToReview = false
|
||||
}
|
||||
}
|
||||
|
||||
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) }
|
||||
|
||||
val recentlyAdded = remember(recentItems) {
|
||||
recentItems.sortedByDescending { it.id }.take(4)
|
||||
}
|
||||
|
||||
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(
|
||||
modifier = modifier.fillMaxSize().padding(16.dp),
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.widthIn(max = 700.dp) // Perfect scaling for tablets/foldables
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState()).padding(0.dp)
|
||||
) {
|
||||
AppTopAppBar(
|
||||
title = "New Words",
|
||||
onNavigateBack = { navController.popBackStack() }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
AIGeneratorCard(
|
||||
category = category,
|
||||
onCategoryChange = { category = it },
|
||||
amount = amount,
|
||||
onAmountChange = { amount = it },
|
||||
languageViewModel = languageViewModel,
|
||||
isGenerating = isGenerating,
|
||||
onGenerate = {
|
||||
if (category.isNotBlank() && !isGenerating) {
|
||||
coroutineScope.launch {
|
||||
vocabularyViewModel.generateVocabularyItems(category.trim(), amount.toInt())
|
||||
navigateToReview = true
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
AddManuallyCard(
|
||||
languageViewModel = languageViewModel,
|
||||
vocabularyViewModel = vocabularyViewModel,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
BottomActionCardsRow(
|
||||
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"
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
if (recentlyAdded.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Recently Added",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
TextButton(onClick = { navController.navigate("library") }) {
|
||||
Text("View All")
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
recentlyAdded.forEach { item ->
|
||||
VocabularyCard(
|
||||
item = item,
|
||||
allLanguages = allLanguages,
|
||||
isSelected = false,
|
||||
onItemClick = { navController.navigate("vocabulary_detail/${item.id}") },
|
||||
onItemLongClick = {},
|
||||
onDeleteClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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))
|
||||
eu.gaudian.translator.view.composable.SingleLanguageDropDown(
|
||||
languageViewModel = languageViewModel,
|
||||
selectedLanguage = selectedLangFirst,
|
||||
onLanguageSelected = { selectedLangFirst = it }
|
||||
)
|
||||
}
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(stringResource(R.string.label_second_language))
|
||||
eu.gaudian.translator.view.composable.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) ---
|
||||
|
||||
@Composable
|
||||
fun AIGeneratorCard(
|
||||
category: String,
|
||||
onCategoryChange: (String) -> Unit,
|
||||
amount: Float,
|
||||
onAmountChange: (Float) -> Unit,
|
||||
languageViewModel: LanguageViewModel,
|
||||
isGenerating: Boolean,
|
||||
onGenerate: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val hints = stringArrayResource(R.array.vocabulary_hints)
|
||||
AppCard(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(24.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.AutoAwesome,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "AI Generator",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.text_search_term),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
InspiringSearchField(
|
||||
value = category,
|
||||
hints = hints,
|
||||
onValueChange = onCategoryChange
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.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(),
|
||||
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))
|
||||
Text(
|
||||
text = stringResource(R.string.text_select_amount),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
AppSlider(
|
||||
value = amount,
|
||||
onValueChange = onAmountChange,
|
||||
valueRange = 1f..25f,
|
||||
steps = 24,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.text_amount_2d, amount.toInt()),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
if (isGenerating) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
AppButton(
|
||||
onClick = onGenerate,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
enabled = category.isNotBlank() && !isGenerating
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(imageVector = Icons.Default.AutoAwesome, contentDescription = null, modifier = Modifier.size(20.dp))
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.text_generate),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- NEW COMPONENTS START HERE ---
|
||||
|
||||
@Composable
|
||||
fun AddManuallyCard(
|
||||
languageViewModel: LanguageViewModel,
|
||||
vocabularyViewModel: VocabularyViewModel,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var wordText by remember { mutableStateOf("") }
|
||||
var translationText by remember { mutableStateOf("") }
|
||||
val selectedSourceLanguage by languageViewModel.selectedSourceLanguage.collectAsState()
|
||||
val selectedTargetLanguage by languageViewModel.selectedTargetLanguage.collectAsState()
|
||||
|
||||
val languageLabel = when {
|
||||
selectedSourceLanguage != null && selectedTargetLanguage != null ->
|
||||
"${selectedSourceLanguage?.name} → ${selectedTargetLanguage?.name}"
|
||||
else -> stringResource(R.string.text_select_languages)
|
||||
}
|
||||
|
||||
val canAdd = wordText.isNotBlank() && translationText.isNotBlank() &&
|
||||
selectedSourceLanguage != null && selectedTargetLanguage != null
|
||||
|
||||
AppCard(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(24.dp)) {
|
||||
// Header Row
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.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))
|
||||
Text(
|
||||
text = stringResource(R.string.label_add_vocabulary),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Input Fields
|
||||
TextField(
|
||||
value = wordText,
|
||||
onValueChange = { wordText = it },
|
||||
placeholder = { Text(stringResource(R.string.text_label_word), color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)) },
|
||||
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))
|
||||
|
||||
TextField(
|
||||
value = translationText,
|
||||
onValueChange = { translationText = it },
|
||||
placeholder = { Text(stringResource(R.string.text_translation), color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)) },
|
||||
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))
|
||||
|
||||
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)
|
||||
AppButton(
|
||||
onClick = {
|
||||
val newItem = VocabularyItem(
|
||||
languageFirstId = selectedSourceLanguage?.nameResId,
|
||||
languageSecondId = selectedTargetLanguage?.nameResId,
|
||||
wordFirst = wordText.trim(),
|
||||
wordSecond = translationText.trim(),
|
||||
id = 0
|
||||
)
|
||||
vocabularyViewModel.addVocabularyItems(listOf(newItem))
|
||||
wordText = ""
|
||||
translationText = ""
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
enabled = canAdd
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.label_add),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BottomActionCardsRow(
|
||||
modifier: Modifier = Modifier,
|
||||
onImportCsvClick: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Explore Packs Card
|
||||
AppCard(
|
||||
modifier = Modifier.weight(1f).height(120.dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.alpha(0.6f),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.LibraryBooks,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(
|
||||
text = "Explore Packs",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
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
|
||||
AppCard(
|
||||
modifier = Modifier.weight(1f).height(120.dp),
|
||||
onClick = onImportCsvClick
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.DriveFolderUpload,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(
|
||||
text = "Import CSV",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user