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 03f15e7..3ad07bd 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,12 +1,77 @@ 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 @Composable @@ -14,13 +79,627 @@ 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.Center + 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 Screen", - style = MaterialTheme.typography.headlineMedium + 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") + ) + + 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