implement LibraryScreen with advanced filtering and refactor CategoryDetailScreen
This commit is contained in:
@@ -0,0 +1,727 @@
|
||||
@file:Suppress("HardCodedStringLiteral")
|
||||
|
||||
package eu.gaudian.translator.view.library
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.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.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.LocalMall
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.Tune
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
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.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.model.Language
|
||||
import eu.gaudian.translator.model.VocabularyCategory
|
||||
import eu.gaudian.translator.model.VocabularyItem
|
||||
import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.insertBreakOpportunities
|
||||
|
||||
/**
|
||||
* Top bar for the library screen with title and add button
|
||||
*/
|
||||
@Composable
|
||||
fun LibraryTopBar(
|
||||
onAddClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Library",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
IconButton(
|
||||
onClick = onAddClick,
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = "Add",
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Top bar shown when items are selected for batch operations
|
||||
*/
|
||||
@Composable
|
||||
fun SelectionTopBar(
|
||||
selectionCount: Int,
|
||||
onCloseClick: () -> Unit,
|
||||
onSelectAllClick: () -> Unit,
|
||||
onDeleteClick: () -> Unit,
|
||||
onMoveToCategoryClick: () -> Unit,
|
||||
onMoveToStageClick: () -> Unit,
|
||||
isRemoveEnabled: Boolean,
|
||||
onRemoveFromCategoryClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var showOverflowMenu by remember { mutableStateOf(false) }
|
||||
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
IconButton(onClick = onCloseClick) {
|
||||
Icon(imageVector = AppIcons.Close, contentDescription = stringResource(R.string.label_close_selection_mode))
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.d_selected, selectionCount),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
|
||||
Row {
|
||||
IconButton(onClick = onSelectAllClick) {
|
||||
Icon(
|
||||
imageVector = AppIcons.SelectAll,
|
||||
contentDescription = stringResource(R.string.select_all)
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onDeleteClick) {
|
||||
Icon(AppIcons.Delete, contentDescription = stringResource(R.string.label_delete))
|
||||
}
|
||||
Box {
|
||||
IconButton(onClick = { showOverflowMenu = true }) {
|
||||
Icon(imageVector = AppIcons.More, contentDescription = stringResource(R.string.more_actions))
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = showOverflowMenu,
|
||||
onDismissRequest = { showOverflowMenu = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.move_to_category)) },
|
||||
onClick = {
|
||||
onMoveToCategoryClick()
|
||||
showOverflowMenu = false
|
||||
},
|
||||
leadingIcon = { Icon(AppIcons.Category, contentDescription = null) }
|
||||
)
|
||||
if (isRemoveEnabled) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.remove_from_category)) },
|
||||
onClick = {
|
||||
onRemoveFromCategoryClick()
|
||||
showOverflowMenu = false
|
||||
},
|
||||
leadingIcon = { Icon(AppIcons.Remove, contentDescription = null) }
|
||||
)
|
||||
}
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.move_to_stage)) },
|
||||
onClick = {
|
||||
onMoveToStageClick()
|
||||
showOverflowMenu = false
|
||||
},
|
||||
leadingIcon = { Icon(AppIcons.Stages, contentDescription = null) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search bar with filter button
|
||||
*/
|
||||
@Composable
|
||||
fun SearchBar(
|
||||
searchQuery: String,
|
||||
onQueryChanged: (String) -> Unit,
|
||||
onFilterClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
|
||||
.padding(start = 16.dp, end = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Search,
|
||||
contentDescription = "Search",
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
androidx.compose.foundation.text.BasicTextField(
|
||||
value = searchQuery,
|
||||
onValueChange = onQueryChanged,
|
||||
modifier = Modifier.weight(1f),
|
||||
textStyle = MaterialTheme.typography.bodyLarge.copy(
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
),
|
||||
singleLine = true,
|
||||
cursorBrush = androidx.compose.ui.graphics.SolidColor(MaterialTheme.colorScheme.primary),
|
||||
decorationBox = { innerTextField ->
|
||||
Box(contentAlignment = Alignment.CenterStart) {
|
||||
if (searchQuery.isEmpty()) {
|
||||
Text(
|
||||
text = "Search cards or topics...",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
innerTextField()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
IconButton(onClick = onFilterClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Tune,
|
||||
contentDescription = "Filter options",
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Segmented control for switching between All Cards and Categories view
|
||||
*/
|
||||
@Composable
|
||||
fun SegmentedControl(
|
||||
isCategoriesView: Boolean,
|
||||
onTabSelected: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
|
||||
.padding(4.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(if (!isCategoriesView) MaterialTheme.colorScheme.primary else Color.Transparent)
|
||||
.clickable { onTabSelected(false) },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "All Cards",
|
||||
color = if (!isCategoriesView) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontWeight = FontWeight.Bold,
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(if (isCategoriesView) MaterialTheme.colorScheme.primary else Color.Transparent)
|
||||
.clickable { onTabSelected(true) },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "Categories",
|
||||
color = if (isCategoriesView) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontWeight = FontWeight.Bold,
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List view of all vocabulary cards
|
||||
*/
|
||||
@Composable
|
||||
fun AllCardsView(
|
||||
vocabularyItems: List<VocabularyItem>,
|
||||
allLanguages: List<Language>,
|
||||
selection: Set<Long>,
|
||||
onItemClick: (VocabularyItem) -> Unit,
|
||||
onItemLongClick: (VocabularyItem) -> Unit,
|
||||
onDeleteClick: (VocabularyItem) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
if (vocabularyItems.isEmpty()) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.size(200.dp),
|
||||
painter = painterResource(id = R.drawable.ic_nothing_found),
|
||||
contentDescription = stringResource(id = R.string.no_vocabulary_items_found_perhaps_try_changing_the_filters)
|
||||
)
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.no_vocabulary_items_found_perhaps_try_changing_the_filters),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(bottom = 100.dp)
|
||||
) {
|
||||
items(
|
||||
items = vocabularyItems,
|
||||
key = { it.id }
|
||||
) { item ->
|
||||
val isSelected = selection.contains(item.id.toLong())
|
||||
VocabularyCard(
|
||||
item = item,
|
||||
allLanguages = allLanguages,
|
||||
isSelected = isSelected,
|
||||
onItemClick = { onItemClick(item) },
|
||||
onItemLongClick = { onItemLongClick(item) },
|
||||
onDeleteClick = { onDeleteClick(item) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Individual vocabulary card component
|
||||
*/
|
||||
@Composable
|
||||
fun VocabularyCard(
|
||||
item: VocabularyItem,
|
||||
allLanguages: List<Language>,
|
||||
isSelected: Boolean,
|
||||
onItemClick: () -> Unit,
|
||||
onItemLongClick: () -> Unit,
|
||||
onDeleteClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val languageMap = remember(allLanguages) { allLanguages.associateBy { it.nameResId } }
|
||||
val langFirst = item.languageFirstId?.let { languageMap[it]?.name } ?: ""
|
||||
val langSecond = item.languageSecondId?.let { languageMap[it]?.name } ?: ""
|
||||
|
||||
Card(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.combinedClickable(
|
||||
onClick = onItemClick,
|
||||
onLongClick = onItemLongClick
|
||||
),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = if (isSelected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant
|
||||
),
|
||||
border = if (isSelected) BorderStroke(2.dp, MaterialTheme.colorScheme.primary) else null
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
// Top row: First word + Language Pill
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = insertBreakOpportunities(item.wordFirst),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f),
|
||||
shape = RoundedCornerShape(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = langFirst,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
// Bottom row: Second word + Language Pill
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = insertBreakOpportunities(item.wordSecond),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.15f),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = langSecond,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isSelected) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.CheckCircle,
|
||||
contentDescription = "Selected",
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
} else {
|
||||
IconButton(onClick = { /* Options menu could go here */ }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = "Options",
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Grid view of categories
|
||||
*/
|
||||
@Composable
|
||||
fun CategoriesView(
|
||||
categories: List<VocabularyCategory>,
|
||||
onCategoryClick: (VocabularyCategory) -> Unit,
|
||||
onExploreMoreClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(2),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(bottom = 100.dp)
|
||||
) {
|
||||
items(categories) { category ->
|
||||
CategoryCard(
|
||||
category = category,
|
||||
onClick = { onCategoryClick(category) }
|
||||
)
|
||||
}
|
||||
|
||||
item(span = { GridItemSpan(2) }) {
|
||||
ExploreMoreCard(onClick = onExploreMoreClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Individual category card in grid view
|
||||
*/
|
||||
@Composable
|
||||
fun CategoryCard(
|
||||
category: VocabularyCategory,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(140.dp)
|
||||
.clickable(onClick = onClick),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.15f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.LocalMall,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Column {
|
||||
Text(
|
||||
text = category.name,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
LinearProgressIndicator(
|
||||
progress = { 0.5f },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(4.dp)
|
||||
.clip(RoundedCornerShape(2.dp)),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
trackColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Card to explore more categories
|
||||
*/
|
||||
@Composable
|
||||
fun ExploreMoreCard(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
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(onClick = onClick)
|
||||
.drawBehind {
|
||||
val stroke = Stroke(
|
||||
width = 2.dp.toPx(),
|
||||
pathEffect = PathEffect.dashPathEffect(floatArrayOf(20f, 20f), 0f)
|
||||
)
|
||||
drawRoundRect(
|
||||
color = borderColor,
|
||||
style = stroke,
|
||||
cornerRadius = CornerRadius(16.dp.toPx(), 16.dp.toPx())
|
||||
)
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.AddCircleOutline,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "Explore more categories",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Crossfade container for switching between views
|
||||
*/
|
||||
@Composable
|
||||
fun LibraryViewContainer(
|
||||
isCategoriesView: Boolean,
|
||||
categoriesContent: @Composable () -> Unit,
|
||||
allCardsContent: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Crossfade(
|
||||
targetState = isCategoriesView,
|
||||
label = "LibraryViewTransition",
|
||||
modifier = modifier
|
||||
) { showCategories ->
|
||||
if (showCategories) {
|
||||
categoriesContent()
|
||||
} else {
|
||||
allCardsContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== PREVIEWS ====================
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun LibraryTopBarPreview() {
|
||||
MaterialTheme {
|
||||
LibraryTopBar(onAddClick = {})
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun SelectionTopBarPreview() {
|
||||
MaterialTheme {
|
||||
SelectionTopBar(
|
||||
selectionCount = 5,
|
||||
onCloseClick = {},
|
||||
onSelectAllClick = {},
|
||||
onDeleteClick = {},
|
||||
onMoveToCategoryClick = {},
|
||||
onMoveToStageClick = {},
|
||||
isRemoveEnabled = true,
|
||||
onRemoveFromCategoryClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun SearchBarPreview() {
|
||||
MaterialTheme {
|
||||
SearchBar(
|
||||
searchQuery = "",
|
||||
onQueryChanged = {},
|
||||
onFilterClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun SegmentedControlPreview() {
|
||||
MaterialTheme {
|
||||
SegmentedControl(
|
||||
isCategoriesView = false,
|
||||
onTabSelected = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun VocabularyCardPreview() {
|
||||
MaterialTheme {
|
||||
VocabularyCard(
|
||||
item = VocabularyItem(
|
||||
id = 1,
|
||||
wordFirst = "Hello",
|
||||
wordSecond = "Hola",
|
||||
languageFirstId = 1,
|
||||
languageSecondId = 2,
|
||||
createdAt = null,
|
||||
features = null,
|
||||
zipfFrequencyFirst = null,
|
||||
zipfFrequencySecond = null
|
||||
),
|
||||
allLanguages = emptyList(),
|
||||
isSelected = false,
|
||||
onItemClick = {},
|
||||
onItemLongClick = {},
|
||||
onDeleteClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun CategoryCardPreview() {
|
||||
MaterialTheme {
|
||||
CategoryCard(
|
||||
category = eu.gaudian.translator.model.TagCategory(
|
||||
1,
|
||||
"Travel Phrases"
|
||||
),
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun ExploreMoreCardPreview() {
|
||||
MaterialTheme {
|
||||
ExploreMoreCard(onClick = {})
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,517 @@
|
||||
@file:Suppress("HardCodedStringLiteral")
|
||||
|
||||
package eu.gaudian.translator.view.library
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
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.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.BottomSheetDefaults
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavHostController
|
||||
import eu.gaudian.translator.view.vocabulary.NewVocListScreen
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.model.VocabularyStage
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppSwitch
|
||||
import eu.gaudian.translator.view.composable.MultipleLanguageDropdown
|
||||
import eu.gaudian.translator.view.dialogs.CategorySelectionDialog
|
||||
import eu.gaudian.translator.view.dialogs.StageSelectionDialog
|
||||
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
||||
import eu.gaudian.translator.viewmodel.LanguageConfigViewModel
|
||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel.SortOrder
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class LibraryFilterState(
|
||||
val searchQuery: String = "",
|
||||
val selectedStage: VocabularyStage? = null,
|
||||
val sortOrder: SortOrder = SortOrder.NEWEST_FIRST,
|
||||
val categoryIds: List<Int> = emptyList(),
|
||||
val dueTodayOnly: Boolean = false,
|
||||
val selectedLanguageIds: List<Int> = emptyList(),
|
||||
val selectedWordClass: String? = null
|
||||
) : Parcelable
|
||||
|
||||
@Composable
|
||||
fun LibraryScreen(
|
||||
navController: NavHostController,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
NewVocListScreen(
|
||||
navController = navController,
|
||||
enableNavigationButtons = true,
|
||||
onNavigateToItem = {},
|
||||
modifier = modifier
|
||||
)
|
||||
val scope = rememberCoroutineScope()
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val lazyListState = rememberLazyListState()
|
||||
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val context = LocalContext.current
|
||||
|
||||
var filterState by rememberSaveable { mutableStateOf(LibraryFilterState()) }
|
||||
var showFilterSheet by remember { mutableStateOf(false) }
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
|
||||
var selection by remember { mutableStateOf<Set<Long>>(emptySet()) }
|
||||
val isInSelectionMode = selection.isNotEmpty()
|
||||
|
||||
var showCategoryDialog by remember { mutableStateOf(false) }
|
||||
var showStageDialog by remember { mutableStateOf(false) }
|
||||
|
||||
var isCategoriesView by remember { mutableStateOf(false) }
|
||||
|
||||
val allLanguages by languageViewModel.allLanguages.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||
val languagesPresent by vocabularyViewModel.languagesPresent.collectAsStateWithLifecycle(initialValue = emptySet())
|
||||
val categories by vocabularyViewModel.categories.collectAsStateWithLifecycle()
|
||||
|
||||
val vocabularyItemsFlow = remember(filterState) {
|
||||
vocabularyViewModel.filterVocabularyItems(
|
||||
languages = filterState.selectedLanguageIds,
|
||||
query = filterState.searchQuery.takeIf { it.isNotBlank() },
|
||||
categoryIds = filterState.categoryIds,
|
||||
stage = filterState.selectedStage,
|
||||
wordClass = filterState.selectedWordClass,
|
||||
dueTodayOnly = filterState.dueTodayOnly,
|
||||
sortOrder = filterState.sortOrder
|
||||
)
|
||||
}
|
||||
|
||||
val vocabularyItems by vocabularyItemsFlow.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||
|
||||
// Set navigation context when vocabulary items are loaded
|
||||
LaunchedEffect(vocabularyItems) {
|
||||
if (vocabularyItems.isNotEmpty()) {
|
||||
vocabularyViewModel.setNavigationContext(vocabularyItems, vocabularyItems.first().id)
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.widthIn(max = 700.dp)
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 24.dp),
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
if (isInSelectionMode) {
|
||||
SelectionTopBar(
|
||||
selectionCount = selection.size,
|
||||
onCloseClick = { selection = emptySet() },
|
||||
onSelectAllClick = {
|
||||
selection = if (selection.size == vocabularyItems.size) emptySet()
|
||||
else vocabularyItems.map { it.id.toLong() }.toSet()
|
||||
},
|
||||
onDeleteClick = {
|
||||
vocabularyViewModel.deleteVocabularyItemsById(selection.map { it.toInt() })
|
||||
selection = emptySet()
|
||||
},
|
||||
onMoveToCategoryClick = { showCategoryDialog = true },
|
||||
onMoveToStageClick = { showStageDialog = true },
|
||||
isRemoveEnabled = false,
|
||||
onRemoveFromCategoryClick = {}
|
||||
)
|
||||
} else {
|
||||
LibraryTopBar(
|
||||
onAddClick = { /* TODO: Add new card/category */ }
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
SearchBar(
|
||||
searchQuery = filterState.searchQuery,
|
||||
onQueryChanged = { filterState = filterState.copy(searchQuery = it) },
|
||||
onFilterClick = { showFilterSheet = true }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
SegmentedControl(
|
||||
isCategoriesView = isCategoriesView,
|
||||
onTabSelected = { isCategoriesView = it }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
LibraryViewContainer(
|
||||
isCategoriesView = isCategoriesView,
|
||||
categoriesContent = {
|
||||
CategoriesView(
|
||||
categories = categories,
|
||||
onCategoryClick = { category ->
|
||||
navController.navigate("category_detail/${category.id}")
|
||||
},
|
||||
onExploreMoreClick = {
|
||||
navController.navigate("category_list_screen")
|
||||
}
|
||||
)
|
||||
},
|
||||
allCardsContent = {
|
||||
AllCardsView(
|
||||
vocabularyItems = vocabularyItems,
|
||||
allLanguages = allLanguages,
|
||||
selection = selection,
|
||||
onItemClick = { item ->
|
||||
if (isInSelectionMode) {
|
||||
selection = if (selection.contains(item.id.toLong())) {
|
||||
selection - item.id.toLong()
|
||||
} else {
|
||||
selection + item.id.toLong()
|
||||
}
|
||||
} else {
|
||||
vocabularyViewModel.setNavigationContext(vocabularyItems, item.id)
|
||||
navController.navigate("vocabulary_detail/${item.id}")
|
||||
}
|
||||
},
|
||||
onItemLongClick = { item ->
|
||||
if (!isInSelectionMode) {
|
||||
selection = setOf(item.id.toLong())
|
||||
}
|
||||
},
|
||||
onDeleteClick = { item ->
|
||||
vocabularyViewModel.deleteData(
|
||||
VocabularyViewModel.DeleteType.VOCABULARY_ITEM,
|
||||
item = item
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
// Floating Action Button for scrolling to top
|
||||
val showFab by remember {
|
||||
derivedStateOf { lazyListState.firstVisibleItemIndex > 5 && !isInSelectionMode }
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = showFab,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(bottom = 16.dp)
|
||||
) {
|
||||
FloatingActionButton(
|
||||
onClick = { scope.launch { lazyListState.animateScrollToItem(0) } },
|
||||
shape = CircleShape,
|
||||
modifier = Modifier.size(50.dp),
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
) {
|
||||
Icon(AppIcons.ArrowCircleUp, contentDescription = "Scroll to top")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showFilterSheet) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { showFilterSheet = false },
|
||||
sheetState = sheetState,
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
dragHandle = { BottomSheetDefaults.DragHandle() }
|
||||
) {
|
||||
FilterBottomSheetContent(
|
||||
currentFilterState = filterState,
|
||||
languageViewModel = languageViewModel,
|
||||
languagesPresent = allLanguages.filter { it.nameResId in languagesPresent },
|
||||
onApplyFilters = { newState ->
|
||||
filterState = newState
|
||||
showFilterSheet = false
|
||||
scope.launch { lazyListState.scrollToItem(0) }
|
||||
},
|
||||
onResetClick = {
|
||||
filterState = LibraryFilterState()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showCategoryDialog) {
|
||||
val selectedItems = vocabularyItems.filter { selection.contains(it.id.toLong()) }
|
||||
CategorySelectionDialog(
|
||||
onCategorySelected = {
|
||||
vocabularyViewModel.addVocabularyItemToCategories(
|
||||
selectedItems,
|
||||
it.mapNotNull { category -> category?.id }
|
||||
)
|
||||
showCategoryDialog = false
|
||||
selection = emptySet()
|
||||
},
|
||||
onDismissRequest = { showCategoryDialog = false }
|
||||
)
|
||||
}
|
||||
|
||||
if (showStageDialog) {
|
||||
val selectedItems = vocabularyItems.filter { selection.contains(it.id.toLong()) }
|
||||
StageSelectionDialog(
|
||||
onStageSelected = { selectedStage ->
|
||||
selectedStage?.let {
|
||||
vocabularyViewModel.addVocabularyItemToStage(selectedItems, it)
|
||||
}
|
||||
showStageDialog = false
|
||||
selection = emptySet()
|
||||
},
|
||||
onDismissRequest = { showStageDialog = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FilterBottomSheetContent(
|
||||
currentFilterState: LibraryFilterState,
|
||||
languageViewModel: LanguageViewModel,
|
||||
languagesPresent: List<eu.gaudian.translator.model.Language>,
|
||||
onApplyFilters: (LibraryFilterState) -> Unit,
|
||||
onResetClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var selectedStage by rememberSaveable { mutableStateOf(currentFilterState.selectedStage) }
|
||||
var dueTodayOnly by rememberSaveable { mutableStateOf(currentFilterState.dueTodayOnly) }
|
||||
var selectedLanguageIds by rememberSaveable { mutableStateOf(currentFilterState.selectedLanguageIds) }
|
||||
var selectedWordClass by rememberSaveable { mutableStateOf(currentFilterState.selectedWordClass) }
|
||||
var sortOrder by rememberSaveable { mutableStateOf(currentFilterState.sortOrder) }
|
||||
|
||||
val context = LocalContext.current
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val languageConfigViewModel: LanguageConfigViewModel = hiltViewModel(activity)
|
||||
val allWordClasses by languageConfigViewModel.allWordClasses.collectAsStateWithLifecycle()
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp)
|
||||
.padding(bottom = 32.dp)
|
||||
.navigationBarsPadding()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Filter Cards",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
TextButton(onClick = {
|
||||
selectedStage = null
|
||||
dueTodayOnly = false
|
||||
selectedLanguageIds = emptyList()
|
||||
selectedWordClass = null
|
||||
sortOrder = SortOrder.NEWEST_FIRST
|
||||
onResetClick()
|
||||
}) {
|
||||
Text("Reset")
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f))
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f, fill = false)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp)
|
||||
) {
|
||||
// Sort Order
|
||||
Column {
|
||||
Text(
|
||||
text = "SORT BY",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
letterSpacing = 1.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 12.dp)
|
||||
)
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
SortOrder.entries.forEach { order ->
|
||||
FilterChip(
|
||||
selected = sortOrder == order,
|
||||
onClick = { sortOrder = order },
|
||||
label = {
|
||||
Text(order.name.replace('_', ' ').lowercase()
|
||||
.replaceFirstChar { it.titlecase() })
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Due Today
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.text_due_today_only).uppercase(),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
letterSpacing = 1.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
AppSwitch(checked = dueTodayOnly, onCheckedChange = { dueTodayOnly = it })
|
||||
}
|
||||
|
||||
// Stages
|
||||
Column {
|
||||
Text(
|
||||
text = "STAGES",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
letterSpacing = 1.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 12.dp)
|
||||
)
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
FilterChip(
|
||||
selected = selectedStage == null,
|
||||
onClick = { selectedStage = null },
|
||||
label = { Text(stringResource(R.string.label_all_stages)) }
|
||||
)
|
||||
VocabularyStage.entries.forEach { stage ->
|
||||
FilterChip(
|
||||
selected = selectedStage == stage,
|
||||
onClick = { selectedStage = stage },
|
||||
label = { Text(stage.toString(context)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Languages
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.language).uppercase(),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
letterSpacing = 1.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 12.dp)
|
||||
)
|
||||
MultipleLanguageDropdown(
|
||||
languageViewModel = languageViewModel,
|
||||
onLanguagesSelected = { languages ->
|
||||
selectedLanguageIds = languages.map { it.nameResId }
|
||||
},
|
||||
alternateLanguages = languagesPresent
|
||||
)
|
||||
}
|
||||
|
||||
// Word Class
|
||||
if (allWordClasses.isNotEmpty()) {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.filter_by_word_type).uppercase(),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
letterSpacing = 1.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 12.dp)
|
||||
)
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
FilterChip(
|
||||
selected = selectedWordClass == null,
|
||||
onClick = { selectedWordClass = null },
|
||||
label = { Text(stringResource(R.string.label_all_types)) }
|
||||
)
|
||||
allWordClasses.forEach { wordClass ->
|
||||
FilterChip(
|
||||
selected = selectedWordClass == wordClass,
|
||||
onClick = { selectedWordClass = wordClass },
|
||||
label = { Text(wordClass.replaceFirstChar { it.titlecase() }) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
onApplyFilters(
|
||||
currentFilterState.copy(
|
||||
selectedStage = selectedStage,
|
||||
dueTodayOnly = dueTodayOnly,
|
||||
selectedLanguageIds = selectedLanguageIds,
|
||||
selectedWordClass = selectedWordClass,
|
||||
sortOrder = sortOrder
|
||||
)
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(28.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Apply Filters",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,16 @@ package eu.gaudian.translator.view.vocabulary
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -30,6 +32,8 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.navigation.NavHostController
|
||||
@@ -42,10 +46,12 @@ import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppScaffold
|
||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||
import eu.gaudian.translator.view.composable.PrimaryButton
|
||||
import eu.gaudian.translator.view.composable.SecondaryButton
|
||||
import eu.gaudian.translator.view.dialogs.DeleteCategoryDialog
|
||||
import eu.gaudian.translator.view.dialogs.DeleteItemsDialog
|
||||
import eu.gaudian.translator.view.dialogs.EditCategoryDialog
|
||||
import eu.gaudian.translator.view.vocabulary.widgets.CategoryProgressCircle
|
||||
import eu.gaudian.translator.viewmodel.CategoryProgress
|
||||
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||
import eu.gaudian.translator.viewmodel.ProgressViewModel
|
||||
@@ -88,11 +94,10 @@ fun CategoryDetailScreen(
|
||||
if (!hasLangList && !hasPair && !hasStages) {
|
||||
append(stringResource(R.string.text_filter_all_items))
|
||||
} else {
|
||||
//append(stringResource(R.string.filter))
|
||||
append(" ")
|
||||
if (hasPair) {
|
||||
val (a,b) = cat.languagePairs
|
||||
append("[${languages.value.find{ it.nameResId == a }?.name} - ${languages.value.find{ it.nameResId == b }?.name}]")
|
||||
val (a, b) = cat.languagePairs
|
||||
append("[${languages.value.find { it.nameResId == a }?.name} - ${languages.value.find { it.nameResId == b }?.name}]")
|
||||
} else if (hasLangList) {
|
||||
append(cat.languages.joinToString(", ") { langId -> languages.value.find { it.nameResId == langId }?.name.toString() })
|
||||
} else {
|
||||
@@ -127,85 +132,49 @@ fun CategoryDetailScreen(
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false },
|
||||
modifier = Modifier.width(220.dp)
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.text_edit_category)) },
|
||||
onClick = {
|
||||
categoryViewModel.setShowEditCategoryDialog(true, categoryId)
|
||||
showMenu = false
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.text_export_category)) },
|
||||
onClick = {
|
||||
vocabularyViewModel.saveCategory(categoryId)
|
||||
showMenu = false
|
||||
}
|
||||
},
|
||||
leadingIcon = { Icon(AppIcons.Share, contentDescription = null) }
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.delete_items_category)) },
|
||||
onClick = {
|
||||
categoryViewModel.setShowDeleteItemsDialog(true, categoryId)
|
||||
showMenu = false
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.text_delete_category)) },
|
||||
onClick = {
|
||||
categoryViewModel.setShowDeleteCategoryDialog(true, categoryId)
|
||||
showMenu = false
|
||||
}
|
||||
},
|
||||
leadingIcon = { Icon(AppIcons.Delete, contentDescription = null) }
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
// TODO: Review this
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
)
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp, horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (categoryProgress != null) {
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
CategoryProgressCircle(
|
||||
totalItems = categoryProgress.totalItems,
|
||||
itemsCompleted = categoryProgress.itemsCompleted,
|
||||
itemsInStages = categoryProgress.itemsInStages,
|
||||
newItems = categoryProgress.newItems,
|
||||
circleSize = 80.dp,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
// Category Header Card with Progress and Action Buttons
|
||||
CategoryHeaderCard(
|
||||
subtitle = subtitle,
|
||||
categoryProgress = categoryProgress,
|
||||
onStartExerciseClick = {
|
||||
val categories = listOf(category)
|
||||
val categoryIds = categories.joinToString(",") { it?.id.toString() }
|
||||
navController.navigate("vocabulary_exercise/false?categories=$categoryIds")
|
||||
},
|
||||
onEditClick = {
|
||||
categoryViewModel.setShowEditCategoryDialog(true, categoryId)
|
||||
},
|
||||
onDeleteClick = {
|
||||
categoryViewModel.setShowDeleteCategoryDialog(true, categoryId)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 8.dp)
|
||||
) {
|
||||
PrimaryButton(
|
||||
text = stringResource(R.string.label_start),
|
||||
icon = AppIcons.Play,
|
||||
onClick = {
|
||||
val categories = listOf(category)
|
||||
val categoryIds = categories.joinToString(",") { it?.id.toString() }
|
||||
navController.navigate("vocabulary_exercise/false?categories=$categoryIds")
|
||||
},
|
||||
modifier = Modifier.heightIn(max = 80.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
@@ -214,7 +183,7 @@ fun CategoryDetailScreen(
|
||||
categoryId = categoryId,
|
||||
showDueTodayOnly = false,
|
||||
onNavigateToItem = onNavigateToItem,
|
||||
navController = navController, // Pass the received navController here
|
||||
navController = navController,
|
||||
isRemoveFromCategoryEnabled = category is TagCategory,
|
||||
showTopBar = false,
|
||||
enableNavigationButtons = true
|
||||
@@ -243,3 +212,131 @@ fun CategoryDetailScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CategoryHeaderCard(
|
||||
subtitle: String,
|
||||
categoryProgress: CategoryProgress?,
|
||||
onStartExerciseClick: () -> Unit,
|
||||
onEditClick: () -> Unit,
|
||||
onDeleteClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(20.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
// Subtitle
|
||||
if (subtitle.isNotBlank()) {
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// Progress Circle
|
||||
if (categoryProgress != null) {
|
||||
CategoryProgressCircle(
|
||||
totalItems = categoryProgress.totalItems,
|
||||
itemsCompleted = categoryProgress.itemsCompleted,
|
||||
itemsInStages = categoryProgress.itemsInStages,
|
||||
newItems = categoryProgress.newItems,
|
||||
circleSize = 120.dp,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
}
|
||||
|
||||
// Action Buttons
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
// Start Exercise Button (Primary)
|
||||
PrimaryButton(
|
||||
text = stringResource(R.string.label_start_exercise),
|
||||
icon = AppIcons.Play,
|
||||
onClick = onStartExerciseClick,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// Secondary Action Buttons
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
// Edit Button
|
||||
SecondaryButton(
|
||||
text = stringResource(R.string.label_edit),
|
||||
icon = AppIcons.Edit,
|
||||
onClick = onEditClick,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
// Delete Button
|
||||
SecondaryButton(
|
||||
text = stringResource(R.string.label_delete),
|
||||
icon = AppIcons.Delete,
|
||||
onClick = onDeleteClick,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== PREVIEWS ====================
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun CategoryHeaderCardPreview() {
|
||||
MaterialTheme {
|
||||
CategoryHeaderCard(
|
||||
subtitle = "German - English | All Stages",
|
||||
categoryProgress = null,
|
||||
onStartExerciseClick = {},
|
||||
onEditClick = {},
|
||||
onDeleteClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun CategoryHeaderCardWithProgressPreview() {
|
||||
MaterialTheme {
|
||||
CategoryHeaderCard(
|
||||
subtitle = "Travel Vocabulary",
|
||||
categoryProgress = eu.gaudian.translator.viewmodel.CategoryProgress(
|
||||
vocabularyCategory = eu.gaudian.translator.model.TagCategory(
|
||||
1,
|
||||
"Travel"
|
||||
),
|
||||
totalItems = 50,
|
||||
newItems = 15,
|
||||
itemsInStages = 25,
|
||||
itemsCompleted = 10
|
||||
),
|
||||
onStartExerciseClick = {},
|
||||
onEditClick = {},
|
||||
onDeleteClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1118,4 +1118,5 @@
|
||||
<string name="label_stats">Stats</string>
|
||||
<string name="label_library">Library</string>
|
||||
<string name="label_legacy_vocabulary">Legacy Vocabulary</string>
|
||||
<string name="label_edit">Edit</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user