From 6c669ac310949d58e1ed7a5490797c7dfecce0e7 Mon Sep 17 00:00:00 2001 From: jonasgaudian <43753916+jonasgaudian@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:11:25 +0100 Subject: [PATCH] implement `LibraryScreen` with advanced filtering and refactor `CategoryDetailScreen` --- .../view/library/LibraryComponents.kt | 727 ++++++++++++++++++ .../translator/view/library/LibraryScreen.kt | 512 +++++++++++- .../view/vocabulary/CategoryDetailScreen.kt | 221 ++++-- app/src/main/res/values/strings.xml | 1 + 4 files changed, 1392 insertions(+), 69 deletions(-) create mode 100644 app/src/main/java/eu/gaudian/translator/view/library/LibraryComponents.kt diff --git a/app/src/main/java/eu/gaudian/translator/view/library/LibraryComponents.kt b/app/src/main/java/eu/gaudian/translator/view/library/LibraryComponents.kt new file mode 100644 index 0000000..7dbf560 --- /dev/null +++ b/app/src/main/java/eu/gaudian/translator/view/library/LibraryComponents.kt @@ -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, + allLanguages: List, + selection: Set, + 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, + 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, + 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 = {}) + } +} diff --git a/app/src/main/java/eu/gaudian/translator/view/library/LibraryScreen.kt b/app/src/main/java/eu/gaudian/translator/view/library/LibraryScreen.kt index a781d5b..9c5520e 100644 --- a/app/src/main/java/eu/gaudian/translator/view/library/LibraryScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/library/LibraryScreen.kt @@ -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 = emptyList(), + val dueTodayOnly: Boolean = false, + val selectedLanguageIds: List = 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>(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, + 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 + ) + } + } } diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/CategoryDetailScreen.kt b/app/src/main/java/eu/gaudian/translator/view/vocabulary/CategoryDetailScreen.kt index a3d9897..504dd82 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/CategoryDetailScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/vocabulary/CategoryDetailScreen.kt @@ -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 @@ -242,4 +211,132 @@ fun CategoryDetailScreen( } } } -} \ No newline at end of file +} + +@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 = {} + ) + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a13f26e..e3b2b76 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1118,4 +1118,5 @@ Stats Library Legacy Vocabulary + Edit