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
|
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.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.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 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
|
@Composable
|
||||||
fun LibraryScreen(
|
fun LibraryScreen(
|
||||||
navController: NavHostController,
|
navController: NavHostController,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
NewVocListScreen(
|
val scope = rememberCoroutineScope()
|
||||||
navController = navController,
|
val activity = LocalContext.current.findActivity()
|
||||||
enableNavigationButtons = true,
|
val lazyListState = rememberLazyListState()
|
||||||
onNavigateToItem = {},
|
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||||
modifier = modifier
|
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 android.annotation.SuppressLint
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
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.padding
|
||||||
import androidx.compose.foundation.layout.width
|
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.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@@ -30,6 +32,8 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
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.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.navigation.NavHostController
|
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.AppScaffold
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
import eu.gaudian.translator.view.composable.PrimaryButton
|
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.DeleteCategoryDialog
|
||||||
import eu.gaudian.translator.view.dialogs.DeleteItemsDialog
|
import eu.gaudian.translator.view.dialogs.DeleteItemsDialog
|
||||||
import eu.gaudian.translator.view.dialogs.EditCategoryDialog
|
import eu.gaudian.translator.view.dialogs.EditCategoryDialog
|
||||||
import eu.gaudian.translator.view.vocabulary.widgets.CategoryProgressCircle
|
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.CategoryViewModel
|
||||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||||
import eu.gaudian.translator.viewmodel.ProgressViewModel
|
import eu.gaudian.translator.viewmodel.ProgressViewModel
|
||||||
@@ -88,11 +94,10 @@ fun CategoryDetailScreen(
|
|||||||
if (!hasLangList && !hasPair && !hasStages) {
|
if (!hasLangList && !hasPair && !hasStages) {
|
||||||
append(stringResource(R.string.text_filter_all_items))
|
append(stringResource(R.string.text_filter_all_items))
|
||||||
} else {
|
} else {
|
||||||
//append(stringResource(R.string.filter))
|
|
||||||
append(" ")
|
append(" ")
|
||||||
if (hasPair) {
|
if (hasPair) {
|
||||||
val (a,b) = cat.languagePairs
|
val (a, b) = cat.languagePairs
|
||||||
append("[${languages.value.find{ it.nameResId == a }?.name} - ${languages.value.find{ it.nameResId == b }?.name}]")
|
append("[${languages.value.find { it.nameResId == a }?.name} - ${languages.value.find { it.nameResId == b }?.name}]")
|
||||||
} else if (hasLangList) {
|
} else if (hasLangList) {
|
||||||
append(cat.languages.joinToString(", ") { langId -> languages.value.find { it.nameResId == langId }?.name.toString() })
|
append(cat.languages.joinToString(", ") { langId -> languages.value.find { it.nameResId == langId }?.name.toString() })
|
||||||
} else {
|
} else {
|
||||||
@@ -127,85 +132,49 @@ fun CategoryDetailScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
|
|
||||||
expanded = showMenu,
|
expanded = showMenu,
|
||||||
onDismissRequest = { showMenu = false },
|
onDismissRequest = { showMenu = false },
|
||||||
modifier = Modifier.width(220.dp)
|
modifier = Modifier.width(220.dp)
|
||||||
) {
|
) {
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringResource(R.string.text_edit_category)) },
|
|
||||||
onClick = {
|
|
||||||
categoryViewModel.setShowEditCategoryDialog(true, categoryId)
|
|
||||||
showMenu = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(stringResource(R.string.text_export_category)) },
|
text = { Text(stringResource(R.string.text_export_category)) },
|
||||||
onClick = {
|
onClick = {
|
||||||
vocabularyViewModel.saveCategory(categoryId)
|
vocabularyViewModel.saveCategory(categoryId)
|
||||||
showMenu = false
|
showMenu = false
|
||||||
}
|
},
|
||||||
|
leadingIcon = { Icon(AppIcons.Share, contentDescription = null) }
|
||||||
)
|
)
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(stringResource(R.string.delete_items_category)) },
|
text = { Text(stringResource(R.string.delete_items_category)) },
|
||||||
onClick = {
|
onClick = {
|
||||||
categoryViewModel.setShowDeleteItemsDialog(true, categoryId)
|
categoryViewModel.setShowDeleteItemsDialog(true, categoryId)
|
||||||
showMenu = false
|
showMenu = false
|
||||||
}
|
},
|
||||||
)
|
leadingIcon = { Icon(AppIcons.Delete, contentDescription = null) }
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringResource(R.string.text_delete_category)) },
|
|
||||||
onClick = {
|
|
||||||
categoryViewModel.setShowDeleteCategoryDialog(true, categoryId)
|
|
||||||
showMenu = false
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
// TODO: Review this
|
|
||||||
containerColor = MaterialTheme.colorScheme.surface
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
Row(
|
// Category Header Card with Progress and Action Buttons
|
||||||
modifier = Modifier
|
CategoryHeaderCard(
|
||||||
.fillMaxWidth()
|
subtitle = subtitle,
|
||||||
.padding(vertical = 8.dp, horizontal = 16.dp),
|
categoryProgress = categoryProgress,
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
onStartExerciseClick = {
|
||||||
verticalAlignment = Alignment.CenterVertically
|
val categories = listOf(category)
|
||||||
) {
|
val categoryIds = categories.joinToString(",") { it?.id.toString() }
|
||||||
if (categoryProgress != null) {
|
navController.navigate("vocabulary_exercise/false?categories=$categoryIds")
|
||||||
Box(modifier = Modifier.weight(1f)) {
|
},
|
||||||
CategoryProgressCircle(
|
onEditClick = {
|
||||||
totalItems = categoryProgress.totalItems,
|
categoryViewModel.setShowEditCategoryDialog(true, categoryId)
|
||||||
itemsCompleted = categoryProgress.itemsCompleted,
|
},
|
||||||
itemsInStages = categoryProgress.itemsInStages,
|
onDeleteClick = {
|
||||||
newItems = categoryProgress.newItems,
|
categoryViewModel.setShowDeleteCategoryDialog(true, categoryId)
|
||||||
circleSize = 80.dp,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
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 ->
|
) { paddingValues ->
|
||||||
@@ -214,7 +183,7 @@ fun CategoryDetailScreen(
|
|||||||
categoryId = categoryId,
|
categoryId = categoryId,
|
||||||
showDueTodayOnly = false,
|
showDueTodayOnly = false,
|
||||||
onNavigateToItem = onNavigateToItem,
|
onNavigateToItem = onNavigateToItem,
|
||||||
navController = navController, // Pass the received navController here
|
navController = navController,
|
||||||
isRemoveFromCategoryEnabled = category is TagCategory,
|
isRemoveFromCategoryEnabled = category is TagCategory,
|
||||||
showTopBar = false,
|
showTopBar = false,
|
||||||
enableNavigationButtons = true
|
enableNavigationButtons = true
|
||||||
@@ -242,4 +211,132 @@ 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_stats">Stats</string>
|
||||||
<string name="label_library">Library</string>
|
<string name="label_library">Library</string>
|
||||||
<string name="label_legacy_vocabulary">Legacy Vocabulary</string>
|
<string name="label_legacy_vocabulary">Legacy Vocabulary</string>
|
||||||
|
<string name="label_edit">Edit</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user