refactor CategoryDropdown and improve vocabulary filtering with multi-category support

This commit is contained in:
jonasgaudian
2026-02-15 14:56:23 +01:00
parent fa3524268a
commit a715ab78e9
10 changed files with 53 additions and 81 deletions

View File

@@ -15,29 +15,34 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment 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.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import eu.gaudian.translator.R import eu.gaudian.translator.R
import eu.gaudian.translator.model.TagCategory import eu.gaudian.translator.model.TagCategory
import eu.gaudian.translator.model.VocabularyCategory import eu.gaudian.translator.model.VocabularyCategory
import eu.gaudian.translator.model.VocabularyFilter import eu.gaudian.translator.model.VocabularyFilter
import eu.gaudian.translator.ui.theme.ThemePreviews 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.AppButton
import eu.gaudian.translator.view.composable.AppCheckbox import eu.gaudian.translator.view.composable.AppCheckbox
import eu.gaudian.translator.view.composable.AppDropdownMenuItem import eu.gaudian.translator.view.composable.AppDropdownMenuItem
import eu.gaudian.translator.view.composable.AppIcons import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppOutlinedButton import eu.gaudian.translator.view.composable.AppOutlinedButton
import eu.gaudian.translator.view.composable.AppOutlinedTextField import eu.gaudian.translator.view.composable.AppOutlinedTextField
import eu.gaudian.translator.viewmodel.CategoryViewModel
/** /**
@@ -95,7 +100,7 @@ fun CategoryDropdownContent(
text = when { text = when {
state.selectedCategories.isEmpty() -> stringResource(R.string.text_select_category) state.selectedCategories.isEmpty() -> stringResource(R.string.text_select_category)
state.selectedCategories.size == 1 -> state.selectedCategories.first()?.name 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) else -> stringResource(R.string.text_2d_categories_selected, state.selectedCategories.size)
}, },
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
@@ -139,7 +144,7 @@ fun CategoryDropdownContent(
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
} }
Text(stringResource(R.string.text_none)) Text(stringResource(R.string.label_no_category))
} }
}, },
onClick = { onClick = {
@@ -270,13 +275,13 @@ fun CategoryDropdownContent(
*/ */
@Composable @Composable
fun CategoryDropdown( fun CategoryDropdown(
modifier: Modifier = Modifier,
initialCategoryId: Int? = null, initialCategoryId: Int? = null,
onCategorySelected: (List<VocabularyCategory?>) -> Unit, onCategorySelected: (List<VocabularyCategory?>) -> Unit,
noneSelectable: Boolean? = true, noneSelectable: Boolean? = true,
multipleSelectable: Boolean = false, multipleSelectable: Boolean = false,
onlyLists: Boolean = false, onlyLists: Boolean = false,
addCategory: Boolean = false, addCategory: Boolean = false,
modifier: Modifier = Modifier,
) { ) {
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
var selectedCategories by remember { var selectedCategories by remember {
@@ -284,9 +289,12 @@ fun CategoryDropdown(
} }
var newCategoryName by remember { mutableStateOf("") } var newCategoryName by remember { mutableStateOf("") }
// For production use, this would come from ViewModel val activity = LocalContext.current.findActivity()
// For preview, we'll use empty list or pass via state val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
val categories by remember { mutableStateOf(emptyList<VocabularyCategory>()) } val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
// Find initial category // Find initial category
val initialCategory = remember(categories, initialCategoryId) { val initialCategory = remember(categories, initialCategoryId) {

View File

@@ -8,9 +8,6 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -18,7 +15,6 @@ import androidx.compose.ui.res.stringResource
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 eu.gaudian.translator.R import eu.gaudian.translator.R
import eu.gaudian.translator.model.TagCategory
import eu.gaudian.translator.model.VocabularyCategory import eu.gaudian.translator.model.VocabularyCategory
import eu.gaudian.translator.utils.findActivity import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.AppDialog import eu.gaudian.translator.view.composable.AppDialog
@@ -34,9 +30,6 @@ fun CategorySelectionDialog(
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity) val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
val categories by categoryViewModel.categories.collectAsState(initial = emptyList()) 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( AppDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
@@ -45,21 +38,8 @@ fun CategorySelectionDialog(
} }
) { ) {
// Dropdown button and menu // Dropdown button and menu
CategoryDropdownContent( CategoryDropdown(
state = CategoryDropdownState( onCategorySelected = onCategorySelected,
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 = ""
},
noneSelectable = false, noneSelectable = false,
multipleSelectable = true, multipleSelectable = true,
onlyLists = true, onlyLists = true,
@@ -79,10 +59,11 @@ fun CategorySelectionDialog(
DialogButton( DialogButton(
onClick = { onClick = {
onCategorySelected(selectedCategories) // The selected categories are handled by CategoryDropdown's internal state
// and passed to onCategorySelected callback
onDismissRequest() onDismissRequest()
}, },
enabled = selectedCategories.isNotEmpty() enabled = true // Always enabled since CategoryDropdown handles validation
) { ) {
Text(stringResource(R.string.label_confirm)) Text(stringResource(R.string.label_confirm))
} }

View File

@@ -55,9 +55,8 @@ fun StartExerciseDialog(
// Map displayed Language to its DB id (lid) using position mapping from load // Map displayed Language to its DB id (lid) using position mapping from load
var languageIdMap by remember { mutableStateOf<Map<Language, Int>>(emptyMap()) } var languageIdMap by remember { mutableStateOf<Map<Language, Int>>(emptyMap()) }
var selectedLanguages by remember { mutableStateOf<List<Language>>(emptyList()) } 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 selectedStages by remember { mutableStateOf<List<VocabularyStage>>(emptyList()) }
var expanded by remember { mutableStateOf(false) } var selectedCategories by remember { mutableStateOf<List<VocabularyCategory>>(emptyList()) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
coroutineScope.launch { coroutineScope.launch {
@@ -87,19 +86,10 @@ fun StartExerciseDialog(
}, },
languages languages
) )
CategoryDropdownContent( CategoryDropdown(
state = CategoryDropdownState(
expanded = expanded,
selectedCategories = selectedCategories,
newCategoryName = "",
categories = categories,
),
onExpand = { isExpanded -> expanded = isExpanded },
onCategorySelected = { cats -> onCategorySelected = { cats ->
selectedCategories = cats.filterIsInstance<VocabularyCategory>() selectedCategories = cats.filterIsInstance<VocabularyCategory>()
}, },
onNewCategoryNameChange = {},
onAddCategory = {},
multipleSelectable = true, multipleSelectable = true,
onlyLists = false, // Show both filters and lists onlyLists = false, // Show both filters and lists
addCategory = false, addCategory = false,

View File

@@ -27,7 +27,6 @@ import androidx.compose.ui.res.stringResource
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 eu.gaudian.translator.R import eu.gaudian.translator.R
import eu.gaudian.translator.model.TagCategory
import eu.gaudian.translator.model.VocabularyCategory import eu.gaudian.translator.model.VocabularyCategory
import eu.gaudian.translator.model.VocabularyItem import eu.gaudian.translator.model.VocabularyItem
import eu.gaudian.translator.utils.findActivity import eu.gaudian.translator.utils.findActivity
@@ -54,8 +53,6 @@ fun VocabularyReviewScreen(
val selectedItems = remember { mutableStateListOf<VocabularyItem>() } val selectedItems = remember { mutableStateListOf<VocabularyItem>() }
val duplicates = remember { mutableStateListOf<Boolean>() } val duplicates = remember { mutableStateListOf<Boolean>() }
var selectedCategories by remember { mutableStateOf<List<VocabularyCategory?>>(emptyList()) } var selectedCategories by remember { mutableStateOf<List<VocabularyCategory?>>(emptyList()) }
var newCategoryName by remember { mutableStateOf("") }
var expanded by remember { mutableStateOf(false) }
LaunchedEffect(generatedItems) { LaunchedEffect(generatedItems) {
val duplicateResults = vocabularyViewModel.findDuplicates(generatedItems) val duplicateResults = vocabularyViewModel.findDuplicates(generatedItems)
@@ -134,21 +131,8 @@ fun VocabularyReviewScreen(
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
modifier = Modifier.padding(8.dp) modifier = Modifier.padding(8.dp)
) )
CategoryDropdownContent( CategoryDropdown(
state = CategoryDropdownState(
expanded = expanded,
selectedCategories = selectedCategories,
newCategoryName = newCategoryName,
categories = categories,
),
onExpand = { isExpanded -> expanded = isExpanded },
onCategorySelected = { selectedCategories = it }, onCategorySelected = { selectedCategories = it },
onNewCategoryNameChange = { newCategoryName = it },
onAddCategory = { name ->
val newCategory = TagCategory(id = 0, name = name.trim())
categoryViewModel.createCategory(newCategory)
newCategoryName = ""
},
noneSelectable = false, noneSelectable = false,
multipleSelectable = true, multipleSelectable = true,
onlyLists = true, onlyLists = true,

View File

@@ -238,7 +238,7 @@ fun VocabularyCardHost(
listOf(currentVocabularyItem), listOf(currentVocabularyItem),
it.mapNotNull { category -> category?.id } it.mapNotNull { category -> category?.id }
) )
showCategoryDialog = false //showCategoryDialog = false
}, },
onDismissRequest = { showCategoryDialog = false } onDismissRequest = { showCategoryDialog = false }
) )

View File

@@ -103,7 +103,7 @@ private data class VocabularyFilterState(
val searchQuery: String = "", val searchQuery: String = "",
val selectedStage: VocabularyStage? = null, val selectedStage: VocabularyStage? = null,
val sortOrder: SortOrder = SortOrder.NEWEST_FIRST, val sortOrder: SortOrder = SortOrder.NEWEST_FIRST,
val categoryId: Int? = null, val categoryIds: List<Int> = emptyList(),
val dueTodayOnly: Boolean = false, val dueTodayOnly: Boolean = false,
val selectedLanguageIds: List<Int> = emptyList(), val selectedLanguageIds: List<Int> = emptyList(),
val selectedWordClass: String? = null val selectedWordClass: String? = null
@@ -133,7 +133,7 @@ fun VocabularyListScreen(
var filterState by rememberSaveable { var filterState by rememberSaveable {
mutableStateOf( mutableStateOf(
VocabularyFilterState( VocabularyFilterState(
categoryId = categoryId, categoryIds = categoryId?.let { listOf(it) } ?: emptyList(),
dueTodayOnly = showDueTodayOnly == true, dueTodayOnly = showDueTodayOnly == true,
selectedStage = stage selectedStage = stage
) )
@@ -142,7 +142,7 @@ fun VocabularyListScreen(
val isFilterActive by remember(filterState) { val isFilterActive by remember(filterState) {
derivedStateOf { derivedStateOf {
filterState.selectedStage != null || filterState.selectedStage != null ||
(filterState.categoryId != null && filterState.categoryId != 0) || filterState.categoryIds.isNotEmpty() ||
filterState.dueTodayOnly || filterState.dueTodayOnly ||
filterState.selectedLanguageIds.isNotEmpty() || filterState.selectedLanguageIds.isNotEmpty() ||
!filterState.selectedWordClass.isNullOrBlank() !filterState.selectedWordClass.isNullOrBlank()
@@ -165,7 +165,7 @@ fun VocabularyListScreen(
vocabularyViewModel.filterVocabularyItems( vocabularyViewModel.filterVocabularyItems(
languages = filterState.selectedLanguageIds, languages = filterState.selectedLanguageIds,
query = filterState.searchQuery.takeIf { it.isNotBlank() }, query = filterState.searchQuery.takeIf { it.isNotBlank() },
categoryId = filterState.categoryId, categoryIds = filterState.categoryIds,
stage = filterState.selectedStage, stage = filterState.selectedStage,
wordClass = filterState.selectedWordClass, wordClass = filterState.selectedWordClass,
dueTodayOnly = filterState.dueTodayOnly, dueTodayOnly = filterState.dueTodayOnly,
@@ -179,7 +179,7 @@ fun VocabularyListScreen(
LaunchedEffect(categoryId, showDueTodayOnly, stage) { LaunchedEffect(categoryId, showDueTodayOnly, stage) {
filterState = filterState.copy( filterState = filterState.copy(
categoryId = categoryId, categoryIds = categoryId?.let { listOf(it) } ?: emptyList(),
dueTodayOnly = showDueTodayOnly == true, dueTodayOnly = showDueTodayOnly == true,
selectedStage = stage selectedStage = stage
) )
@@ -382,7 +382,8 @@ fun VocabularyListScreen(
languageViewModel = languageViewModel, languageViewModel = languageViewModel,
languagesPresent = allLanguages.filter { it.nameResId in languagesPresent }, languagesPresent = allLanguages.filter { it.nameResId in languagesPresent },
hideCategory = categoryId != null && categoryId != 0, hideCategory = categoryId != null && categoryId != 0,
hideStage = stage != null hideStage = stage != null,
categoryViewModel = categoryViewModel
) )
} }
@@ -394,7 +395,7 @@ fun VocabularyListScreen(
selectedItems, selectedItems,
it.mapNotNull { category -> category?.id } it.mapNotNull { category -> category?.id }
) )
showCategoryDialog = false //showCategoryDialog = false
}, },
onDismissRequest = { showCategoryDialog = false } onDismissRequest = { showCategoryDialog = false }
) )
@@ -807,11 +808,12 @@ private fun FilterSortBottomSheet(
onDismiss: () -> Unit, onDismiss: () -> Unit,
onApplyFilters: (VocabularyFilterState) -> Unit, onApplyFilters: (VocabularyFilterState) -> Unit,
hideCategory: Boolean = false, hideCategory: Boolean = false,
hideStage: Boolean = false hideStage: Boolean = false,
categoryViewModel: CategoryViewModel
) { ) {
var selectedStage by rememberSaveable { mutableStateOf(currentFilterState.selectedStage) } var selectedStage by rememberSaveable { mutableStateOf(currentFilterState.selectedStage) }
var dueTodayOnly by rememberSaveable { mutableStateOf(currentFilterState.dueTodayOnly) } 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 selectedLanguageIds by rememberSaveable { mutableStateOf(currentFilterState.selectedLanguageIds) }
var selectedWordClass by rememberSaveable { mutableStateOf(currentFilterState.selectedWordClass) } var selectedWordClass by rememberSaveable { mutableStateOf(currentFilterState.selectedWordClass) }
@@ -840,7 +842,7 @@ private fun FilterSortBottomSheet(
TextButton(onClick = { TextButton(onClick = {
if (!hideStage) selectedStage = null if (!hideStage) selectedStage = null
dueTodayOnly = false dueTodayOnly = false
if (!hideCategory) selectedCategoryId = null if (!hideCategory) selectedCategoryIds = emptyList()
selectedLanguageIds = emptyList() selectedLanguageIds = emptyList()
selectedWordClass = null selectedWordClass = null
}) { }) {
@@ -853,7 +855,7 @@ private fun FilterSortBottomSheet(
currentFilterState.copy( currentFilterState.copy(
selectedStage = selectedStage, selectedStage = selectedStage,
dueTodayOnly = dueTodayOnly, dueTodayOnly = dueTodayOnly,
categoryId = selectedCategoryId, categoryIds = selectedCategoryIds,
selectedLanguageIds = selectedLanguageIds, selectedLanguageIds = selectedLanguageIds,
selectedWordClass = selectedWordClass selectedWordClass = selectedWordClass
) )
@@ -893,16 +895,18 @@ private fun FilterSortBottomSheet(
Text(stringResource(R.string.label_category), style = MaterialTheme.typography.titleMedium) Text(stringResource(R.string.label_category), style = MaterialTheme.typography.titleMedium)
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
CategoryDropdown( CategoryDropdown(
initialCategoryId = selectedCategoryId, initialCategoryId = selectedCategoryIds.firstOrNull(),
onCategorySelected = { categories -> onCategorySelected = { categories ->
selectedCategoryId = categories.firstOrNull()?.id selectedCategoryIds = categories.mapNotNull { it?.id }
} },
multipleSelectable = true,
noneSelectable = false
) )
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
} }
if (!hideStage) { 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( FlowRow(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
@@ -955,6 +959,7 @@ private fun FilterSortBottomSheet(
fun FilterSortBottomSheetPreview() { fun FilterSortBottomSheetPreview() {
val activity = LocalContext.current.findActivity() val activity = LocalContext.current.findActivity()
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity) val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
FilterSortBottomSheet( FilterSortBottomSheet(
currentFilterState = VocabularyFilterState(), currentFilterState = VocabularyFilterState(),
languageViewModel = languageViewModel, languageViewModel = languageViewModel,
@@ -962,6 +967,7 @@ fun FilterSortBottomSheetPreview() {
onDismiss = {}, onDismiss = {},
onApplyFilters = {}, onApplyFilters = {},
hideCategory = false, hideCategory = false,
hideStage = false hideStage = false,
categoryViewModel = categoryViewModel
) )
} }

View File

@@ -299,6 +299,7 @@ fun VocabularySortingItem(
val activity = LocalContext.current.findActivity() val activity = LocalContext.current.findActivity()
val languageConfigViewModel: LanguageConfigViewModel = hiltViewModel(viewModelStoreOwner = activity) val languageConfigViewModel: LanguageConfigViewModel = hiltViewModel(viewModelStoreOwner = activity)
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity) val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
var wordFirst by remember { mutableStateOf(item.wordFirst) } var wordFirst by remember { mutableStateOf(item.wordFirst) }
var wordSecond by remember { mutableStateOf(item.wordSecond) } var wordSecond by remember { mutableStateOf(item.wordSecond) }
var selectedCategories by remember { mutableStateOf<List<Int>>(emptyList()) } var selectedCategories by remember { mutableStateOf<List<Int>>(emptyList()) }
@@ -313,6 +314,7 @@ fun VocabularySortingItem(
var articlesLangSecond by remember { mutableStateOf(emptySet<String>()) } var articlesLangSecond by remember { mutableStateOf(emptySet<String>()) }
var showDuplicateDialog by remember { mutableStateOf(false) } 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 // NEW: Calculate if the item is valid for the "Done" button in faulty mode
val isItemNowValid by remember(wordFirst, wordSecond, langFirst, langSecond) { val isItemNowValid by remember(wordFirst, wordSecond, langFirst, langSecond) {

View File

@@ -360,7 +360,7 @@
<string name="days_2d">%1$d Tage</string> <string name="days_2d">%1$d Tage</string>
<string name="progress_by_category">Fortschritt nach Kategorie</string> <string name="progress_by_category">Fortschritt nach Kategorie</string>
<string name="label_apply_filters">Filter anwenden</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="label_category">Kategorie</string>
<string name="language">Sprache</string> <string name="language">Sprache</string>
<string name="label_clear_all">Alle löschen</string> <string name="label_clear_all">Alle löschen</string>

View File

@@ -358,7 +358,7 @@
<string name="days_2d">%1$d dias</string> <string name="days_2d">%1$d dias</string>
<string name="progress_by_category">Progresso por Categoria</string> <string name="progress_by_category">Progresso por Categoria</string>
<string name="label_apply_filters">Aplicar Filtros</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="label_category">Categoria</string>
<string name="language">Idioma</string> <string name="language">Idioma</string>
<string name="label_clear_all">Limpar Tudo</string> <string name="label_clear_all">Limpar Tudo</string>

View File

@@ -140,7 +140,7 @@
<string name="fetching_grammar_details">Fetching Grammar Details</string> <string name="fetching_grammar_details">Fetching Grammar Details</string>
<string name="filter_and_sort">Filter and Sort</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="filter_by_word_type">Filter by Word Type</string>
<string name="find_translations">Find Translations</string> <string name="find_translations">Find Translations</string>
@@ -1039,4 +1039,5 @@
<string name="duplicate">Duplicate</string> <string name="duplicate">Duplicate</string>
<string name="hint_scan_hint_title">Finding the right AI model</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="hint_translate_how_it_works">How translation works</string>
<string name="label_no_category">None</string>
</resources> </resources>