From 3e3d6d9cd105cc15b98322ae1735f6e7d9f0c877 Mon Sep 17 00:00:00 2001 From: jonasgaudian <43753916+jonasgaudian@users.noreply.github.com> Date: Mon, 16 Feb 2026 22:39:56 +0100 Subject: [PATCH] delete `NewVocListScreen.kt`, update `NewWordScreen` to display recently added items, and refactor `VocabularyCard` styling in `LibraryComponents.kt`. --- .../view/composable/ComponentLibrary.kt | 2 +- .../view/library/LibraryComponents.kt | 6 +- .../view/vocabulary/NewVocListScreen.kt | 1065 ----------------- .../view/vocabulary/NewWordScreen.kt | 46 +- 4 files changed, 47 insertions(+), 1072 deletions(-) delete mode 100644 app/src/main/java/eu/gaudian/translator/view/vocabulary/NewVocListScreen.kt 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 1a14588..3c771b6 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 @@ -127,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 --- diff --git a/app/src/main/java/eu/gaudian/translator/view/library/LibraryComponents.kt b/app/src/main/java/eu/gaudian/translator/view/library/LibraryComponents.kt index c727ae5..fb4f5eb 100644 --- a/app/src/main/java/eu/gaudian/translator/view/library/LibraryComponents.kt +++ b/app/src/main/java/eu/gaudian/translator/view/library/LibraryComponents.kt @@ -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 ) { diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/NewVocListScreen.kt b/app/src/main/java/eu/gaudian/translator/view/vocabulary/NewVocListScreen.kt deleted file mode 100644 index 2cce7fd..0000000 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/NewVocListScreen.kt +++ /dev/null @@ -1,1065 +0,0 @@ -@file:Suppress("HardCodedStringLiteral", "AssignedValueIsNeverRead") - -package eu.gaudian.translator.view.vocabulary - -import android.os.Parcelable -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.Crossfade -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBarsPadding -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.lazy.LazyColumn -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.GridItemSpan -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -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.Add -import androidx.compose.material.icons.filled.AddCircleOutline -import androidx.compose.material.icons.filled.CheckCircle -import androidx.compose.material.icons.filled.LocalMall -import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material.icons.filled.Search -import androidx.compose.material.icons.filled.Tune -import androidx.compose.material3.BottomSheetDefaults -import androidx.compose.material3.Button -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.FilterChip -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.material3.rememberModalBottomSheetState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -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.draw.drawBehind -import androidx.compose.ui.geometry.CornerRadius -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.PathEffect -import androidx.compose.ui.graphics.drawscope.Stroke -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavHostController -import eu.gaudian.translator.R -import eu.gaudian.translator.model.Language -import eu.gaudian.translator.model.VocabularyCategory -import eu.gaudian.translator.model.VocabularyItem -import eu.gaudian.translator.model.VocabularyStage -import eu.gaudian.translator.utils.findActivity -import eu.gaudian.translator.view.composable.AppIcons -import eu.gaudian.translator.view.composable.AppSwitch -import eu.gaudian.translator.view.composable.MultipleLanguageDropdown -import eu.gaudian.translator.view.composable.insertBreakOpportunities -import eu.gaudian.translator.view.dialogs.CategorySelectionDialog -import eu.gaudian.translator.view.dialogs.StageSelectionDialog -import eu.gaudian.translator.viewmodel.CategoryViewModel -import eu.gaudian.translator.viewmodel.LanguageConfigViewModel -import eu.gaudian.translator.viewmodel.LanguageViewModel -import eu.gaudian.translator.viewmodel.VocabularyViewModel -import eu.gaudian.translator.viewmodel.VocabularyViewModel.SortOrder -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.launch -import kotlinx.parcelize.Parcelize - -@Parcelize -data class NewVocabularyFilterState( - val searchQuery: String = "", - val selectedStage: VocabularyStage? = null, - val sortOrder: SortOrder = SortOrder.NEWEST_FIRST, - val categoryIds: List = emptyList(), - val dueTodayOnly: Boolean = false, - val selectedLanguageIds: List = emptyList(), - val selectedWordClass: String? = null -) : Parcelable - -@Composable -fun NewVocListScreen( - categoryId: Int? = null, - showDueTodayOnly: Boolean? = null, - stage: VocabularyStage? = null, - onNavigateToItem: (VocabularyItem) -> Unit?, - onNavigateBack: (() -> Unit)? = null, - navController: NavHostController? = null, - itemsToShow: List = emptyList(), - isRemoveFromCategoryEnabled: Boolean = false, - showTopBar: Boolean = true, - enableNavigationButtons: Boolean = false, - modifier: Modifier = Modifier -) { - val scope = rememberCoroutineScope() - val activity = LocalContext.current.findActivity() - val lazyListState = rememberLazyListState() - val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity) - val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity) - val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity) - val context = LocalContext.current - - var filterState by rememberSaveable { - mutableStateOf( - NewVocabularyFilterState( - categoryIds = categoryId?.let { listOf(it) } ?: emptyList(), - dueTodayOnly = showDueTodayOnly == true, - selectedStage = stage - ) - ) - } - - var showFilterSheet by remember { mutableStateOf(false) } - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - - var selection by remember { mutableStateOf>(emptySet()) } - val isInSelectionMode = selection.isNotEmpty() - - var showCategoryDialog by remember { mutableStateOf(false) } - var showStageDialog by remember { mutableStateOf(false) } - - var isCategoriesView by remember { mutableStateOf(false) } - - val allLanguages by languageViewModel.allLanguages.collectAsStateWithLifecycle(initialValue = emptyList()) - val languagesPresent by vocabularyViewModel.languagesPresent.collectAsStateWithLifecycle(initialValue = emptySet()) - val categories by vocabularyViewModel.categories.collectAsStateWithLifecycle() - - val vocabularyItemsFlow: Flow> = remember(filterState) { - vocabularyViewModel.filterVocabularyItems( - languages = filterState.selectedLanguageIds, - query = filterState.searchQuery.takeIf { it.isNotBlank() }, - categoryIds = filterState.categoryIds, - stage = filterState.selectedStage, - wordClass = filterState.selectedWordClass, - dueTodayOnly = filterState.dueTodayOnly, - sortOrder = filterState.sortOrder - ) - } - - val vocabularyItems: List = itemsToShow.ifEmpty { - vocabularyItemsFlow.collectAsStateWithLifecycle(initialValue = emptyList()).value - } - - LaunchedEffect(categoryId, showDueTodayOnly, stage) { - filterState = filterState.copy( - categoryIds = categoryId?.let { listOf(it) } ?: emptyList(), - dueTodayOnly = showDueTodayOnly == true, - selectedStage = stage - ) - } - - // Set navigation context when navigation buttons are enabled - LaunchedEffect(vocabularyItems, enableNavigationButtons) { - if (enableNavigationButtons && vocabularyItems.isNotEmpty()) { - vocabularyViewModel.setNavigationContext(vocabularyItems, vocabularyItems.first().id) - } - } - - Box( - modifier = modifier.fillMaxSize(), - contentAlignment = Alignment.TopCenter - ) { - Column( - modifier = Modifier - .widthIn(max = 700.dp) - .fillMaxSize() - .padding(horizontal = 24.dp), - ) { - Spacer(modifier = Modifier.height(24.dp)) - - if (isInSelectionMode) { - SelectionTopBar( - selectionCount = selection.size, - onCloseClick = { selection = emptySet() }, - onSelectAllClick = { - selection = if (selection.size == vocabularyItems.size) emptySet() else vocabularyItems.map { it.id.toLong() }.toSet() - }, - onDeleteClick = { - vocabularyViewModel.deleteVocabularyItemsById(selection.map { it.toInt() }) - selection = emptySet() - }, - onMoveToCategoryClick = { showCategoryDialog = true }, - onMoveToStageClick = { showStageDialog = true }, - isRemoveEnabled = isRemoveFromCategoryEnabled, - onRemoveFromCategoryClick = { - if (categoryId != null) { - val itemsToRemove = vocabularyItems.filter { selection.contains(it.id.toLong()) } - vocabularyViewModel.removeVocabularyItemsFromCategory(itemsToRemove, categoryId) - selection = emptySet() - } - } - ) - } else { - LibraryTopBar( - onAddClick = { /* TODO: Add new card/category */ } - ) - } - - Spacer(modifier = Modifier.height(24.dp)) - - SearchBar( - searchQuery = filterState.searchQuery, - onQueryChanged = { filterState = filterState.copy(searchQuery = it) }, - onFilterClick = { showFilterSheet = true } - ) - - Spacer(modifier = Modifier.height(24.dp)) - - SegmentedControl( - isCategoriesView = isCategoriesView, - onTabSelected = { isCategoriesView = it } - ) - - Spacer(modifier = Modifier.height(24.dp)) - - Crossfade( - targetState = isCategoriesView, - label = "LibraryViewTransition", - modifier = Modifier.weight(1f) - ) { showCategories -> - if (showCategories) { - CategoriesView(categories = categories) - } else { - AllCardsView( - vocabularyItems = vocabularyItems, - allLanguages = allLanguages, - selection = selection, - listState = lazyListState, - onItemClick = { item -> - if (isInSelectionMode) { - selection = if (selection.contains(item.id.toLong())) { - selection - item.id.toLong() - } else { - selection + item.id.toLong() - } - } else { - if (navController != null && enableNavigationButtons) { - vocabularyViewModel.setNavigationContext(vocabularyItems, item.id) - navController.navigate("vocabulary_detail/${item.id}") - } else { - onNavigateToItem(item) - } - } - }, - onItemLongClick = { item -> - if (!isInSelectionMode) { - selection = setOf(item.id.toLong()) - } - }, - onDeleteClick = { item -> - vocabularyViewModel.deleteData(VocabularyViewModel.DeleteType.VOCABULARY_ITEM, item = item) - } - ) - } - } - } - - // Floating Action Button for scrolling to top - val showFab by remember { derivedStateOf { lazyListState.firstVisibleItemIndex > 5 && !isInSelectionMode } } - AnimatedVisibility( - visible = showFab, - modifier = Modifier - .align(Alignment.BottomCenter) - .padding(bottom = 16.dp) - ) { - FloatingActionButton( - onClick = { scope.launch { lazyListState.animateScrollToItem(0) } }, - shape = CircleShape, - modifier = Modifier.size(50.dp), - containerColor = MaterialTheme.colorScheme.surfaceVariant - ) { - Icon(AppIcons.ArrowCircleUp, contentDescription = "Scroll to top") - } - } - } - - if (showFilterSheet) { - ModalBottomSheet( - onDismissRequest = { showFilterSheet = false }, - sheetState = sheetState, - containerColor = MaterialTheme.colorScheme.surface, - dragHandle = { BottomSheetDefaults.DragHandle() } - ) { - FilterBottomSheetContent( - currentFilterState = filterState, - languageViewModel = languageViewModel, - languagesPresent = allLanguages.filter { it.nameResId in languagesPresent }, - onApplyFilters = { newState -> - filterState = newState - showFilterSheet = false - scope.launch { lazyListState.scrollToItem(0) } - }, - onResetClick = { - filterState = NewVocabularyFilterState( - categoryIds = categoryId?.let { listOf(it) } ?: emptyList() - ) - } - ) - } - } - - if (showCategoryDialog) { - val selectedItems = vocabularyItems.filter { selection.contains(it.id.toLong()) } - CategorySelectionDialog( - onCategorySelected = { - vocabularyViewModel.addVocabularyItemToCategories( - selectedItems, - it.mapNotNull { category -> category?.id } - ) - }, - onDismissRequest = { showCategoryDialog = false } - ) - } - - if (showStageDialog) { - val selectedItems = vocabularyItems.filter { selection.contains(it.id.toLong()) } - StageSelectionDialog( - onStageSelected = { selectedStage -> - selectedStage?.let { - vocabularyViewModel.addVocabularyItemToStage(selectedItems, it) - } - showStageDialog = false - selection = emptySet() - }, - onDismissRequest = { showStageDialog = false } - ) - } -} - -@Composable -fun LibraryTopBar(onAddClick: () -> Unit) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = "Library", - style = MaterialTheme.typography.headlineMedium, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurface - ) - IconButton( - onClick = onAddClick, - modifier = Modifier - .clip(CircleShape) - .background(MaterialTheme.colorScheme.surfaceVariant) - ) { - Icon( - imageVector = Icons.Default.Add, - contentDescription = "Add", - tint = MaterialTheme.colorScheme.primary - ) - } - } -} - -@Composable -fun SelectionTopBar( - selectionCount: Int, - onCloseClick: () -> Unit, - onSelectAllClick: () -> Unit, - onDeleteClick: () -> Unit, - onMoveToCategoryClick: () -> Unit, - onMoveToStageClick: () -> Unit, - isRemoveEnabled: Boolean, - onRemoveFromCategoryClick: () -> Unit -) { - var showOverflowMenu by remember { mutableStateOf(false) } - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - IconButton(onClick = onCloseClick) { - Icon(imageVector = AppIcons.Close, contentDescription = stringResource(R.string.label_close_selection_mode)) - } - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = stringResource(R.string.d_selected, selectionCount), - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold - ) - } - - Row { - IconButton(onClick = onSelectAllClick) { - Icon( - imageVector = AppIcons.SelectAll, - contentDescription = stringResource(R.string.select_all) - ) - } - IconButton(onClick = onDeleteClick) { - Icon(AppIcons.Delete, contentDescription = stringResource(R.string.label_delete)) - } - Box { - IconButton(onClick = { showOverflowMenu = true }) { - Icon(imageVector = AppIcons.More, contentDescription = stringResource(R.string.more_actions)) - } - DropdownMenu( - expanded = showOverflowMenu, - onDismissRequest = { showOverflowMenu = false } - ) { - DropdownMenuItem( - text = { Text(stringResource(R.string.move_to_category)) }, - onClick = { - onMoveToCategoryClick() - showOverflowMenu = false - }, - leadingIcon = { Icon(AppIcons.Category, contentDescription = null) } - ) - if (isRemoveEnabled) { - DropdownMenuItem( - text = { Text(stringResource(R.string.remove_from_category)) }, - onClick = { - onRemoveFromCategoryClick() - showOverflowMenu = false - }, - leadingIcon = { Icon(AppIcons.Remove, contentDescription = null) } - ) - } - DropdownMenuItem( - text = { Text(stringResource(R.string.move_to_stage)) }, - onClick = { - onMoveToStageClick() - showOverflowMenu = false - }, - leadingIcon = { Icon(AppIcons.Stages, contentDescription = null) } - ) - } - } - } - } -} - -@Composable -fun SearchBar( - searchQuery: String, - onQueryChanged: (String) -> Unit, - onFilterClick: () -> Unit -) { - Row( - modifier = Modifier - .fillMaxWidth() - .height(56.dp) - .clip(RoundedCornerShape(16.dp)) - .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)) - .padding(start = 16.dp, end = 8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.Default.Search, - contentDescription = "Search", - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - Spacer(modifier = Modifier.width(12.dp)) - - androidx.compose.foundation.text.BasicTextField( - value = searchQuery, - onValueChange = onQueryChanged, - modifier = Modifier.weight(1f), - textStyle = MaterialTheme.typography.bodyLarge.copy( - color = MaterialTheme.colorScheme.onSurface - ), - singleLine = true, - cursorBrush = androidx.compose.ui.graphics.SolidColor(MaterialTheme.colorScheme.primary), - decorationBox = { innerTextField -> - Box(contentAlignment = Alignment.CenterStart) { - if (searchQuery.isEmpty()) { - Text( - text = "Search cards or topics...", - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), - style = MaterialTheme.typography.bodyLarge - ) - } - innerTextField() - } - } - ) - - IconButton(onClick = onFilterClick) { - Icon( - imageVector = Icons.Default.Tune, - contentDescription = "Filter options", - tint = MaterialTheme.colorScheme.primary - ) - } - } -} - -@Composable -fun SegmentedControl( - isCategoriesView: Boolean, - onTabSelected: (Boolean) -> Unit -) { - Row( - modifier = Modifier - .fillMaxWidth() - .height(48.dp) - .clip(RoundedCornerShape(12.dp)) - .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)) - .padding(4.dp) - ) { - Box( - modifier = Modifier - .weight(1f) - .fillMaxHeight() - .clip(RoundedCornerShape(8.dp)) - .background(if (!isCategoriesView) MaterialTheme.colorScheme.primary else Color.Transparent) - .clickable { onTabSelected(false) }, - contentAlignment = Alignment.Center - ) { - Text( - text = "All Cards", - color = if (!isCategoriesView) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant, - fontWeight = FontWeight.Bold, - style = MaterialTheme.typography.labelLarge - ) - } - - Box( - modifier = Modifier - .weight(1f) - .fillMaxHeight() - .clip(RoundedCornerShape(8.dp)) - .background(if (isCategoriesView) MaterialTheme.colorScheme.primary else Color.Transparent) - .clickable { onTabSelected(true) }, - contentAlignment = Alignment.Center - ) { - Text( - text = "Categories", - color = if (isCategoriesView) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant, - fontWeight = FontWeight.Bold, - style = MaterialTheme.typography.labelLarge - ) - } - } -} - -@Composable -fun AllCardsView( - vocabularyItems: List, - allLanguages: List, - selection: Set, - onItemClick: (VocabularyItem) -> Unit, - onItemLongClick: (VocabularyItem) -> Unit, - onDeleteClick: (VocabularyItem) -> Unit, - listState: androidx.compose.foundation.lazy.LazyListState -) { - if (vocabularyItems.isEmpty()) { - Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - androidx.compose.foundation.Image( - modifier = Modifier.size(200.dp), - painter = painterResource(id = R.drawable.ic_nothing_found), - contentDescription = stringResource(id = R.string.no_vocabulary_items_found_perhaps_try_changing_the_filters) - ) - Spacer(modifier = Modifier.size(16.dp)) - Text( - text = stringResource(R.string.no_vocabulary_items_found_perhaps_try_changing_the_filters), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = androidx.compose.ui.text.style.TextAlign.Center - ) - } - } else { - LazyColumn( - state = listState, - verticalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(bottom = 100.dp) - ) { - items( - items = vocabularyItems, - key = { it.id } - ) { item -> - val isSelected = selection.contains(item.id.toLong()) - VocabularyCard( - item = item, - allLanguages = allLanguages, - isSelected = isSelected, - onItemClick = { onItemClick(item) }, - onItemLongClick = { onItemLongClick(item) }, - onDeleteClick = { onDeleteClick(item) } - ) - } - } - } -} - -@Composable -fun VocabularyCard( - item: VocabularyItem, - allLanguages: List, - isSelected: Boolean, - onItemClick: () -> Unit, - onItemLongClick: () -> Unit, - onDeleteClick: () -> Unit -) { - val languageMap = remember(allLanguages) { allLanguages.associateBy { it.nameResId } } - val langFirst = item.languageFirstId?.let { languageMap[it]?.name } ?: "" - val langSecond = item.languageSecondId?.let { languageMap[it]?.name } ?: "" - - Card( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(16.dp)) - .combinedClickable( - onClick = onItemClick, - onLongClick = onItemLongClick - ), - shape = RoundedCornerShape(16.dp), - colors = CardDefaults.cardColors( - containerColor = if (isSelected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant - ), - border = if (isSelected) BorderStroke(2.dp, MaterialTheme.colorScheme.primary) else null - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column(modifier = Modifier.weight(1f)) { - // Top row: First word + Language Pill - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = insertBreakOpportunities(item.wordFirst), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurface - ) - Spacer(modifier = Modifier.width(8.dp)) - Surface( - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f), - shape = RoundedCornerShape(4.dp) - ) { - Text( - text = langFirst, - style = MaterialTheme.typography.labelSmall, - modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - - Spacer(modifier = Modifier.height(4.dp)) - - // Bottom row: Second word + Language Pill - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = insertBreakOpportunities(item.wordSecond), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Spacer(modifier = Modifier.width(8.dp)) - Surface( - color = MaterialTheme.colorScheme.primary.copy(alpha = 0.15f), - shape = RoundedCornerShape(8.dp) - ) { - Text( - text = langSecond, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp) - ) - } - } - } - - if (isSelected) { - Icon( - imageVector = Icons.Default.CheckCircle, - contentDescription = "Selected", - tint = MaterialTheme.colorScheme.primary - ) - } else { - IconButton(onClick = { /* Options menu could go here, for now just delete or nothing */ }) { - Icon( - imageVector = Icons.Default.MoreVert, - contentDescription = "Options", - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - } -} - -@Composable -fun CategoriesView(categories: List) { - LazyVerticalGrid( - columns = GridCells.Fixed(2), - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(bottom = 100.dp) - ) { - items(categories) { category -> - CategoryCard(category) - } - - item(span = { GridItemSpan(2) }) { - ExploreMoreCard() - } - } -} - -@Composable -fun CategoryCard(category: VocabularyCategory) { - Card( - modifier = Modifier - .fillMaxWidth() - .height(140.dp), - shape = RoundedCornerShape(16.dp), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant) - ) { - Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp), - verticalArrangement = Arrangement.SpaceBetween - ) { - Box( - modifier = Modifier - .size(40.dp) - .clip(RoundedCornerShape(8.dp)) - .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.15f)), - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = Icons.Default.LocalMall, // Placeholder icon - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(24.dp) - ) - } - - Column { - Text( - text = category.name, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - - Spacer(modifier = Modifier.height(8.dp)) - - // Placeholder for progress - LinearProgressIndicator( - progress = { 0.5f }, - modifier = Modifier - .fillMaxWidth() - .height(4.dp) - .clip(RoundedCornerShape(2.dp)), - color = MaterialTheme.colorScheme.primary, - trackColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f) - ) - } - } - } -} - -@Composable -fun ExploreMoreCard() { - val borderColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) - - Box( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp) - .height(80.dp) - .clip(RoundedCornerShape(16.dp)) - .clickable { /* TODO: Open explore categories */ } - .drawBehind { - val stroke = Stroke( - width = 2.dp.toPx(), - pathEffect = PathEffect.dashPathEffect(floatArrayOf(20f, 20f), 0f) - ) - drawRoundRect( - color = borderColor, - style = stroke, - cornerRadius = CornerRadius(16.dp.toPx(), 16.dp.toPx()) - ) - }, - contentAlignment = Alignment.Center - ) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Icon( - imageVector = Icons.Default.AddCircleOutline, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = "Explore more categories", - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } -} - -@Composable -fun FilterBottomSheetContent( - currentFilterState: NewVocabularyFilterState, - languageViewModel: LanguageViewModel, - languagesPresent: List, - onApplyFilters: (NewVocabularyFilterState) -> Unit, - onResetClick: () -> Unit -) { - var selectedStage by rememberSaveable { mutableStateOf(currentFilterState.selectedStage) } - var dueTodayOnly by rememberSaveable { mutableStateOf(currentFilterState.dueTodayOnly) } - var selectedLanguageIds by rememberSaveable { mutableStateOf(currentFilterState.selectedLanguageIds) } - var selectedWordClass by rememberSaveable { mutableStateOf(currentFilterState.selectedWordClass) } - var sortOrder by rememberSaveable { mutableStateOf(currentFilterState.sortOrder) } - - val context = LocalContext.current - val activity = LocalContext.current.findActivity() - val languageConfigViewModel: LanguageConfigViewModel = hiltViewModel(activity) - val allWordClasses by languageConfigViewModel.allWordClasses.collectAsStateWithLifecycle() - - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp) - .padding(bottom = 32.dp) - .navigationBarsPadding() - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = "Filter Cards", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold - ) - TextButton(onClick = { - selectedStage = null - dueTodayOnly = false - selectedLanguageIds = emptyList() - selectedWordClass = null - sortOrder = SortOrder.NEWEST_FIRST - onResetClick() - }) { - Text("Reset") - } - } - - HorizontalDivider(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)) - Spacer(modifier = Modifier.height(16.dp)) - - Column( - modifier = Modifier - .weight(1f, fill = false) - .verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.spacedBy(24.dp) - ) { - // Sort Order - Column { - Text( - text = "SORT BY", - style = MaterialTheme.typography.labelMedium, - fontWeight = FontWeight.Bold, - letterSpacing = 1.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(bottom = 12.dp) - ) - FlowRow( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.fillMaxWidth() - ) { - SortOrder.entries.forEach { order -> - FilterChip( - selected = sortOrder == order, - onClick = { sortOrder = order }, - label = { Text(order.name.replace('_', ' ').lowercase().replaceFirstChar { it.titlecase() }) } - ) - } - } - } - - // Due Today - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - text = stringResource(R.string.text_due_today_only).uppercase(), - style = MaterialTheme.typography.labelMedium, - fontWeight = FontWeight.Bold, - letterSpacing = 1.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - AppSwitch(checked = dueTodayOnly, onCheckedChange = { dueTodayOnly = it }) - } - - // Stages - Column { - Text( - text = "STAGES", - style = MaterialTheme.typography.labelMedium, - fontWeight = FontWeight.Bold, - letterSpacing = 1.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(bottom = 12.dp) - ) - FlowRow( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.fillMaxWidth() - ) { - FilterChip( - selected = selectedStage == null, - onClick = { selectedStage = null }, - label = { Text(stringResource(R.string.label_all_stages)) } - ) - VocabularyStage.entries.forEach { stage -> - FilterChip( - selected = selectedStage == stage, - onClick = { selectedStage = stage }, - label = { Text(stage.toString(context)) } - ) - } - } - } - - // Languages - Column { - Text( - text = stringResource(R.string.language).uppercase(), - style = MaterialTheme.typography.labelMedium, - fontWeight = FontWeight.Bold, - letterSpacing = 1.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(bottom = 12.dp) - ) - MultipleLanguageDropdown( - languageViewModel = languageViewModel, - onLanguagesSelected = { languages -> - selectedLanguageIds = languages.map { it.nameResId } - }, - alternateLanguages = languagesPresent - ) - } - - // Word Class - if (allWordClasses.isNotEmpty()) { - Column { - Text( - text = stringResource(R.string.filter_by_word_type).uppercase(), - style = MaterialTheme.typography.labelMedium, - fontWeight = FontWeight.Bold, - letterSpacing = 1.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(bottom = 12.dp) - ) - FlowRow( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.fillMaxWidth() - ) { - FilterChip( - selected = selectedWordClass == null, - onClick = { selectedWordClass = null }, - label = { Text(stringResource(R.string.label_all_types)) } - ) - allWordClasses.forEach { wordClass -> - FilterChip( - selected = selectedWordClass == wordClass, - onClick = { selectedWordClass = wordClass }, - label = { Text(wordClass.replaceFirstChar { it.titlecase() }) } - ) - } - } - } - } - } - - Spacer(modifier = Modifier.height(24.dp)) - - Button( - onClick = { - onApplyFilters( - currentFilterState.copy( - selectedStage = selectedStage, - dueTodayOnly = dueTodayOnly, - selectedLanguageIds = selectedLanguageIds, - selectedWordClass = selectedWordClass, - sortOrder = sortOrder - ) - ) - }, - modifier = Modifier - .fillMaxWidth() - .height(56.dp), - shape = RoundedCornerShape(28.dp) - ) { - Text( - text = "Apply Filters", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold - ) - } - } -} 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 73c0b8e..df86c91 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 @@ -68,9 +68,9 @@ 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.view.library.VocabularyCard import eu.gaudian.translator.viewmodel.LanguageViewModel import eu.gaudian.translator.viewmodel.VocabularyViewModel import kotlinx.coroutines.launch @@ -85,6 +85,8 @@ fun NewWordScreen( 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) } @@ -111,6 +113,10 @@ fun NewWordScreen( var selectedLangSecond by remember { mutableStateOf(null) } var parseError by remember { mutableStateOf(null) } + val recentlyAdded = remember(recentItems) { + recentItems.sortedByDescending { it.id }.take(4) + } + fun parseCsv(text: String): List> { if (text.isBlank()) return emptyList() val candidates = listOf(',', ';', '\t') @@ -265,6 +271,40 @@ fun NewWordScreen( } ) + 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)) } @@ -313,7 +353,7 @@ fun NewWordScreen( Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { Column(modifier = Modifier.weight(1f)) { Text(stringResource(R.string.label_first_language)) - SingleLanguageDropDown( + eu.gaudian.translator.view.composable.SingleLanguageDropDown( languageViewModel = languageViewModel, selectedLanguage = selectedLangFirst, onLanguageSelected = { selectedLangFirst = it } @@ -321,7 +361,7 @@ fun NewWordScreen( } Column(modifier = Modifier.weight(1f)) { Text(stringResource(R.string.label_second_language)) - SingleLanguageDropDown( + eu.gaudian.translator.view.composable.SingleLanguageDropDown( languageViewModel = languageViewModel, selectedLanguage = selectedLangSecond, onLanguageSelected = { selectedLangSecond = it }