implement LibraryScreen UI with search, filtering, and segmented view for cards and categories
This commit is contained in:
@@ -1,12 +1,77 @@
|
|||||||
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.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.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.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
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.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
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -14,13 +79,627 @@ fun LibraryScreen(
|
|||||||
navController: NavHostController,
|
navController: NavHostController,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
var isCategoriesView by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
// Bottom Sheet State
|
||||||
|
var showFilterSheet by remember { mutableStateOf(false) }
|
||||||
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier.fillMaxSize(),
|
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<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(
|
||||||
text = "Library Screen",
|
text = "Library",
|
||||||
style = MaterialTheme.typography.headlineMedium
|
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
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user