From af78bd316dcf6e115dff355d0cb854ef55b4e1d3 Mon Sep 17 00:00:00 2001 From: jonasgaudian <43753916+jonasgaudian@users.noreply.github.com> Date: Mon, 16 Feb 2026 15:49:57 +0100 Subject: [PATCH] implement `LibraryScreen` UI with search, filtering, and segmented view for cards and categories --- .../translator/view/library/LibraryScreen.kt | 698 +---------- .../view/vocabulary/NewVocListScreen.kt | 1062 +++++++++++++++++ app/src/main/res/values/arrays.xml | 2 +- 3 files changed, 1069 insertions(+), 693 deletions(-) create mode 100644 app/src/main/java/eu/gaudian/translator/view/vocabulary/NewVocListScreen.kt diff --git a/app/src/main/java/eu/gaudian/translator/view/library/LibraryScreen.kt b/app/src/main/java/eu/gaudian/translator/view/library/LibraryScreen.kt index 3ad07bd..a781d5b 100644 --- a/app/src/main/java/eu/gaudian/translator/view/library/LibraryScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/library/LibraryScreen.kt @@ -1,705 +1,19 @@ package eu.gaudian.translator.view.library -import androidx.compose.animation.Crossfade -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -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.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.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -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.Computer -import androidx.compose.material.icons.filled.Eco -import androidx.compose.material.icons.filled.FlightTakeoff -import androidx.compose.material.icons.filled.LocalMall -import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material.icons.filled.Restaurant -import androidx.compose.material.icons.filled.Search -import androidx.compose.material.icons.filled.Tune -import androidx.compose.material.icons.filled.Work -import androidx.compose.material3.BottomSheetDefaults -import androidx.compose.material3.Button -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -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.getValue -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.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.graphics.vector.ImageVector -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.navigation.NavHostController +import eu.gaudian.translator.view.vocabulary.NewVocListScreen @Composable fun LibraryScreen( navController: NavHostController, modifier: Modifier = Modifier ) { - var isCategoriesView by remember { mutableStateOf(false) } - - // Bottom Sheet State - var showFilterSheet by remember { mutableStateOf(false) } - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - - 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)) - LibraryTopBar() - Spacer(modifier = Modifier.height(24.dp)) - - // Pass the click handler to the SearchBar - SearchBar(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() - } else { - AllCardsView() - } - } - } - } - - // Modal Bottom Sheet triggered by the filter icon - if (showFilterSheet) { - ModalBottomSheet( - onDismissRequest = { showFilterSheet = false }, - sheetState = sheetState, - containerColor = MaterialTheme.colorScheme.surface, - dragHandle = { BottomSheetDefaults.DragHandle() } - ) { - FilterBottomSheetContent( - onApplyClick = { - // TODO: Apply actual filter logic here - showFilterSheet = false - }, - onResetClick = { - // TODO: Reset filter logic here - } - ) - } - } -} - -// ... [LibraryTopBar remains the same] ... - -@Composable -fun SearchBar(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), // Less padding on right to account for icon button - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.Default.Search, - contentDescription = "Search", - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - Spacer(modifier = Modifier.width(12.dp)) - - // Weight(1f) pushes the filter icon to the far right - Text( - text = "Search cards or topics...", - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.weight(1f) - ) - - // Filter Icon Button - IconButton(onClick = onFilterClick) { - Icon( - imageVector = Icons.Default.Tune, // Standard filter/slider icon - contentDescription = "Filter options", - tint = MaterialTheme.colorScheme.primary - ) - } - } -} - -// --- FILTER BOTTOM SHEET COMPONENTS --- - -@Composable -fun FilterBottomSheetContent( - onApplyClick: () -> Unit, - onResetClick: () -> Unit -) { - // Dummy states just to make the UI interactive - var selectedStatuses by remember { mutableStateOf(setOf("Learning", "To Review")) } - var selectedDifficulties by remember { mutableStateOf(setOf("Medium")) } - var selectedTypes by remember { mutableStateOf(setOf()) } - - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp) - .padding(bottom = 32.dp) // Extra padding for bottom navigation - ) { - // Header - 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 = { - selectedStatuses = emptySet() - selectedDifficulties = emptySet() - selectedTypes = emptySet() - onResetClick() - }) { - Text("Reset") - } - } - - HorizontalDivider(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)) - Spacer(modifier = Modifier.height(16.dp)) - - // Filters Content - LazyColumn( - modifier = Modifier.weight(1f, fill = false), // Allows it to scroll if screen is small - verticalArrangement = Arrangement.spacedBy(24.dp) - ) { - item { - FilterSection( - title = "Status", - options = listOf("New", "Learning", "To Review", "Mastered"), - selectedOptions = selectedStatuses, - onOptionToggle = { option -> - selectedStatuses = if (selectedStatuses.contains(option)) { - selectedStatuses - option - } else { - selectedStatuses + option - } - } - ) - } - item { - FilterSection( - title = "Difficulty", - options = listOf("Easy", "Medium", "Hard"), - selectedOptions = selectedDifficulties, - onOptionToggle = { option -> - selectedDifficulties = if (selectedDifficulties.contains(option)) selectedDifficulties - option else selectedDifficulties + option - } - ) - } - item { - FilterSection( - title = "Part of Speech", - options = listOf("Noun", "Verb", "Adjective", "Adverb", "Pronoun", "Preposition"), - selectedOptions = selectedTypes, - onOptionToggle = { option -> - selectedTypes = if (selectedTypes.contains(option)) selectedTypes - option else selectedTypes + option - } - ) - } - } - - Spacer(modifier = Modifier.height(24.dp)) - - // Apply Button - Button( - onClick = onApplyClick, - modifier = Modifier.fillMaxWidth().height(56.dp), - shape = RoundedCornerShape(28.dp) - ) { - Text( - text = "Apply Filters", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold - ) - } - } -} - -@Composable -fun FilterSection( - title: String, - options: List, - selectedOptions: Set, - onOptionToggle: (String) -> Unit -) { - Column { - Text( - text = title.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() - ) { - options.forEach { option -> - val isSelected = selectedOptions.contains(option) - Surface( - shape = RoundedCornerShape(20.dp), - color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), - border = if (isSelected) null else BorderStroke(1.dp, MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.2f)), - onClick = { onOptionToggle(option) } - ) { - Text( - text = option, - color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), - style = MaterialTheme.typography.bodyMedium, - fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal - ) - } - } - } - } -} - -@Composable -fun LibraryTopBar() { - 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 = { /* TODO: Add new card/category */ }, - modifier = Modifier - .clip(CircleShape) - .background(MaterialTheme.colorScheme.surfaceVariant) - ) { - Icon( - imageVector = Icons.Default.Add, - contentDescription = "Add", - tint = MaterialTheme.colorScheme.primary - ) - } - } -} - -@Composable -fun SearchBar() { - Row( - modifier = Modifier - .fillMaxWidth() - .height(56.dp) - .clip(RoundedCornerShape(16.dp)) - .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)) - .padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.Default.Search, - contentDescription = "Search", - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - Spacer(modifier = Modifier.width(12.dp)) - Text( - text = "Search cards or topics...", - color = MaterialTheme.colorScheme.onSurfaceVariant, - style = MaterialTheme.typography.bodyLarge - ) - } -} - -@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) - ) { - // All Cards Tab - 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 - ) - } - - // Categories Tab - 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 - ) - } - } -} - -// --- ALL CARDS VIEW (LIST) --- - -@Composable -fun AllCardsView() { - // Dummy Data - val flashcards = listOf( - FlashcardData("Schlüssel", "DER", "Chave", "Comum"), - FlashcardData("Haus", "DAS", "Casa", "Iniciante"), - FlashcardData("Apfel", "DER", "Maçã", "Comum"), - FlashcardData("Bücherregal", "DAS", "Estante", "Casa"), - FlashcardData("Fenster", "DAS", "Janela", "Comum"), - FlashcardData("Kühlschrank", "DER", "Geladeira", "Casa") + NewVocListScreen( + navController = navController, + enableNavigationButtons = true, + onNavigateToItem = {}, + modifier = modifier ) - - LazyColumn( - verticalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(bottom = 100.dp) // Padding for bottom nav overlap - ) { - items(flashcards) { card -> - VocabularyCard(card) - } - } } - -@Composable -fun VocabularyCard(card: FlashcardData) { - Card( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column(modifier = Modifier.weight(1f)) { - // Top row: German word + Article Pill - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = card.word, - 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 = "(${card.article})", - 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: Translation + Category Tag - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = card.translation, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Text( - text = " • ", - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Surface( - color = MaterialTheme.colorScheme.primary.copy(alpha = 0.15f), - shape = RoundedCornerShape(8.dp) - ) { - Text( - text = card.tag, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp) - ) - } - } - } - - IconButton(onClick = { /* TODO: Card options */ }) { - Icon( - imageVector = Icons.Default.MoreVert, - contentDescription = "Options", - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } -} - -// --- CATEGORIES VIEW (GRID) --- - -@Composable -fun CategoriesView() { - // Dummy Data - val categories = listOf( - CategoryData("Travel", 120, 150, Icons.Default.FlightTakeoff), - CategoryData("Food", 45, 100, Icons.Default.Restaurant), - CategoryData("Business", 200, 200, Icons.Default.Work, isCompleted = true), - CategoryData("Nature", 10, 120, Icons.Default.Eco), - CategoryData("Technology", 0, 180, Icons.Default.Computer), - CategoryData("Shopping", 0, 95, Icons.Default.LocalMall) - ) - - LazyVerticalGrid( - columns = GridCells.Fixed(2), - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(bottom = 100.dp) // Padding for bottom nav overlap - ) { - items(categories) { category -> - CategoryCard(category) - } - - // The dashed "Explore more" item spans both columns - item(span = { GridItemSpan(2) }) { - ExploreMoreCard() - } - } -} - -@Composable -fun CategoryCard(category: CategoryData) { - 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 - ) { - // Icon Background Box - Box( - modifier = Modifier - .size(40.dp) - .clip(RoundedCornerShape(8.dp)) - .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.15f)), - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = category.icon, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(24.dp) - ) - } - - Column { - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = category.title, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - if (category.isCompleted) { - Spacer(modifier = Modifier.width(4.dp)) - Icon( - imageVector = Icons.Default.CheckCircle, - contentDescription = "Completed", - tint = Color(0xFF10B981), // Green - modifier = Modifier.size(16.dp) - ) - } - } - - Spacer(modifier = Modifier.height(8.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - text = if (category.isCompleted) "COMPLETED" else "PROGRESS", - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 10.sp - ) - Text( - text = "${category.current}/${category.total}", - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 10.sp - ) - } - - Spacer(modifier = Modifier.height(6.dp)) - - // Progress Bar - LinearProgressIndicator( - progress = { category.current.toFloat() / category.total.toFloat() }, - modifier = Modifier.fillMaxWidth().height(4.dp).clip(RoundedCornerShape(2.dp)), - color = if (category.isCompleted) Color(0xFF10B981) else 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 */ } - // We use drawBehind to apply the custom dashed stroke to a rounded rectangle - .drawBehind { - val stroke = Stroke( - width = 2.dp.toPx(), - pathEffect = PathEffect.dashPathEffect(floatArrayOf(20f, 20f), 0f) // Fixed reference! - ) - 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 - ) - } - } -} - -// --- DUMMY DATA MODELS --- - -data class FlashcardData( - val word: String, - val article: String, - val translation: String, - val tag: String -) - -data class CategoryData( - val title: String, - val current: Int, - val total: Int, - val icon: ImageVector, - val isCompleted: Boolean = false -) \ No newline at end of file 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 new file mode 100644 index 0000000..5d6c373 --- /dev/null +++ b/app/src/main/java/eu/gaudian/translator/view/vocabulary/NewVocListScreen.kt @@ -0,0 +1,1062 @@ +@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, + 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 +) { + 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( + 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/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index b1acb45..71c83ff 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -61,7 +61,7 @@ Version 0.3.0 \n• Enabled CSV Import for Vocabulary\n• Option to use a translation server for translations instead of AI models for some supported langugaes\n• UI bug fixes \n• Show word frequency \n• Performance optimizations \n• Improved translations (German and Portuguese) Version 0.4.0 \n• Added dictionary download (beta) \n• UI enhancements \n• Bugfixes \n• Re-designed vocabulary card with improved UI \n• More pre-configured providers \n• Improved performance - Version 0.5.0 \n• Reworked hints and help content, added more instcructions and help \n• UI changes in the flashcards with a more intuitive design \n• Lots of bugfixes \n• Improved translations for German and Portuguese + Version 0.5.0 \n• Reworked UI with new focus on Flashcards and Exercises \n•