refactor CategoryDropdown and improve vocabulary filtering with multi-category support
This commit is contained in:
@@ -15,29 +15,34 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
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.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.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.model.TagCategory
|
||||
import eu.gaudian.translator.model.VocabularyCategory
|
||||
import eu.gaudian.translator.model.VocabularyFilter
|
||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.composable.AppButton
|
||||
import eu.gaudian.translator.view.composable.AppCheckbox
|
||||
import eu.gaudian.translator.view.composable.AppDropdownMenuItem
|
||||
import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppOutlinedButton
|
||||
import eu.gaudian.translator.view.composable.AppOutlinedTextField
|
||||
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
||||
|
||||
|
||||
/**
|
||||
@@ -95,7 +100,7 @@ fun CategoryDropdownContent(
|
||||
text = when {
|
||||
state.selectedCategories.isEmpty() -> stringResource(R.string.text_select_category)
|
||||
state.selectedCategories.size == 1 -> state.selectedCategories.first()?.name
|
||||
?: stringResource(R.string.text_none)
|
||||
?: stringResource(R.string.label_no_category)
|
||||
else -> stringResource(R.string.text_2d_categories_selected, state.selectedCategories.size)
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
@@ -139,7 +144,7 @@ fun CategoryDropdownContent(
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
}
|
||||
Text(stringResource(R.string.text_none))
|
||||
Text(stringResource(R.string.label_no_category))
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
@@ -270,13 +275,13 @@ fun CategoryDropdownContent(
|
||||
*/
|
||||
@Composable
|
||||
fun CategoryDropdown(
|
||||
modifier: Modifier = Modifier,
|
||||
initialCategoryId: Int? = null,
|
||||
onCategorySelected: (List<VocabularyCategory?>) -> Unit,
|
||||
noneSelectable: Boolean? = true,
|
||||
multipleSelectable: Boolean = false,
|
||||
onlyLists: Boolean = false,
|
||||
addCategory: Boolean = false,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
var selectedCategories by remember {
|
||||
@@ -284,9 +289,12 @@ fun CategoryDropdown(
|
||||
}
|
||||
var newCategoryName by remember { mutableStateOf("") }
|
||||
|
||||
// For production use, this would come from ViewModel
|
||||
// For preview, we'll use empty list or pass via state
|
||||
val categories by remember { mutableStateOf(emptyList<VocabularyCategory>()) }
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
|
||||
|
||||
|
||||
|
||||
|
||||
// Find initial category
|
||||
val initialCategory = remember(categories, initialCategoryId) {
|
||||
|
||||
@@ -8,9 +8,6 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
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.platform.LocalContext
|
||||
@@ -18,7 +15,6 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.model.TagCategory
|
||||
import eu.gaudian.translator.model.VocabularyCategory
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.composable.AppDialog
|
||||
@@ -34,9 +30,6 @@ fun CategorySelectionDialog(
|
||||
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
|
||||
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
|
||||
var selectedCategories by remember { mutableStateOf<List<VocabularyCategory?>>(emptyList()) }
|
||||
var newCategoryName by remember { mutableStateOf("") }
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
AppDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
@@ -45,21 +38,8 @@ fun CategorySelectionDialog(
|
||||
}
|
||||
) {
|
||||
// Dropdown button and menu
|
||||
CategoryDropdownContent(
|
||||
state = CategoryDropdownState(
|
||||
expanded = expanded,
|
||||
selectedCategories = selectedCategories,
|
||||
newCategoryName = newCategoryName,
|
||||
categories = categories,
|
||||
),
|
||||
onExpand = { isExpanded -> expanded = isExpanded },
|
||||
onCategorySelected = { selectedCategories = it },
|
||||
onNewCategoryNameChange = { newCategoryName = it },
|
||||
onAddCategory = { name ->
|
||||
val newCategory = TagCategory(id = 0, name = name.trim())
|
||||
categoryViewModel.createCategory(newCategory)
|
||||
newCategoryName = ""
|
||||
},
|
||||
CategoryDropdown(
|
||||
onCategorySelected = onCategorySelected,
|
||||
noneSelectable = false,
|
||||
multipleSelectable = true,
|
||||
onlyLists = true,
|
||||
@@ -79,10 +59,11 @@ fun CategorySelectionDialog(
|
||||
|
||||
DialogButton(
|
||||
onClick = {
|
||||
onCategorySelected(selectedCategories)
|
||||
// The selected categories are handled by CategoryDropdown's internal state
|
||||
// and passed to onCategorySelected callback
|
||||
onDismissRequest()
|
||||
},
|
||||
enabled = selectedCategories.isNotEmpty()
|
||||
enabled = true // Always enabled since CategoryDropdown handles validation
|
||||
) {
|
||||
Text(stringResource(R.string.label_confirm))
|
||||
}
|
||||
|
||||
@@ -55,9 +55,8 @@ fun StartExerciseDialog(
|
||||
// Map displayed Language to its DB id (lid) using position mapping from load
|
||||
var languageIdMap by remember { mutableStateOf<Map<Language, Int>>(emptyMap()) }
|
||||
var selectedLanguages by remember { mutableStateOf<List<Language>>(emptyList()) }
|
||||
var selectedCategories by remember { mutableStateOf<List<VocabularyCategory>>(emptyList()) }
|
||||
var selectedStages by remember { mutableStateOf<List<VocabularyStage>>(emptyList()) }
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
var selectedCategories by remember { mutableStateOf<List<VocabularyCategory>>(emptyList()) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
coroutineScope.launch {
|
||||
@@ -87,19 +86,10 @@ fun StartExerciseDialog(
|
||||
},
|
||||
languages
|
||||
)
|
||||
CategoryDropdownContent(
|
||||
state = CategoryDropdownState(
|
||||
expanded = expanded,
|
||||
selectedCategories = selectedCategories,
|
||||
newCategoryName = "",
|
||||
categories = categories,
|
||||
),
|
||||
onExpand = { isExpanded -> expanded = isExpanded },
|
||||
CategoryDropdown(
|
||||
onCategorySelected = { cats ->
|
||||
selectedCategories = cats.filterIsInstance<VocabularyCategory>()
|
||||
},
|
||||
onNewCategoryNameChange = {},
|
||||
onAddCategory = {},
|
||||
multipleSelectable = true,
|
||||
onlyLists = false, // Show both filters and lists
|
||||
addCategory = false,
|
||||
|
||||
@@ -27,7 +27,6 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.model.TagCategory
|
||||
import eu.gaudian.translator.model.VocabularyCategory
|
||||
import eu.gaudian.translator.model.VocabularyItem
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
@@ -54,8 +53,6 @@ fun VocabularyReviewScreen(
|
||||
val selectedItems = remember { mutableStateListOf<VocabularyItem>() }
|
||||
val duplicates = remember { mutableStateListOf<Boolean>() }
|
||||
var selectedCategories by remember { mutableStateOf<List<VocabularyCategory?>>(emptyList()) }
|
||||
var newCategoryName by remember { mutableStateOf("") }
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(generatedItems) {
|
||||
val duplicateResults = vocabularyViewModel.findDuplicates(generatedItems)
|
||||
@@ -134,21 +131,8 @@ fun VocabularyReviewScreen(
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
modifier = Modifier.padding(8.dp)
|
||||
)
|
||||
CategoryDropdownContent(
|
||||
state = CategoryDropdownState(
|
||||
expanded = expanded,
|
||||
selectedCategories = selectedCategories,
|
||||
newCategoryName = newCategoryName,
|
||||
categories = categories,
|
||||
),
|
||||
onExpand = { isExpanded -> expanded = isExpanded },
|
||||
CategoryDropdown(
|
||||
onCategorySelected = { selectedCategories = it },
|
||||
onNewCategoryNameChange = { newCategoryName = it },
|
||||
onAddCategory = { name ->
|
||||
val newCategory = TagCategory(id = 0, name = name.trim())
|
||||
categoryViewModel.createCategory(newCategory)
|
||||
newCategoryName = ""
|
||||
},
|
||||
noneSelectable = false,
|
||||
multipleSelectable = true,
|
||||
onlyLists = true,
|
||||
|
||||
@@ -238,7 +238,7 @@ fun VocabularyCardHost(
|
||||
listOf(currentVocabularyItem),
|
||||
it.mapNotNull { category -> category?.id }
|
||||
)
|
||||
showCategoryDialog = false
|
||||
//showCategoryDialog = false
|
||||
},
|
||||
onDismissRequest = { showCategoryDialog = false }
|
||||
)
|
||||
|
||||
@@ -103,7 +103,7 @@ private data class VocabularyFilterState(
|
||||
val searchQuery: String = "",
|
||||
val selectedStage: VocabularyStage? = null,
|
||||
val sortOrder: SortOrder = SortOrder.NEWEST_FIRST,
|
||||
val categoryId: Int? = null,
|
||||
val categoryIds: List<Int> = emptyList(),
|
||||
val dueTodayOnly: Boolean = false,
|
||||
val selectedLanguageIds: List<Int> = emptyList(),
|
||||
val selectedWordClass: String? = null
|
||||
@@ -133,7 +133,7 @@ fun VocabularyListScreen(
|
||||
var filterState by rememberSaveable {
|
||||
mutableStateOf(
|
||||
VocabularyFilterState(
|
||||
categoryId = categoryId,
|
||||
categoryIds = categoryId?.let { listOf(it) } ?: emptyList(),
|
||||
dueTodayOnly = showDueTodayOnly == true,
|
||||
selectedStage = stage
|
||||
)
|
||||
@@ -142,7 +142,7 @@ fun VocabularyListScreen(
|
||||
val isFilterActive by remember(filterState) {
|
||||
derivedStateOf {
|
||||
filterState.selectedStage != null ||
|
||||
(filterState.categoryId != null && filterState.categoryId != 0) ||
|
||||
filterState.categoryIds.isNotEmpty() ||
|
||||
filterState.dueTodayOnly ||
|
||||
filterState.selectedLanguageIds.isNotEmpty() ||
|
||||
!filterState.selectedWordClass.isNullOrBlank()
|
||||
@@ -165,7 +165,7 @@ fun VocabularyListScreen(
|
||||
vocabularyViewModel.filterVocabularyItems(
|
||||
languages = filterState.selectedLanguageIds,
|
||||
query = filterState.searchQuery.takeIf { it.isNotBlank() },
|
||||
categoryId = filterState.categoryId,
|
||||
categoryIds = filterState.categoryIds,
|
||||
stage = filterState.selectedStage,
|
||||
wordClass = filterState.selectedWordClass,
|
||||
dueTodayOnly = filterState.dueTodayOnly,
|
||||
@@ -179,7 +179,7 @@ fun VocabularyListScreen(
|
||||
|
||||
LaunchedEffect(categoryId, showDueTodayOnly, stage) {
|
||||
filterState = filterState.copy(
|
||||
categoryId = categoryId,
|
||||
categoryIds = categoryId?.let { listOf(it) } ?: emptyList(),
|
||||
dueTodayOnly = showDueTodayOnly == true,
|
||||
selectedStage = stage
|
||||
)
|
||||
@@ -382,7 +382,8 @@ fun VocabularyListScreen(
|
||||
languageViewModel = languageViewModel,
|
||||
languagesPresent = allLanguages.filter { it.nameResId in languagesPresent },
|
||||
hideCategory = categoryId != null && categoryId != 0,
|
||||
hideStage = stage != null
|
||||
hideStage = stage != null,
|
||||
categoryViewModel = categoryViewModel
|
||||
)
|
||||
}
|
||||
|
||||
@@ -394,7 +395,7 @@ fun VocabularyListScreen(
|
||||
selectedItems,
|
||||
it.mapNotNull { category -> category?.id }
|
||||
)
|
||||
showCategoryDialog = false
|
||||
//showCategoryDialog = false
|
||||
},
|
||||
onDismissRequest = { showCategoryDialog = false }
|
||||
)
|
||||
@@ -807,11 +808,12 @@ private fun FilterSortBottomSheet(
|
||||
onDismiss: () -> Unit,
|
||||
onApplyFilters: (VocabularyFilterState) -> Unit,
|
||||
hideCategory: Boolean = false,
|
||||
hideStage: Boolean = false
|
||||
hideStage: Boolean = false,
|
||||
categoryViewModel: CategoryViewModel
|
||||
) {
|
||||
var selectedStage by rememberSaveable { mutableStateOf(currentFilterState.selectedStage) }
|
||||
var dueTodayOnly by rememberSaveable { mutableStateOf(currentFilterState.dueTodayOnly) }
|
||||
var selectedCategoryId by rememberSaveable { mutableStateOf(currentFilterState.categoryId) }
|
||||
var selectedCategoryIds by rememberSaveable { mutableStateOf(currentFilterState.categoryIds) }
|
||||
var selectedLanguageIds by rememberSaveable { mutableStateOf(currentFilterState.selectedLanguageIds) }
|
||||
var selectedWordClass by rememberSaveable { mutableStateOf(currentFilterState.selectedWordClass) }
|
||||
|
||||
@@ -840,7 +842,7 @@ private fun FilterSortBottomSheet(
|
||||
TextButton(onClick = {
|
||||
if (!hideStage) selectedStage = null
|
||||
dueTodayOnly = false
|
||||
if (!hideCategory) selectedCategoryId = null
|
||||
if (!hideCategory) selectedCategoryIds = emptyList()
|
||||
selectedLanguageIds = emptyList()
|
||||
selectedWordClass = null
|
||||
}) {
|
||||
@@ -853,7 +855,7 @@ private fun FilterSortBottomSheet(
|
||||
currentFilterState.copy(
|
||||
selectedStage = selectedStage,
|
||||
dueTodayOnly = dueTodayOnly,
|
||||
categoryId = selectedCategoryId,
|
||||
categoryIds = selectedCategoryIds,
|
||||
selectedLanguageIds = selectedLanguageIds,
|
||||
selectedWordClass = selectedWordClass
|
||||
)
|
||||
@@ -893,16 +895,18 @@ private fun FilterSortBottomSheet(
|
||||
Text(stringResource(R.string.label_category), style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
CategoryDropdown(
|
||||
initialCategoryId = selectedCategoryId,
|
||||
initialCategoryId = selectedCategoryIds.firstOrNull(),
|
||||
onCategorySelected = { categories ->
|
||||
selectedCategoryId = categories.firstOrNull()?.id
|
||||
}
|
||||
selectedCategoryIds = categories.mapNotNull { it?.id }
|
||||
},
|
||||
multipleSelectable = true,
|
||||
noneSelectable = false
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
if (!hideStage) {
|
||||
Text(stringResource(R.string.filter_by_stage), style = MaterialTheme.typography.titleMedium)
|
||||
Text(stringResource(R.string.label_filter_by_stage), style = MaterialTheme.typography.titleMedium)
|
||||
FlowRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
@@ -955,6 +959,7 @@ private fun FilterSortBottomSheet(
|
||||
fun FilterSortBottomSheetPreview() {
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
FilterSortBottomSheet(
|
||||
currentFilterState = VocabularyFilterState(),
|
||||
languageViewModel = languageViewModel,
|
||||
@@ -962,6 +967,7 @@ fun FilterSortBottomSheetPreview() {
|
||||
onDismiss = {},
|
||||
onApplyFilters = {},
|
||||
hideCategory = false,
|
||||
hideStage = false
|
||||
hideStage = false,
|
||||
categoryViewModel = categoryViewModel
|
||||
)
|
||||
}
|
||||
@@ -299,6 +299,7 @@ fun VocabularySortingItem(
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val languageConfigViewModel: LanguageConfigViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
var wordFirst by remember { mutableStateOf(item.wordFirst) }
|
||||
var wordSecond by remember { mutableStateOf(item.wordSecond) }
|
||||
var selectedCategories by remember { mutableStateOf<List<Int>>(emptyList()) }
|
||||
@@ -313,6 +314,7 @@ fun VocabularySortingItem(
|
||||
var articlesLangSecond by remember { mutableStateOf(emptySet<String>()) }
|
||||
|
||||
var showDuplicateDialog by remember { mutableStateOf(false) }
|
||||
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
|
||||
|
||||
// NEW: Calculate if the item is valid for the "Done" button in faulty mode
|
||||
val isItemNowValid by remember(wordFirst, wordSecond, langFirst, langSecond) {
|
||||
|
||||
@@ -360,7 +360,7 @@
|
||||
<string name="days_2d">%1$d Tage</string>
|
||||
<string name="progress_by_category">Fortschritt nach Kategorie</string>
|
||||
<string name="label_apply_filters">Filter anwenden</string>
|
||||
<string name="filter_by_stage">Nach Stufe filtern</string>
|
||||
<string name="label_filter_by_stage">Nach Stufe filtern</string>
|
||||
<string name="label_category">Kategorie</string>
|
||||
<string name="language">Sprache</string>
|
||||
<string name="label_clear_all">Alle löschen</string>
|
||||
|
||||
@@ -358,7 +358,7 @@
|
||||
<string name="days_2d">%1$d dias</string>
|
||||
<string name="progress_by_category">Progresso por Categoria</string>
|
||||
<string name="label_apply_filters">Aplicar Filtros</string>
|
||||
<string name="filter_by_stage">Filtrar por Estágio</string>
|
||||
<string name="label_filter_by_stage">Filtrar por Estágio</string>
|
||||
<string name="label_category">Categoria</string>
|
||||
<string name="language">Idioma</string>
|
||||
<string name="label_clear_all">Limpar Tudo</string>
|
||||
|
||||
@@ -140,7 +140,7 @@
|
||||
<string name="fetching_grammar_details">Fetching Grammar Details</string>
|
||||
|
||||
<string name="filter_and_sort">Filter and Sort</string>
|
||||
<string name="filter_by_stage">Filter by Stage</string>
|
||||
<string name="label_filter_by_stage">Filter by Stage</string>
|
||||
<string name="filter_by_word_type">Filter by Word Type</string>
|
||||
|
||||
<string name="find_translations">Find Translations</string>
|
||||
@@ -1039,4 +1039,5 @@
|
||||
<string name="duplicate">Duplicate</string>
|
||||
<string name="hint_scan_hint_title">Finding the right AI model</string>
|
||||
<string name="hint_translate_how_it_works">How translation works</string>
|
||||
<string name="label_no_category">None</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user