implement LibraryScreen UI with search, filtering, and segmented view for cards and categories
This commit is contained in:
@@ -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
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user