implement LibraryScreen UI with search, filtering, and segmented view for cards and categories

This commit is contained in:
jonasgaudian
2026-02-16 15:49:57 +01:00
parent 24cebc4b15
commit af78bd316d
3 changed files with 1069 additions and 693 deletions

View File

@@ -1,705 +1,19 @@
package eu.gaudian.translator.view.library 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.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.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 androidx.navigation.NavHostController
import eu.gaudian.translator.view.vocabulary.NewVocListScreen
@Composable @Composable
fun LibraryScreen( fun LibraryScreen(
navController: NavHostController, navController: NavHostController,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
var isCategoriesView by remember { mutableStateOf(false) } NewVocListScreen(
navController = navController,
// Bottom Sheet State enableNavigationButtons = true,
var showFilterSheet by remember { mutableStateOf(false) } onNavigateToItem = {},
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) modifier = modifier
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<String>()) }
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<String>,
selectedOptions: Set<String>,
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")
) )
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
)

File diff suppressed because it is too large Load Diff

View File

@@ -61,7 +61,7 @@
<string-array name="changelog_entries"> <string-array name="changelog_entries">
<item>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)</item> <item>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)</item>
<item>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</item> <item>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</item>
<item>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</item> <item>Version 0.5.0 \n• Reworked UI with new focus on Flashcards and Exercises \n• </item>
<item> </item> <item> </item>
</string-array> </string-array>