Refactor VocabularyListScreen to AllCardsListScreen, introduce NavigationRoutes for centralized route management, and externalize hardcoded strings.

This commit is contained in:
jonasgaudian
2026-02-17 16:26:30 +01:00
parent 02530dafbf
commit db959dab20
12 changed files with 188 additions and 353 deletions

View File

@@ -253,7 +253,7 @@ fun TranslatorApp(
val currentDestination = navBackStackEntry?.destination
val selectedScreen = Screen.fromDestination(currentDestination)
val isBottomBarHidden = currentDestination?.hierarchy?.any { destination ->
@Suppress("HardCodedStringLiteral") val isBottomBarHidden = currentDestination?.hierarchy?.any { destination ->
destination.route in setOf(
Screen.Translation.route,
Screen.Dictionary.route,
@@ -310,6 +310,7 @@ fun TranslatorApp(
}
},
onPlayClicked = {
@Suppress("HardCodedStringLiteral")
navController.navigate("start_exercise")
}
)

View File

@@ -37,6 +37,7 @@ import eu.gaudian.translator.view.settings.TranslationSettingsScreen
import eu.gaudian.translator.view.settings.settingsGraph
import eu.gaudian.translator.view.stats.StatsScreen
import eu.gaudian.translator.view.translation.TranslationScreen
import eu.gaudian.translator.view.vocabulary.AllCardsListScreen
import eu.gaudian.translator.view.vocabulary.CategoryDetailScreen
import eu.gaudian.translator.view.vocabulary.CategoryListScreen
import eu.gaudian.translator.view.vocabulary.LanguageProgressScreen
@@ -47,11 +48,26 @@ import eu.gaudian.translator.view.vocabulary.StageDetailScreen
import eu.gaudian.translator.view.vocabulary.VocabularyCardHost
import eu.gaudian.translator.view.vocabulary.VocabularyExerciseHostScreen
import eu.gaudian.translator.view.vocabulary.VocabularyHeatmapScreen
import eu.gaudian.translator.view.vocabulary.VocabularyListScreen
import eu.gaudian.translator.view.vocabulary.VocabularySortingScreen
private const val TRANSITION_DURATION = 300
object NavigationRoutes {
const val NEW_WORD = "new_word"
const val NEW_WORD_REVIEW = "new_word_review"
const val START_EXERCISE = "start_exercise"
const val CATEGORY_DETAIL = "category_detail"
const val CATEGORY_LIST = "category_list_screen"
const val VOCABULARY_DETAIL = "vocabulary_detail"
const val STATS_VOCABULARY_HEATMAP = "stats/vocabulary_heatmap"
const val STATS_LANGUAGE_PROGRESS = "stats/language_progress"
const val STATS_CATEGORY_DETAIL = "stats/category_detail"
const val STATS_CATEGORY_LIST = "stats/category_list_screen"
const val STATS_VOCABULARY_SORTING = "stats/vocabulary_sorting"
const val STATS_NO_GRAMMAR_ITEMS = "stats/no_grammar_items"
const val STATS_VOCABULARY_LIST = "stats/vocabulary_list"
}
@Composable
fun AppNavHost(
navController: NavHostController,
@@ -130,15 +146,15 @@ fun AppNavHost(
HomeScreen(navController = navController)
}
composable("new_word") {
composable(NavigationRoutes.NEW_WORD) {
NewWordScreen(navController = navController)
}
composable("new_word_review") {
composable(NavigationRoutes.NEW_WORD_REVIEW) {
NewWordReviewScreen(navController = navController)
}
composable("start_exercise") {
composable(NavigationRoutes.START_EXERCISE) {
StartExerciseScreen(navController = navController)
}
@@ -203,7 +219,7 @@ fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
composable("vocabulary_list/{showDueTodayOnly}/{categoryId}") { backStackEntry ->
val showDueTodayOnly = backStackEntry.arguments?.getString("showDueTodayOnly")?.toBoolean() ?: false
val categoryId = backStackEntry.arguments?.getString("categoryId")?.toIntOrNull()
VocabularyListScreen(
AllCardsListScreen(
navController = navController,
showDueTodayOnly = showDueTodayOnly,
categoryId = categoryId,
@@ -220,7 +236,7 @@ fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
)
}
composable("vocabulary_heatmap") {
composable(NavigationRoutes.STATS_VOCABULARY_HEATMAP) {
VocabularyHeatmapScreen(
navController = navController,
)
@@ -232,7 +248,7 @@ fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
if (it.equals("null", ignoreCase = true)) null else VocabularyStage.valueOf(it)
}
VocabularyListScreen(
AllCardsListScreen(
navController = navController,
showDueTodayOnly = showDueTodayOnly,
stage = stage,
@@ -373,7 +389,7 @@ fun NavGraphBuilder.statsGraph(
composable("stats/vocabulary_list/{showDueTodayOnly}/{categoryId}") { backStackEntry ->
val showDueTodayOnly = backStackEntry.arguments?.getString("showDueTodayOnly")?.toBoolean() ?: false
val categoryId = backStackEntry.arguments?.getString("categoryId")?.toIntOrNull()
VocabularyListScreen(
AllCardsListScreen(
navController = navController,
showDueTodayOnly = showDueTodayOnly,
categoryId = categoryId,
@@ -384,12 +400,12 @@ fun NavGraphBuilder.statsGraph(
enableNavigationButtons = true
)
}
composable("stats/language_progress") {
composable(NavigationRoutes.STATS_LANGUAGE_PROGRESS) {
LanguageProgressScreen(
navController = navController
)
}
composable("stats/vocabulary_heatmap") {
composable(NavigationRoutes.STATS_VOCABULARY_HEATMAP) {
VocabularyHeatmapScreen(
navController = navController,
)
@@ -401,7 +417,7 @@ fun NavGraphBuilder.statsGraph(
if (it.equals("null", ignoreCase = true)) null else VocabularyStage.valueOf(it)
}
VocabularyListScreen(
AllCardsListScreen(
navController = navController,
showDueTodayOnly = showDueTodayOnly,
stage = stage,

View File

@@ -43,9 +43,8 @@ import eu.gaudian.translator.view.hints.LocalShowHints
@Composable
fun AppTopAppBar(
title: String,
additionalContent: @Composable () -> Unit = {},
modifier: Modifier = Modifier,
title: String,
onNavigateBack: (() -> Unit)? = null,
navigationIcon: @Composable (() -> Unit)? = null,
actions: @Composable RowScope.() -> Unit = {},

View File

@@ -15,7 +15,7 @@ import androidx.navigation.NavHostController
import eu.gaudian.translator.R
import eu.gaudian.translator.view.composable.AppButton
import eu.gaudian.translator.view.composable.AppTopAppBar
import eu.gaudian.translator.view.vocabulary.VocabularyListScreen
import eu.gaudian.translator.view.vocabulary.AllCardsListScreen
@Composable
fun ExerciseVocabularyScreen(
@@ -41,7 +41,7 @@ fun ExerciseVocabularyScreen(
) { paddingValues ->
Box(modifier = Modifier.padding(paddingValues)) {
VocabularyListScreen(
AllCardsListScreen(
navController = navController as NavHostController?,
onNavigateToItem = { item ->
// Navigate to the detail screen for a specific vocabulary item

View File

@@ -1,5 +1,6 @@
package eu.gaudian.translator.view.home
import android.content.Context
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -37,12 +38,14 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
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.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavHostController
import eu.gaudian.translator.R
import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.NavigationRoutes
import eu.gaudian.translator.view.composable.AppCard
import eu.gaudian.translator.view.composable.Screen
import eu.gaudian.translator.view.settings.SettingsRoutes
@@ -78,33 +81,36 @@ fun HomeScreen(
verticalArrangement = Arrangement.spacedBy(15.dp)
) {
item { Spacer(modifier = Modifier.height(16.dp)) }
item { TopProfileSection(navController = navController) }
item { TopProfileSection(
navController = navController,
context = LocalContext.current
) }
item {
StreakAndGoalSection(
streak = streak,
progress = progress,
progressTitle = "$todayCompletedCount / $dailyGoal",
onGoalClick = { navController.navigate(SettingsRoutes.VOCABULARY_OPTIONS) },
onStreakClick = { navController.navigate("stats/vocabulary_heatmap") }
onStreakClick = { navController.navigate(NavigationRoutes.STATS_VOCABULARY_HEATMAP) }
)
}
item {
//TODO replace with actual implementation
@Suppress("HardCodedStringLiteral")
ActionCard(
title = "Daily Review",
subtitle = "42 words need attention",
icon = Icons.Default.Psychology,
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary
)
}
item {
ActionCard(
title = "New Words",
subtitle = "Expand your vocabulary",
title = stringResource(R.string.label_new_words),
subtitle = stringResource(R.string.desc_expand_your_vocabulary),
icon = Icons.Default.AddCircle,
containerColor = MaterialTheme.colorScheme.surfaceVariant,
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
onClick = { navController.navigate("new_word") }
onClick = { navController.navigate(NavigationRoutes.NEW_WORD) }
)
}
item { WeeklyProgressSection(navController = navController) }
@@ -117,8 +123,8 @@ fun HomeScreen(
}
@Composable
fun TopProfileSection(navController: NavHostController) {
val context = LocalContext.current
fun TopProfileSection(navController: NavHostController, context: Context) {
val motivationalPhrases = remember {
context.resources.getStringArray(R.array.motivational_phrases)
}
@@ -126,7 +132,9 @@ fun TopProfileSection(navController: NavHostController) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().padding(8.dp)
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
Column(modifier = Modifier.weight(1f)) {
@@ -146,7 +154,7 @@ fun TopProfileSection(navController: NavHostController) {
) {
Icon(
imageVector = Icons.Default.Settings,
contentDescription = "Settings",
contentDescription = stringResource(R.string.label_settings),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
@@ -169,8 +177,8 @@ fun StreakAndGoalSection(
StatCard(
modifier = Modifier.weight(1f),
icon = Icons.Default.LocalFireDepartment,
title = "$streak Days",
subtitle = "CURRENT STREAK",
title = stringResource(R.string.label_2d_days, streak),
subtitle = stringResource(R.string.label_current_streak).uppercase(),
onClick = onStreakClick
)
// Goal Card
@@ -178,7 +186,7 @@ fun StreakAndGoalSection(
modifier = Modifier.weight(1f),
progress = progress,
title = progressTitle,
subtitle = "DAILY GOAL",
subtitle = stringResource(R.string.label_daily_goal).uppercase(),
onClick = onGoalClick
)
}
@@ -293,7 +301,6 @@ fun ActionCard(
title: String,
subtitle: String,
icon: ImageVector,
containerColor: Color,
contentColor: Color,
onClick: (() -> Unit)? = null
) {
@@ -314,7 +321,7 @@ fun ActionCard(
}
Icon(
imageVector = Icons.Default.ChevronRight,
contentDescription = "Go",
contentDescription = stringResource(R.string.cd_go),
modifier = Modifier.size(24.dp)
)
}
@@ -351,9 +358,9 @@ fun WeeklyProgressSection(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(text = "Weekly Progress", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
TextButton(onClick = { navController.navigate("stats/vocabulary_heatmap") }) {
Text("See History")
Text(text = stringResource(R.string.label_weekly_progress), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
TextButton(onClick = { navController.navigate(NavigationRoutes.STATS_VOCABULARY_HEATMAP) }) {
Text(stringResource(R.string.label_see_history))
}
}
@@ -361,7 +368,7 @@ fun WeeklyProgressSection(
AppCard(
modifier = Modifier.fillMaxWidth(),
onClick = { navController.navigate("stats/vocabulary_heatmap") }
onClick = { navController.navigate(NavigationRoutes.STATS_VOCABULARY_HEATMAP) }
) {
if (weeklyActivityStats.isEmpty()) {
Column(
@@ -372,7 +379,7 @@ fun WeeklyProgressSection(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "No activity data available",
text = stringResource(R.string.text_desc_no_activity_data_available),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
@@ -403,7 +410,7 @@ fun BottomStatsSection(
onClick = { navController.navigate(Screen.Library.route) }
) {
Column(modifier = Modifier.padding(20.dp)) {
Text(text = "TOTAL WORDS", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f))
Text(text = stringResource(R.string.label_total_words).uppercase(), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f))
Spacer(modifier = Modifier.height(8.dp))
Text(text = totalWords.toString(), style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(8.dp))
@@ -413,7 +420,7 @@ fun BottomStatsSection(
// Learned
AppCard(
modifier = Modifier.weight(1f),
onClick = { navController.navigate("stats/language_progress") }
onClick = { navController.navigate(NavigationRoutes.STATS_LANGUAGE_PROGRESS) }
) {
Column(modifier = Modifier.padding(20.dp)) {
Text(text = "LEARNED", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f))

View File

@@ -1,4 +1,4 @@
@file:Suppress("HardCodedStringLiteral")
@file:Suppress("AssignedValueIsNeverRead")
package eu.gaudian.translator.view.library
@@ -44,6 +44,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@@ -61,8 +62,10 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController
import eu.gaudian.translator.R
import eu.gaudian.translator.R.string.text_add_new_word_to_list
import eu.gaudian.translator.model.VocabularyStage
import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.NavigationRoutes
import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppSwitch
import eu.gaudian.translator.view.composable.BottomSheetMenuItem
@@ -70,7 +73,6 @@ import eu.gaudian.translator.view.composable.MultipleLanguageDropdown
import eu.gaudian.translator.view.dialogs.AddCategoryDialog
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
@@ -99,8 +101,6 @@ fun LibraryScreen(
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) }
@@ -135,8 +135,8 @@ fun LibraryScreen(
val vocabularyItems by vocabularyItemsFlow.collectAsStateWithLifecycle(initialValue = emptyList())
var isHeaderVisible by remember { mutableStateOf(true) }
var previousIndex by remember { mutableStateOf(0) }
var previousScrollOffset by remember { mutableStateOf(0) }
var previousIndex by remember { mutableIntStateOf(0) }
var previousScrollOffset by remember { mutableIntStateOf(0) }
// Set navigation context when vocabulary items are loaded
LaunchedEffect(vocabularyItems) {
@@ -229,10 +229,11 @@ fun LibraryScreen(
CategoriesView(
categories = categories,
onCategoryClick = { category ->
navController.navigate("category_detail/${category.id}")
@Suppress("HardCodedStringLiteral")
navController.navigate("${NavigationRoutes.CATEGORY_DETAIL}/${category.id}")
},
onExploreMoreClick = {
navController.navigate("category_list_screen")
navController.navigate(NavigationRoutes.CATEGORY_LIST)
}
)
},
@@ -251,7 +252,8 @@ fun LibraryScreen(
}
} else {
vocabularyViewModel.setNavigationContext(vocabularyItems, item.id)
navController.navigate("vocabulary_detail/${item.id}")
@Suppress("HardCodedStringLiteral")
navController.navigate("${NavigationRoutes.VOCABULARY_DETAIL}/${item.id}")
}
},
onItemLongClick = { item ->
@@ -287,7 +289,7 @@ fun LibraryScreen(
modifier = Modifier.size(50.dp),
containerColor = MaterialTheme.colorScheme.surfaceVariant
) {
Icon(AppIcons.ArrowCircleUp, contentDescription = "Scroll to top")
Icon(AppIcons.ArrowCircleUp, contentDescription = stringResource(R.string.cd_scroll_to_top))
}
}
}
@@ -344,17 +346,17 @@ fun LibraryScreen(
BottomSheetMenuItem(
icon = Icons.Rounded.Add, // Or any custom vector icon you prefer
title = stringResource(R.string.label_add_vocabulary),
subtitle = "Füge ein neues Wort zu deiner Liste hinzu", // Suggest adding this to strings.xml
subtitle = stringResource(text_add_new_word_to_list), // Suggest adding this to strings.xml
onClick = {
showAddMenu = false
navController.navigate("new_word")
navController.navigate(NavigationRoutes.NEW_WORD)
}
)
BottomSheetMenuItem(
icon = Icons.Rounded.Folder,
title = stringResource(R.string.label_add_category),
subtitle = "Organisiere deine Vokabeln in Gruppen", // Suggest adding this to strings.xml
subtitle = stringResource(R.string.text_desc_organize_vocabulary_groups), // Suggest adding this to strings.xml
onClick = {
showAddMenu = false
showAddCategoryDialog = true
@@ -418,7 +420,7 @@ fun FilterBottomSheetContent(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Filter Cards",
text = stringResource(R.string.label_filter_cards),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
@@ -430,7 +432,7 @@ fun FilterBottomSheetContent(
sortOrder = SortOrder.NEWEST_FIRST
onResetClick()
}) {
Text("Reset")
Text(stringResource(R.string.label_reset))
}
}
@@ -446,7 +448,7 @@ fun FilterBottomSheetContent(
// Sort Order
Column {
Text(
text = "SORT BY",
text = stringResource(R.string.label_sort_by).uppercase(),
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold,
letterSpacing = 1.sp,
@@ -589,7 +591,7 @@ fun FilterBottomSheetContent(
shape = RoundedCornerShape(28.dp)
) {
Text(
text = "Apply Filters",
text = stringResource(R.string.label_apply_filters),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)

View File

@@ -1,4 +1,4 @@
@file:Suppress("HardCodedStringLiteral", "AssignedValueIsNeverRead", "UnusedMaterial3ScaffoldPaddingParameter")
@file:Suppress("AssignedValueIsNeverRead")
package eu.gaudian.translator.view.stats
@@ -56,8 +56,8 @@ import androidx.navigation.compose.rememberNavController
import eu.gaudian.translator.R
import eu.gaudian.translator.model.VocabularyStage
import eu.gaudian.translator.model.WidgetType
import eu.gaudian.translator.utils.Log
import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.NavigationRoutes
import eu.gaudian.translator.view.composable.AppCard
import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppOutlinedCard
@@ -82,14 +82,11 @@ import kotlinx.coroutines.launch
@SuppressLint("FrequentlyChangingValue")
@Composable
fun StatsScreen(
modifier: Modifier = Modifier,
navController: NavHostController,
onShowCustomExerciseDialog: () -> Unit = {},
startDailyExercise: (Boolean) -> Unit = {},
onNavigateToCategoryDetail: ((Int) -> Unit)? = null,
onNavigateToCategoryList: (() -> Unit)? = null,
onShowWordPairExerciseDialog: () -> Unit = {},
onScroll: (Boolean) -> Unit = {},
modifier: Modifier = Modifier
) {
val activity = LocalContext.current.findActivity()
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
@@ -128,10 +125,11 @@ fun StatsScreen(
}
val handleNavigateToCategoryDetail = onNavigateToCategoryDetail ?: { categoryId ->
navController.navigate("stats/category_detail/$categoryId")
@Suppress("HardCodedStringLiteral")
navController.navigate("${NavigationRoutes.STATS_CATEGORY_DETAIL}/$categoryId")
}
val handleNavigateToCategoryList = onNavigateToCategoryList ?: {
navController.navigate("stats/category_list_screen")
navController.navigate(NavigationRoutes.STATS_CATEGORY_LIST)
}
AppOutlinedCard(modifier = modifier) {
@@ -270,11 +268,8 @@ fun StatsScreen(
navController = navController,
vocabularyViewModel = vocabularyViewModel,
progressViewModel = progressViewModel,
onShowCustomExerciseDialog = onShowCustomExerciseDialog,
startDailyExercise = startDailyExercise,
onNavigateToCategoryDetail = handleNavigateToCategoryDetail,
onNavigateToCategoryList = handleNavigateToCategoryList,
onShowWordPairExerciseDialog = onShowWordPairExerciseDialog,
onMissingLanguage = { missingId ->
selectedMissingLanguageId = missingId
showMissingLanguageDialog = true
@@ -524,17 +519,15 @@ class DragDropState(
// Remainder of your existing components
// --------------------------------------------------------------------------------
@Suppress("HardCodedStringLiteral")
@Composable
private fun LazyWidget(
widgetType: WidgetType,
navController: NavController,
vocabularyViewModel: VocabularyViewModel,
progressViewModel: ProgressViewModel,
onShowCustomExerciseDialog: () -> Unit,
startDailyExercise: (Boolean) -> Unit,
onNavigateToCategoryDetail: (Int) -> Unit,
onNavigateToCategoryList: () -> Unit,
onShowWordPairExerciseDialog: () -> Unit,
onMissingLanguage: (Int) -> Unit
) {
when (widgetType) {
@@ -542,10 +535,10 @@ private fun LazyWidget(
WidgetType.Status -> LazyStatusWidget(
vocabularyViewModel = vocabularyViewModel,
onNavigateToNew = { navController.navigate("stats/vocabulary_sorting?mode=NEW") },
onNavigateToDuplicates = { navController.navigate("stats/vocabulary_sorting?mode=DUPLICATES") },
onNavigateToFaulty = { navController.navigate("stats/vocabulary_sorting?mode=FAULTY") },
onNavigateToNoGrammar = { navController.navigate("stats/no_grammar_items") },
onNavigateToNew = { navController.navigate("${NavigationRoutes.STATS_VOCABULARY_SORTING}?mode=NEW") },
onNavigateToDuplicates = { navController.navigate("${NavigationRoutes.STATS_VOCABULARY_SORTING}?mode=DUPLICATES") },
onNavigateToFaulty = { navController.navigate("${NavigationRoutes.STATS_VOCABULARY_SORTING}?mode=FAULTY") },
onNavigateToNoGrammar = { navController.navigate(NavigationRoutes.STATS_NO_GRAMMAR_ITEMS) },
onNavigateToMissingLanguage = onMissingLanguage
)
@@ -557,7 +550,7 @@ private fun LazyWidget(
lastSevenDays = progressViewModel.lastSevenDays.collectAsState().value,
dueTodayCount = progressViewModel.dueTodayCount.collectAsState().value,
wordsCompleted = progressViewModel.totalWordsCompleted.collectAsState().value,
onStatisticsClicked = { navController.navigate("stats/vocabulary_heatmap") }
onStatisticsClicked = { navController.navigate(NavigationRoutes.STATS_VOCABULARY_HEATMAP) }
)
WidgetType.WeeklyActivityChart -> WeeklyActivityChartWidget(
@@ -566,13 +559,19 @@ private fun LazyWidget(
WidgetType.AllVocabulary -> AllVocabularyWidget(
vocabularyViewModel = vocabularyViewModel,
onOpenAllVocabulary = { navController.navigate("stats/vocabulary_list/false/null") },
onStageClicked = { stage -> navController.navigate("stats/vocabulary_list/false/$stage") }
onOpenAllVocabulary = { navController.navigate("${NavigationRoutes.STATS_VOCABULARY_LIST}/false/null") },
onStageClicked = { stage ->
@Suppress("HardCodedStringLiteral")
navController.navigate("${NavigationRoutes.STATS_VOCABULARY_LIST}/false/$stage")
}
)
WidgetType.DueToday -> DueTodayWidget(
vocabularyViewModel = vocabularyViewModel,
onStageClicked = { stage -> navController.navigate("stats/vocabulary_list/false/$stage") }
onStageClicked = { stage ->
@Suppress("HardCodedStringLiteral")
navController.navigate("${NavigationRoutes.STATS_VOCABULARY_LIST}/false/$stage")
}
)
WidgetType.CategoryProgress -> CategoryProgressWidget(
@@ -586,7 +585,7 @@ private fun LazyWidget(
totalWords = vocabularyViewModel.vocabularyItems.collectAsState().value.size,
learnedWords = vocabularyViewModel.stageStats.collectAsState().value
.firstOrNull { it.stage == VocabularyStage.LEARNED }?.itemCount ?: 0,
onNavigateToProgress = { navController.navigate("stats/language_progress") }
onNavigateToProgress = { navController.navigate(NavigationRoutes.STATS_LANGUAGE_PROGRESS) }
)
}
@@ -649,11 +648,8 @@ fun StatsScreenPreview() {
val navController = rememberNavController()
StatsScreen(
navController = navController,
onShowCustomExerciseDialog = {},
onNavigateToCategoryDetail = {},
startDailyExercise = {},
onNavigateToCategoryList = {},
onShowWordPairExerciseDialog = {},
)
}
@@ -675,6 +671,7 @@ fun WidgetContainerPreview() {
.padding(16.dp),
contentAlignment = Alignment.Center
) {
@Suppress("HardCodedStringLiteral")
Text("Preview Content")
}
}

View File

@@ -179,7 +179,7 @@ fun CategoryDetailScreen(
}
) { paddingValues ->
Column(modifier = Modifier.padding(paddingValues)) {
VocabularyListScreen(
AllCardsListScreen(
categoryId = categoryId,
showDueTodayOnly = false,
onNavigateToItem = onNavigateToItem,
@@ -324,8 +324,8 @@ fun CategoryHeaderCardWithProgressPreview() {
MaterialTheme {
CategoryHeaderCard(
subtitle = "Travel Vocabulary",
categoryProgress = eu.gaudian.translator.viewmodel.CategoryProgress(
vocabularyCategory = eu.gaudian.translator.model.TagCategory(
categoryProgress = CategoryProgress(
vocabularyCategory = TagCategory(
1,
"Travel"
),

View File

@@ -80,8 +80,8 @@ fun NoGrammarItemsScreen(
}
}
} else {
// Use the generic VocabularyListScreen to display the items
VocabularyListScreen(
// Use the generic AllCardsListScreen to display the items
AllCardsListScreen(
itemsToShow = itemsWithoutGrammar,
onNavigateToItem = { item ->
@Suppress("HardCodedStringLiteral")

View File

@@ -49,7 +49,7 @@ fun StageDetailScreen(
onStageTapped = {},
)
VocabularyListScreen(
AllCardsListScreen(
categoryId = null,
showDueTodayOnly = true,
stage = stage,

View File

@@ -5,19 +5,13 @@ package eu.gaudian.translator.view.vocabulary
import android.os.Parcelable
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
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.FlowRow
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
@@ -25,14 +19,10 @@ import androidx.compose.foundation.layout.navigationBarsPadding
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.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.FabPosition
@@ -43,7 +33,6 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults
@@ -59,20 +48,15 @@ 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.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import eu.gaudian.translator.R
import eu.gaudian.translator.model.Language
import eu.gaudian.translator.model.VocabularyItem
@@ -84,10 +68,10 @@ import eu.gaudian.translator.view.composable.AppScaffold
import eu.gaudian.translator.view.composable.AppSwitch
import eu.gaudian.translator.view.composable.AppTopAppBar
import eu.gaudian.translator.view.composable.MultipleLanguageDropdown
import eu.gaudian.translator.view.composable.insertBreakOpportunities
import eu.gaudian.translator.view.dialogs.CategoryDropdown
import eu.gaudian.translator.view.dialogs.CategorySelectionDialog
import eu.gaudian.translator.view.dialogs.StageSelectionDialog
import eu.gaudian.translator.view.library.AllCardsView
import eu.gaudian.translator.viewmodel.CategoryViewModel
import eu.gaudian.translator.viewmodel.LanguageConfigViewModel
import eu.gaudian.translator.viewmodel.LanguageViewModel
@@ -110,7 +94,7 @@ private data class VocabularyFilterState(
) : Parcelable
@Composable
fun VocabularyListScreen(
fun AllCardsListScreen(
categoryId: Int? = null,
showDueTodayOnly: Boolean? = null,
stage: VocabularyStage? = null,
@@ -245,11 +229,7 @@ fun VocabularyListScreen(
)
"search" -> SearchTopAppBar(
searchQuery = filterState.searchQuery,
onQueryChanged = { newQuery ->
filterState = filterState.copy(searchQuery = newQuery)
},
onCloseSearch = {
onCloseSearch = {
isSearchActive = false
filterState = filterState.copy(searchQuery = "")
}
@@ -295,78 +275,40 @@ fun VocabularyListScreen(
floatingActionButtonPosition = FabPosition.Center
) { paddingValues ->
Column(modifier = Modifier.padding(paddingValues).padding(bottom = 0.dp)) {
if (vocabularyItems.isEmpty()) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
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))
Box(modifier = Modifier
AllCardsView(
vocabularyItems = vocabularyItems,
allLanguages = allLanguages,
selection = selection,
listState = lazyListState,
modifier = Modifier
.fillMaxSize()
.padding(8.dp), contentAlignment = Alignment.Center) {
Text(
text = stringResource(R.string.no_vocabulary_items_found_perhaps_try_changing_the_filters),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = androidx.compose.ui.text.style.TextAlign.Center
)
}
}
} else {
LazyColumn(
state = lazyListState,
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(vertical = 0.dp)
) {
items(
items = vocabularyItems,
key = { it.id }
) { item ->
val isSelected = selection.contains(item.id.toLong())
VocabularyListItem(
item = item,
allLanguages = allLanguages,
isSelected = isSelected,
onItemClick = {
if (isInSelectionMode) {
selection = if (isSelected) {
selection - item.id.toLong()
} else {
selection + item.id.toLong()
}
} else {
if (navController != null && enableNavigationButtons) {
vocabularyViewModel.setNavigationContext(vocabularyItems, item.id)
navController.navigate("vocabulary_detail/${item.id}")
} else {
onNavigateToItem(item)
}
}
},
onItemLongClick = {
if (!isInSelectionMode) {
selection = setOf(item.id.toLong())
}
},
onDeleteClick = {
vocabularyViewModel.deleteData(VocabularyViewModel.DeleteType.VOCABULARY_ITEM, item = item)
},
modifier = Modifier.animateItem()
)
.padding(horizontal = 8.dp),
onItemClick = { item ->
val isSelected = selection.contains(item.id.toLong())
if (isInSelectionMode) {
selection = if (isSelected) {
selection - item.id.toLong()
} else {
selection + item.id.toLong()
}
} else {
if (navController != null && enableNavigationButtons) {
vocabularyViewModel.setNavigationContext(vocabularyItems, item.id)
navController.navigate("vocabulary_detail/${item.id}")
} else {
onNavigateToItem(item)
}
}
},
onItemLongClick = { item ->
if (!isInSelectionMode) {
selection = setOf(item.id.toLong())
}
},
onDeleteClick = { item ->
vocabularyViewModel.deleteData(VocabularyViewModel.DeleteType.VOCABULARY_ITEM, item = item)
}
}
)
}
@@ -382,8 +324,7 @@ fun VocabularyListScreen(
languageViewModel = languageViewModel,
languagesPresent = allLanguages.filter { it.nameResId in languagesPresent },
hideCategory = categoryId != null && categoryId != 0,
hideStage = stage != null,
categoryViewModel = categoryViewModel
hideStage = stage != null
)
}
@@ -417,21 +358,34 @@ fun VocabularyListScreen(
}
}
@ThemePreviews
@Deprecated("Use AllCardsListScreen which renders AllCardsView")
@Composable
fun VocabularyListScreenPreview() {
val navController = rememberNavController()
VocabularyListScreen(
categoryId = 1,
showDueTodayOnly = false,
stage = VocabularyStage.NEW,
onNavigateToItem = {},
onNavigateBack = {},
navController = navController
fun VocabularyListScreen(
categoryId: Int? = null,
showDueTodayOnly: Boolean? = null,
stage: VocabularyStage? = null,
onNavigateToItem: (VocabularyItem) -> Unit?,
onNavigateBack: (() -> Unit)? = null,
navController: NavHostController? = null,
itemsToShow: List<VocabularyItem> = emptyList(),
isRemoveFromCategoryEnabled: Boolean = false,
showTopBar: Boolean = true,
enableNavigationButtons: Boolean = false
) {
AllCardsListScreen(
categoryId = categoryId,
showDueTodayOnly = showDueTodayOnly,
stage = stage,
onNavigateToItem = onNavigateToItem,
onNavigateBack = onNavigateBack,
navController = navController,
itemsToShow = itemsToShow,
isRemoveFromCategoryEnabled = isRemoveFromCategoryEnabled,
showTopBar = showTopBar,
enableNavigationButtons = enableNavigationButtons
)
}
@Composable
private fun DefaultTopAppBar(
title: String,
@@ -505,8 +459,6 @@ private fun DefaultTopAppBar(
@Composable
private fun SearchTopAppBar(
searchQuery: String,
onQueryChanged: (String) -> Unit,
onCloseSearch: () -> Unit
) {
val focusRequester = remember { FocusRequester() }
@@ -518,37 +470,6 @@ private fun SearchTopAppBar(
AppTopAppBar(
modifier = Modifier.height(56.dp),
title = "TODO",
additionalContent = {
Box(
modifier = Modifier.fillMaxHeight(),
contentAlignment = Alignment.Center
) {
BasicTextField(
value = searchQuery,
onValueChange = onQueryChanged,
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
textStyle = MaterialTheme.typography.bodyLarge.copy(
color = MaterialTheme.colorScheme.onSurface
),
singleLine = true,
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
decorationBox = { innerTextField ->
Box(contentAlignment = Alignment.CenterStart) {
if (searchQuery.isEmpty()) {
Text(
text = stringResource(R.string.search_vocabulary),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
innerTextField()
}
}
)
}
},
navigationIcon = {
IconButton(onClick = onCloseSearch) {
Icon(
@@ -566,8 +487,6 @@ private fun SearchTopAppBar(
@Composable
fun SearchTopAppBarPreview() {
SearchTopAppBar(
searchQuery = stringResource(R.string.search_query),
onQueryChanged = {},
onCloseSearch = {}
)
}
@@ -670,112 +589,6 @@ fun ContextualTopAppBarPreview() {
)
}
@Composable
private fun VocabularyListItem(
item: VocabularyItem,
allLanguages: List<Language>,
isSelected: Boolean,
onItemClick: () -> Unit,
onItemLongClick: () -> Unit,
onDeleteClick: () -> Unit,
modifier: Modifier = Modifier
) {
val languageMap = remember(allLanguages) { allLanguages.associateBy { it.nameResId } }
val langFirst = item.languageFirstId?.let { languageMap[it]?.name } ?: ""
val langSecond = item.languageSecondId?.let { languageMap[it]?.name } ?: ""
OutlinedCard(
modifier = modifier
.fillMaxWidth()
.clip(CardDefaults.shape)
.combinedClickable(
onClick = onItemClick,
onLongClick = onItemLongClick
)
.animateContentSize(),
elevation = CardDefaults.cardElevation(defaultElevation = if (isSelected) 4.dp else 0.dp),
colors = CardDefaults.cardColors(
containerColor = if (isSelected) MaterialTheme.colorScheme.primaryContainer
else MaterialTheme.colorScheme.surface
),
border = BorderStroke(
width = if (isSelected) 1.5.dp else 1.dp,
color = if (isSelected) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.outline.copy(alpha = 0.5f)
)
) {
Row(
modifier = Modifier.padding(horizontal = 4.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
LanguageRow(word = item.wordFirst, language = langFirst)
HorizontalDivider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f))
LanguageRow(word = item.wordSecond, language = langSecond)
}
Box(
modifier = Modifier.padding(4.dp),
contentAlignment = Alignment.Center
) {
Crossfade(targetState = isSelected, label = "action-icon-fade") { selected ->
if (selected) {
Icon(
imageVector = AppIcons.Check,
contentDescription = "Selected",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(24.dp)
)
} else {
IconButton(onClick = onDeleteClick) {
Icon(
imageVector = AppIcons.Delete,
contentDescription = stringResource(id = R.string.label_delete),
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
)
}
}
}
}
}
}
}
@Composable
private fun LanguageRow(word: String, language: String) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 2.dp)) {
Text(
text = insertBreakOpportunities(word),
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.weight(0.7f)
)
Spacer(Modifier.width(8.dp))
Text(
text = language,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
modifier = Modifier.weight(0.3f)
)
}
}
@ThemePreviews
@Composable
fun LanguageRowPreview() {
LanguageRow(
word = "Hello",
language = "English"
)
}
@Composable
private fun FilterSortBottomSheet(
@@ -785,8 +598,7 @@ private fun FilterSortBottomSheet(
onDismiss: () -> Unit,
onApplyFilters: (VocabularyFilterState) -> Unit,
hideCategory: Boolean = false,
hideStage: Boolean = false,
categoryViewModel: CategoryViewModel
hideStage: Boolean = false
) {
var selectedStage by rememberSaveable { mutableStateOf(currentFilterState.selectedStage) }
var dueTodayOnly by rememberSaveable { mutableStateOf(currentFilterState.dueTodayOnly) }
@@ -931,20 +743,3 @@ private fun FilterSortBottomSheet(
}
}
@ThemePreviews
@Composable
fun FilterSortBottomSheetPreview() {
val activity = LocalContext.current.findActivity()
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
FilterSortBottomSheet(
currentFilterState = VocabularyFilterState(),
languageViewModel = languageViewModel,
languagesPresent = emptyList(),
onDismiss = {},
onApplyFilters = {},
hideCategory = false,
hideStage = false,
categoryViewModel = categoryViewModel
)
}

View File

@@ -1120,4 +1120,22 @@
<string name="label_stats">Stats</string>
<string name="label_library">Library</string>
<string name="label_edit">Edit</string>
<string name="label_total_wordss">Total Words</string>
<string name="label_new_words">New Words</string>
<string name="desc_expand_your_vocabulary">Expand your vocabulary</string>
<string name="label_settings">Settings</string>
<string name="label_2d_days">%1$d Days</string>
<string name="label_current_streak">Current Streak</string>
<string name="label_daily_goal">Daily Goal</string>
<string name="text_desc_no_activity_data_available">No activity data available</string>
<string name="label_see_history">See History</string>
<string name="label_weekly_progress">Weekly Progress</string>
<string name="cd_go">Go</string>
<string name="label_aapply_filters">Apply Filters</string>
<string name="label_sort_by">Sort By</string>
<string name="label_reset">Reset</string>
<string name="label_filter_cards">Filter Cards</string>
<string name="text_desc_organize_vocabulary_groups">Organize Your Vocabulary in Groups</string>
<string name="text_add_new_word_to_list">Extract a New Word to Your List</string>
<string name="cd_scroll_to_top">Scroll to top</string>
</resources>