Refactor the dictionary and corrector navigation by promoting the Corrector to a top-level destination and removing the tabbed MainDictionaryScreen.

This commit is contained in:
jonasgaudian
2026-02-20 00:03:19 +01:00
parent c94b29073f
commit cfd71162a0
7 changed files with 114 additions and 318 deletions

View File

@@ -82,19 +82,14 @@ val LocalConnectionConfigured = compositionLocalOf { true }
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private val settingsViewModel: SettingsViewModel by viewModels() private val settingsViewModel: SettingsViewModel by viewModels()
private var isReady = false private var isReady = false
private var isUiLoaded = false private var isUiLoaded = false
private var isInitializing = true
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen().apply { installSplashScreen().apply {
// The splash screen will now correctly wait until isReady is true
setKeepOnScreenCondition { !isReady } setKeepOnScreenCondition { !isReady }
} }
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
lifecycleScope.launch { lifecycleScope.launch {
@@ -104,28 +99,22 @@ class MainActivity : ComponentActivity() {
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
// Show UI immediately and load data in background
setContent { setContent {
AppTheme(settingsViewModel = settingsViewModel) { AppTheme(settingsViewModel = settingsViewModel) {
TranslatorApp(settingsViewModel = settingsViewModel) TranslatorApp(settingsViewModel = settingsViewModel)
} }
} }
// Mark UI as loaded immediately after setContent
isUiLoaded = true isUiLoaded = true
// Start initialization in background without blocking UI
initializeData() initializeData()
} }
private fun initializeData() { private fun initializeData() {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
// Get repositories from the Application instance (lazy initialization)
val myApp = application as MyApplication val myApp = application as MyApplication
val languageRepository = myApp.languageRepository val languageRepository = myApp.languageRepository
val apiRepository = myApp.apiRepository val apiRepository = myApp.apiRepository
// Perform initialization in parallel where possible
val languageJob = launch { val languageJob = launch {
languageRepository.initializeDefaultLanguages() languageRepository.initializeDefaultLanguages()
languageRepository.initializeAllLanguages() languageRepository.initializeAllLanguages()
@@ -135,13 +124,10 @@ class MainActivity : ComponentActivity() {
apiRepository.initialInit() apiRepository.initialInit()
} }
// Wait for both to complete
languageJob.join() languageJob.join()
apiJob.join() apiJob.join()
// Signal readiness after all work is done.
isReady = true isReady = true
isInitializing = false
} }
} }
} }
@@ -149,10 +135,7 @@ class MainActivity : ComponentActivity() {
@Suppress("AssignedValueIsNeverRead") @Suppress("AssignedValueIsNeverRead")
@SuppressLint("LocalContextResourcesRead") @SuppressLint("LocalContextResourcesRead")
@Composable @Composable
fun TranslatorApp( fun TranslatorApp(settingsViewModel: SettingsViewModel) {
settingsViewModel: SettingsViewModel
) {
val activity = LocalContext.current.findActivity() val activity = LocalContext.current.findActivity()
val statusViewModel: StatusViewModel = hiltViewModel(activity) val statusViewModel: StatusViewModel = hiltViewModel(activity)
val statusMessageService = StatusMessageService val statusMessageService = StatusMessageService
@@ -179,7 +162,6 @@ fun TranslatorApp(
showExitDialog = true showExitDialog = true
} }
if (showExitDialog) { if (showExitDialog) {
AppAlertDialog( AppAlertDialog(
onDismissRequest = { showExitDialog = false }, onDismissRequest = { showExitDialog = false },
@@ -188,7 +170,6 @@ fun TranslatorApp(
confirmButton = { confirmButton = {
TextButton(onClick = { TextButton(onClick = {
showExitDialog = false showExitDialog = false
// Minimize the app similar to default back at root behavior
activity.moveTaskToBack(true) activity.moveTaskToBack(true)
}) { }) {
Text(stringResource(R.string.quit)) Text(stringResource(R.string.quit))
@@ -202,7 +183,6 @@ fun TranslatorApp(
) )
} }
// Check for app updates and show "What's New" dialog if needed
var showWhatsNewDialog by remember { mutableStateOf(false) } var showWhatsNewDialog by remember { mutableStateOf(false) }
val context = LocalContext.current val context = LocalContext.current
val changelogEntries = context.resources.getStringArray(R.array.changelog_entries) val changelogEntries = context.resources.getStringArray(R.array.changelog_entries)
@@ -210,7 +190,6 @@ fun TranslatorApp(
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
try { try {
// Only check for updates if the intro is completed
if (introCompleted) { if (introCompleted) {
val currentVersion = BuildConfig.VERSION_NAME val currentVersion = BuildConfig.VERSION_NAME
val hasSeenCurrentVersion = settingsViewModel.hasSeenCurrentVersion(currentVersion) val hasSeenCurrentVersion = settingsViewModel.hasSeenCurrentVersion(currentVersion)
@@ -260,7 +239,8 @@ fun TranslatorApp(
Screen.Translation.route, Screen.Translation.route,
Screen.Dictionary.route, Screen.Dictionary.route,
Screen.Exercises.route, Screen.Exercises.route,
Screen.Settings.route Screen.Settings.route,
Screen.Corrector.route
) )
} == true } == true
val isBottomBarHidden = isHiddenByHierarchy || currentRoute in setOf( val isBottomBarHidden = isHiddenByHierarchy || currentRoute in setOf(
@@ -283,12 +263,11 @@ fun TranslatorApp(
Screen.Translation, Screen.Translation,
Screen.Dictionary, Screen.Dictionary,
Screen.Settings, Screen.Settings,
Screen.Exercises Screen.Exercises,
Screen.Corrector
) )
// Always reset the selected section to its root and clear back stack between sections
if (inSameSection) { if (inSameSection) {
// If already within the same section, ensure we are at its graph root
navController.navigate(screen.route) { navController.navigate(screen.route) {
popUpTo(screen.route) { popUpTo(screen.route) {
inclusive = false inclusive = false
@@ -303,9 +282,8 @@ fun TranslatorApp(
restoreState = false restoreState = false
} }
} else { } else {
// Switching sections: clear entire back stack to start to avoid back navigation results
navController.navigate(screen.route) { navController.navigate(screen.route) {
popUpTo(0) { // Pop everything popUpTo(0) {
inclusive = true inclusive = true
saveState = false saveState = false
} }
@@ -340,8 +318,7 @@ fun TranslatorApp(
statusState = statusState, statusState = statusState,
navController = navController, navController = navController,
onDismiss = { statusMessageService.trigger(StatusAction.HideMessageBar) }, onDismiss = { statusMessageService.trigger(StatusAction.HideMessageBar) },
modifier = Modifier modifier = Modifier.fillMaxWidth()
.fillMaxWidth()
) )
AppNavHost( AppNavHost(
navController = navController, navController = navController,
@@ -393,10 +370,8 @@ private fun AppTheme(
val window = (view.context as Activity).window val window = (view.context as Activity).window
val windowInsetsController = WindowInsetsControllerCompat(window, view) val windowInsetsController = WindowInsetsControllerCompat(window, view)
// We must keep this for older Android version!!!
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
window.statusBarColor = colorScheme.surface.toArgb() window.statusBarColor = colorScheme.surface.toArgb()
//Elevation must be the same as BottomNavigationBar
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
window.navigationBarColor = colorScheme.surfaceColorAtElevation(8.dp).toArgb() window.navigationBarColor = colorScheme.surfaceColorAtElevation(8.dp).toArgb()
@@ -443,6 +418,4 @@ private fun AppTheme(
content() content()
} }
} }
} }

View File

@@ -23,9 +23,10 @@ import eu.gaudian.translator.model.VocabularyStage
import eu.gaudian.translator.view.categories.CategoryDetailScreen import eu.gaudian.translator.view.categories.CategoryDetailScreen
import eu.gaudian.translator.view.categories.CategoryListScreen import eu.gaudian.translator.view.categories.CategoryListScreen
import eu.gaudian.translator.view.composable.Screen import eu.gaudian.translator.view.composable.Screen
import eu.gaudian.translator.view.dictionary.CorrectionScreen
import eu.gaudian.translator.view.dictionary.DictionaryResultScreen import eu.gaudian.translator.view.dictionary.DictionaryResultScreen
import eu.gaudian.translator.view.dictionary.DictionaryScreen
import eu.gaudian.translator.view.dictionary.EtymologyResultScreen import eu.gaudian.translator.view.dictionary.EtymologyResultScreen
import eu.gaudian.translator.view.dictionary.MainDictionaryScreen
import eu.gaudian.translator.view.home.DailyReviewScreen import eu.gaudian.translator.view.home.DailyReviewScreen
import eu.gaudian.translator.view.home.HomeScreen import eu.gaudian.translator.view.home.HomeScreen
import eu.gaudian.translator.view.library.LibraryScreen import eu.gaudian.translator.view.library.LibraryScreen
@@ -78,22 +79,14 @@ fun AppNavHost(
navController: NavHostController, navController: NavHostController,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
// 1. Define your Main Tab "Leaf" Routes (the actual start destinations of your graphs)
val mainTabRoutes = setOf( val mainTabRoutes = setOf(
Screen.Home.route, Screen.Home.route,
Screen.Library.route, Screen.Library.route,
Screen.Stats.route, Screen.Stats.route,
) )
// Helper to check if a route is a top-level tab
// Note: Routes can be "main_home", "main_library" etc. but mainTabRoutes contains parent routes
fun isTabTransition(initial: String?, target: String?): Boolean { fun isTabTransition(initial: String?, target: String?): Boolean {
if (initial == null || target == null) return false if (initial == null || target == null) return false
// Check if either the direct route OR a "main_*" variant is in mainTabRoutes
val initialIsTab = mainTabRoutes.contains(initial) || val initialIsTab = mainTabRoutes.contains(initial) ||
mainTabRoutes.any { route -> mainTabRoutes.any { route ->
initial == "main_${route}" || initial.startsWith("${route}_") initial == "main_${route}" || initial.startsWith("${route}_")
@@ -109,45 +102,33 @@ fun AppNavHost(
navController = navController, navController = navController,
startDestination = Screen.Home.route, startDestination = Screen.Home.route,
modifier = modifier, modifier = modifier,
// ENTER TRANSITION
enterTransition = { enterTransition = {
if (isTabTransition(initialState.destination.route, targetState.destination.route)) { if (isTabTransition(initialState.destination.route, targetState.destination.route)) {
// Tab Switch: Just Fade In (Subtle Scale for modern feel)
fadeIn(animationSpec = tween(TRANSITION_DURATION)) + fadeIn(animationSpec = tween(TRANSITION_DURATION)) +
scaleIn(initialScale = 0.96f, animationSpec = tween(TRANSITION_DURATION)) scaleIn(initialScale = 0.96f, animationSpec = tween(TRANSITION_DURATION))
} else { } else {
// Detail Screen: Slide in from Right
slideInHorizontally( slideInHorizontally(
initialOffsetX = { fullWidth -> (fullWidth * 0.1f).toInt() }, initialOffsetX = { fullWidth -> (fullWidth * 0.1f).toInt() },
animationSpec = tween(TRANSITION_DURATION, easing = FastOutSlowInEasing) animationSpec = tween(TRANSITION_DURATION, easing = FastOutSlowInEasing)
) + fadeIn(animationSpec = tween(TRANSITION_DURATION)) ) + fadeIn(animationSpec = tween(TRANSITION_DURATION))
} }
}, },
// EXIT TRANSITION
exitTransition = { exitTransition = {
if (isTabTransition(initialState.destination.route, targetState.destination.route)) { if (isTabTransition(initialState.destination.route, targetState.destination.route)) {
// Tab Switch: Just Fade Out
fadeOut(animationSpec = tween(TRANSITION_DURATION)) fadeOut(animationSpec = tween(TRANSITION_DURATION))
} else { } else {
// Detail Screen: Slide out to Left
slideOutHorizontally( slideOutHorizontally(
targetOffsetX = { fullWidth -> -(fullWidth * 0.1f).toInt() }, targetOffsetX = { fullWidth -> -(fullWidth * 0.1f).toInt() },
animationSpec = tween(TRANSITION_DURATION, easing = FastOutSlowInEasing) animationSpec = tween(TRANSITION_DURATION, easing = FastOutSlowInEasing)
) + fadeOut(animationSpec = tween(TRANSITION_DURATION)) ) + fadeOut(animationSpec = tween(TRANSITION_DURATION))
} }
}, },
// POP ENTER (Pressing Back) -> Always Slide back from left
popEnterTransition = { popEnterTransition = {
slideInHorizontally( slideInHorizontally(
initialOffsetX = { fullWidth -> -(fullWidth * 0.1f).toInt() }, initialOffsetX = { fullWidth -> -(fullWidth * 0.1f).toInt() },
animationSpec = tween(TRANSITION_DURATION, easing = FastOutSlowInEasing) animationSpec = tween(TRANSITION_DURATION, easing = FastOutSlowInEasing)
) + fadeIn(animationSpec = tween(TRANSITION_DURATION)) ) + fadeIn(animationSpec = tween(TRANSITION_DURATION))
}, },
// POP EXIT (Pressing Back) -> Always Slide away to right
popExitTransition = { popExitTransition = {
slideOutHorizontally( slideOutHorizontally(
targetOffsetX = { fullWidth -> (fullWidth * 0.1f).toInt() }, targetOffsetX = { fullWidth -> (fullWidth * 0.1f).toInt() },
@@ -158,23 +139,18 @@ fun AppNavHost(
composable(Screen.Home.route) { composable(Screen.Home.route) {
HomeScreen(navController = navController) HomeScreen(navController = navController)
} }
composable(NavigationRoutes.DAILY_REVIEW) { composable(NavigationRoutes.DAILY_REVIEW) {
DailyReviewScreen(navController = navController) DailyReviewScreen(navController = navController)
} }
composable(NavigationRoutes.NEW_WORD) { composable(NavigationRoutes.NEW_WORD) {
NewWordScreen(navController = navController) NewWordScreen(navController = navController)
} }
composable(NavigationRoutes.NEW_WORD_REVIEW) { composable(NavigationRoutes.NEW_WORD_REVIEW) {
NewWordReviewScreen(navController = navController) NewWordReviewScreen(navController = navController)
} }
composable(NavigationRoutes.EXPLORE_PACKS) { composable(NavigationRoutes.EXPLORE_PACKS) {
ExplorePacksScreen(navController = navController) ExplorePacksScreen(navController = navController)
} }
composable( composable(
route = "${NavigationRoutes.START_EXERCISE}?categoryId={categoryId}", route = "${NavigationRoutes.START_EXERCISE}?categoryId={categoryId}",
arguments = listOf( arguments = listOf(
@@ -193,7 +169,6 @@ fun AppNavHost(
dueTodayOnly = false dueTodayOnly = false
) )
} }
composable(NavigationRoutes.START_EXERCISE_DAILY) { composable(NavigationRoutes.START_EXERCISE_DAILY) {
StartExerciseScreen( StartExerciseScreen(
navController = navController, navController = navController,
@@ -201,13 +176,12 @@ fun AppNavHost(
dueTodayOnly = true dueTodayOnly = true
) )
} }
// Define all other navigation graphs at the same top level.
homeGraph(navController) homeGraph(navController)
libraryGraph(navController) libraryGraph(navController)
statsGraph(navController) statsGraph(navController)
translationGraph(navController) translationGraph(navController)
dictionaryGraph(navController) dictionaryGraph(navController)
correctorGraph(navController)
exerciseGraph(navController) exerciseGraph(navController)
settingsGraph(navController) settingsGraph(navController)
} }
@@ -233,9 +207,7 @@ fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
LibraryScreen(navController = navController) LibraryScreen(navController = navController)
} }
composable("vocabulary_sorting") { composable("vocabulary_sorting") {
VocabularySortingScreen( VocabularySortingScreen(navController = navController)
navController = navController
)
} }
composable("vocabulary_detail/{itemId}") { backStackEntry -> composable("vocabulary_detail/{itemId}") { backStackEntry ->
val itemId = backStackEntry.arguments?.getString("itemId")?.toIntOrNull() val itemId = backStackEntry.arguments?.getString("itemId")?.toIntOrNull()
@@ -252,10 +224,7 @@ fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
composable("dictionary_result/{entryId}") { backStackEntry -> composable("dictionary_result/{entryId}") { backStackEntry ->
val entryId = backStackEntry.arguments?.getString("entryId")?.toIntOrNull() val entryId = backStackEntry.arguments?.getString("entryId")?.toIntOrNull()
if (entryId != null) { if (entryId != null) {
DictionaryResultScreen( DictionaryResultScreen(entryId = entryId, navController = navController)
entryId = entryId,
navController = navController,
)
} else { } else {
Text("Error: Invalid Entry ID") Text("Error: Invalid Entry ID")
} }
@@ -267,23 +236,16 @@ fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
navController = navController, navController = navController,
showDueTodayOnly = showDueTodayOnly, showDueTodayOnly = showDueTodayOnly,
categoryId = categoryId, categoryId = categoryId,
onNavigateToItem = { item -> onNavigateToItem = { item -> navController.navigate("vocabulary_detail/${item.id}") },
navController.navigate("vocabulary_detail/${item.id}")
},
onNavigateBack = { navController.popBackStack() }, onNavigateBack = { navController.popBackStack() },
enableNavigationButtons = true enableNavigationButtons = true
) )
} }
composable("language_progress") { composable("language_progress") {
LanguageJourneyScreen( LanguageJourneyScreen(navController = navController)
navController = navController
)
} }
composable(NavigationRoutes.STATS_VOCABULARY_HEATMAP) { composable(NavigationRoutes.STATS_VOCABULARY_HEATMAP) {
VocabularyHeatmapScreen( VocabularyHeatmapScreen(navController = navController)
navController = navController,
)
} }
composable("vocabulary_list/{showDueTodayOnly}/{stage}") { backStackEntry -> composable("vocabulary_list/{showDueTodayOnly}/{stage}") { backStackEntry ->
val showDueTodayOnly = backStackEntry.arguments?.getString("showDueTodayOnly")?.toBoolean() ?: false val showDueTodayOnly = backStackEntry.arguments?.getString("showDueTodayOnly")?.toBoolean() ?: false
@@ -291,14 +253,11 @@ fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
val stage = stageString?.let { val stage = stageString?.let {
if (it.equals("null", ignoreCase = true)) null else VocabularyStage.valueOf(it) if (it.equals("null", ignoreCase = true)) null else VocabularyStage.valueOf(it)
} }
AllCardsListScreen( AllCardsListScreen(
navController = navController, navController = navController,
showDueTodayOnly = showDueTodayOnly, showDueTodayOnly = showDueTodayOnly,
stage = stage, stage = stage,
onNavigateToItem = { item -> onNavigateToItem = { item -> navController.navigate("vocabulary_detail/${item.id}") },
navController.navigate("vocabulary_detail/${item.id}")
},
onNavigateBack = { navController.popBackStack() }, onNavigateBack = { navController.popBackStack() },
categoryId = 0, categoryId = 0,
enableNavigationButtons = true enableNavigationButtons = true
@@ -311,22 +270,15 @@ fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
navArgument("categories") { type = NavType.StringType; nullable = true }, navArgument("categories") { type = NavType.StringType; nullable = true },
navArgument("stages") { type = NavType.StringType; nullable = true }, navArgument("stages") { type = NavType.StringType; nullable = true },
navArgument("languages") { type = NavType.StringType; nullable = true }, navArgument("languages") { type = NavType.StringType; nullable = true },
navArgument("dailyOnly") { navArgument("dailyOnly") { type = NavType.BoolType; defaultValue = false }
type = NavType.BoolType
defaultValue = false
},
) )
) { backStackEntry -> ) { backStackEntry ->
val arguments = backStackEntry.arguments val arguments = backStackEntry.arguments
val dailyOnly = arguments?.getBoolean("dailyOnly") ?: false val dailyOnly = arguments?.getBoolean("dailyOnly") ?: false
val categoryIds = arguments?.getString("categories") val categoryIds = arguments?.getString("categories")
val stageNames = arguments?.getString("stages") val stageNames = arguments?.getString("stages")
val languageIds = arguments?.getString("languages") val languageIds = arguments?.getString("languages")
val dailyOnlyJson = "{\"dailyOnly\": $dailyOnly}" val dailyOnlyJson = "{\"dailyOnly\": $dailyOnly}"
VocabularyExerciseHostScreen( VocabularyExerciseHostScreen(
categoryIdsAsJson = categoryIds, categoryIdsAsJson = categoryIds,
stageNamesAsJson = stageNames, stageNamesAsJson = stageNames,
@@ -336,13 +288,7 @@ fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
navController = navController navController = navController
) )
} }
composable( composable("vocabulary_exercise/{dailyOnly}?", arguments = listOf(navArgument("dailyOnly") { type = NavType.BoolType })) { _ ->
route = "vocabulary_exercise/{dailyOnly}?",
arguments = listOf(
navArgument("dailyOnly") { type = NavType.BoolType },
)
) { _ ->
VocabularyExerciseHostScreen( VocabularyExerciseHostScreen(
categoryIdsAsJson = null, categoryIdsAsJson = null,
stageNamesAsJson = null, stageNamesAsJson = null,
@@ -352,34 +298,18 @@ fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
dailyOnlyAsJson = "{\"dailyOnly\": true}" dailyOnlyAsJson = "{\"dailyOnly\": true}"
) )
} }
composable( composable("stage_detail/{stage}", arguments = listOf(navArgument("stage") { type = NavType.EnumType(VocabularyStage::class.java) })) { backStackEntry ->
"stage_detail/{stage}", @Suppress("DEPRECATION")
arguments = listOf( val stage = backStackEntry.arguments?.getSerializable("stage") as VocabularyStage
navArgument("stage") { StageDetailScreen(navController = navController, stage = stage)
type = NavType.EnumType(VocabularyStage::class.java)
}
)
)
{ backStackEntry ->
@Suppress("DEPRECATION") val stage = backStackEntry.arguments?.getSerializable("stage") as VocabularyStage
//NOTE: can ignore warning for now, once moved away from min SDK 28, use:
// val stage = backStackEntry.arguments?.getSerializable("stage", VocabularyStage::class.java)
StageDetailScreen(
navController = navController,
stage = stage
)
} }
composable("category_detail/{categoryId}") { backStackEntry -> composable("category_detail/{categoryId}") { backStackEntry ->
val categoryId = backStackEntry.arguments?.getString("categoryId")?.toIntOrNull() val categoryId = backStackEntry.arguments?.getString("categoryId")?.toIntOrNull()
if (categoryId != null) { if (categoryId != null) {
CategoryDetailScreen( CategoryDetailScreen(
categoryId = categoryId, categoryId = categoryId,
onBackClick = { navController.popBackStack() }, onBackClick = { navController.popBackStack() },
onNavigateToItem = { item -> onNavigateToItem = { item -> navController.navigate("vocabulary_detail/${item.id}") },
navController.navigate("vocabulary_detail/${item.id}")
},
navController = navController navController = navController
) )
} }
@@ -387,37 +317,22 @@ fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
composable("category_list_screen") { composable("category_list_screen") {
CategoryListScreen( CategoryListScreen(
onNavigateBack = { navController.popBackStack() }, onNavigateBack = { navController.popBackStack() },
onCategoryClicked = { categoryId -> onCategoryClicked = { categoryId -> navController.navigate("category_detail/$categoryId") }
navController.navigate("category_detail/$categoryId")
}
) )
} }
composable( composable("vocabulary_sorting?mode={mode}", arguments = listOf(navArgument("mode") { type = NavType.StringType; nullable = true })) { backStackEntry ->
route = "vocabulary_sorting?mode={mode}", // Route now accepts an optional 'mode'
arguments = listOf(
navArgument("mode") { // Define the argument
type = NavType.StringType
nullable = true
}
)
) { backStackEntry ->
VocabularySortingScreen( VocabularySortingScreen(
navController = navController, navController = navController,
// Pass the argument to the screen
initialFilterMode = backStackEntry.arguments?.getString("mode") initialFilterMode = backStackEntry.arguments?.getString("mode")
) )
} }
composable("no_grammar_items") { composable("no_grammar_items") {
NoGrammarItemsScreen( NoGrammarItemsScreen(navController = navController)
navController = navController
)
} }
} }
} }
fun NavGraphBuilder.statsGraph( fun NavGraphBuilder.statsGraph(navController: NavHostController) {
navController: NavHostController,
) {
navigation( navigation(
startDestination = "main_stats", startDestination = "main_stats",
route = Screen.Stats.route route = Screen.Stats.route
@@ -426,9 +341,7 @@ fun NavGraphBuilder.statsGraph(
StatsScreen(navController = navController) StatsScreen(navController = navController)
} }
composable("stats/vocabulary_sorting") { composable("stats/vocabulary_sorting") {
VocabularySortingScreen( VocabularySortingScreen(navController = navController)
navController = navController
)
} }
composable("stats/vocabulary_list/{showDueTodayOnly}/{categoryId}") { backStackEntry -> composable("stats/vocabulary_list/{showDueTodayOnly}/{categoryId}") { backStackEntry ->
val showDueTodayOnly = backStackEntry.arguments?.getString("showDueTodayOnly")?.toBoolean() ?: false val showDueTodayOnly = backStackEntry.arguments?.getString("showDueTodayOnly")?.toBoolean() ?: false
@@ -437,22 +350,16 @@ fun NavGraphBuilder.statsGraph(
navController = navController, navController = navController,
showDueTodayOnly = showDueTodayOnly, showDueTodayOnly = showDueTodayOnly,
categoryId = categoryId, categoryId = categoryId,
onNavigateToItem = { item -> onNavigateToItem = { item -> navController.navigate("vocabulary_detail/${item.id}") },
navController.navigate("vocabulary_detail/${item.id}")
},
onNavigateBack = { navController.popBackStack() }, onNavigateBack = { navController.popBackStack() },
enableNavigationButtons = true enableNavigationButtons = true
) )
} }
composable(NavigationRoutes.STATS_LANGUAGE_PROGRESS) { composable(NavigationRoutes.STATS_LANGUAGE_PROGRESS) {
LanguageJourneyScreen( LanguageJourneyScreen(navController = navController)
navController = navController
)
} }
composable(NavigationRoutes.STATS_VOCABULARY_HEATMAP) { composable(NavigationRoutes.STATS_VOCABULARY_HEATMAP) {
VocabularyHeatmapScreen( VocabularyHeatmapScreen(navController = navController)
navController = navController,
)
} }
composable("stats/vocabulary_list/{showDueTodayOnly}/{stage}") { backStackEntry -> composable("stats/vocabulary_list/{showDueTodayOnly}/{stage}") { backStackEntry ->
val showDueTodayOnly = backStackEntry.arguments?.getString("showDueTodayOnly")?.toBoolean() ?: false val showDueTodayOnly = backStackEntry.arguments?.getString("showDueTodayOnly")?.toBoolean() ?: false
@@ -460,14 +367,11 @@ fun NavGraphBuilder.statsGraph(
val stage = stageString?.let { val stage = stageString?.let {
if (it.equals("null", ignoreCase = true)) null else VocabularyStage.valueOf(it) if (it.equals("null", ignoreCase = true)) null else VocabularyStage.valueOf(it)
} }
AllCardsListScreen( AllCardsListScreen(
navController = navController, navController = navController,
showDueTodayOnly = showDueTodayOnly, showDueTodayOnly = showDueTodayOnly,
stage = stage, stage = stage,
onNavigateToItem = { item -> onNavigateToItem = { item -> navController.navigate("vocabulary_detail/${item.id}") },
navController.navigate("vocabulary_detail/${item.id}")
},
onNavigateBack = { navController.popBackStack() }, onNavigateBack = { navController.popBackStack() },
categoryId = 0, categoryId = 0,
enableNavigationButtons = true enableNavigationButtons = true
@@ -475,14 +379,11 @@ fun NavGraphBuilder.statsGraph(
} }
composable("stats/category_detail/{categoryId}") { backStackEntry -> composable("stats/category_detail/{categoryId}") { backStackEntry ->
val categoryId = backStackEntry.arguments?.getString("categoryId")?.toIntOrNull() val categoryId = backStackEntry.arguments?.getString("categoryId")?.toIntOrNull()
if (categoryId != null) { if (categoryId != null) {
CategoryDetailScreen( CategoryDetailScreen(
categoryId = categoryId, categoryId = categoryId,
onBackClick = { navController.popBackStack() }, onBackClick = { navController.popBackStack() },
onNavigateToItem = { item -> onNavigateToItem = { item -> navController.navigate("vocabulary_detail/${item.id}") },
navController.navigate("vocabulary_detail/${item.id}")
},
navController = navController navController = navController
) )
} }
@@ -490,29 +391,14 @@ fun NavGraphBuilder.statsGraph(
composable("stats/category_list_screen") { composable("stats/category_list_screen") {
CategoryListScreen( CategoryListScreen(
onNavigateBack = { navController.popBackStack() }, onNavigateBack = { navController.popBackStack() },
onCategoryClicked = { categoryId -> onCategoryClicked = { categoryId -> navController.navigate("stats/category_detail/$categoryId") }
navController.navigate("stats/category_detail/$categoryId")
}
) )
} }
composable( composable("stats/vocabulary_sorting?mode={mode}", arguments = listOf(navArgument("mode") { type = NavType.StringType; nullable = true })) { backStackEntry ->
route = "stats/vocabulary_sorting?mode={mode}", VocabularySortingScreen(navController = navController, initialFilterMode = backStackEntry.arguments?.getString("mode"))
arguments = listOf(
navArgument("mode") {
type = NavType.StringType
nullable = true
}
)
) { backStackEntry ->
VocabularySortingScreen(
navController = navController,
initialFilterMode = backStackEntry.arguments?.getString("mode")
)
} }
composable("stats/no_grammar_items") { composable("stats/no_grammar_items") {
NoGrammarItemsScreen( NoGrammarItemsScreen(navController = navController)
navController = navController
)
} }
} }
} }
@@ -539,15 +425,16 @@ fun NavGraphBuilder.dictionaryGraph(navController: NavHostController) {
route = Screen.Dictionary.route route = Screen.Dictionary.route
) { ) {
composable("main_dictionary") { composable("main_dictionary") {
MainDictionaryScreen(navController = navController) DictionaryScreen(
navController = navController,
onEntryClick = { entry -> navController.navigate("dictionary_result/${entry.id}") },
onNavigateToOptions = { navController.navigate("dictionary_options") }
)
} }
composable("dictionary_result/{entryId}") { backStackEntry -> composable("dictionary_result/{entryId}") { backStackEntry ->
val entryId = backStackEntry.arguments?.getString("entryId")?.toIntOrNull() val entryId = backStackEntry.arguments?.getString("entryId")?.toIntOrNull()
if (entryId != null) { if (entryId != null) {
DictionaryResultScreen( DictionaryResultScreen(entryId = entryId, navController = navController)
entryId = entryId,
navController = navController,
)
} else { } else {
Text("Error: Invalid Entry ID") Text("Error: Invalid Entry ID")
} }
@@ -558,43 +445,39 @@ fun NavGraphBuilder.dictionaryGraph(navController: NavHostController) {
composable("etymology_result/{word}/{languageCode}") { backStackEntry -> composable("etymology_result/{word}/{languageCode}") { backStackEntry ->
val word = backStackEntry.arguments?.getString("word") ?: "" val word = backStackEntry.arguments?.getString("word") ?: ""
val languageCode = backStackEntry.arguments?.getString("languageCode")?.toIntOrNull() ?: 1 val languageCode = backStackEntry.arguments?.getString("languageCode")?.toIntOrNull() ?: 1
EtymologyResultScreen( EtymologyResultScreen(navController = navController, word = word, languageCode = languageCode)
navController = navController,
word = word,
languageCode = languageCode
)
} }
}
}
fun NavGraphBuilder.correctorGraph(navController: NavHostController) {
navigation(
startDestination = "main_corrector",
route = Screen.Corrector.route
) {
composable("main_corrector") {
CorrectionScreen(navController = navController)
}
} }
} }
@OptIn(androidx.compose.animation.ExperimentalAnimationApi::class) @OptIn(androidx.compose.animation.ExperimentalAnimationApi::class)
fun NavGraphBuilder.exerciseGraph( fun NavGraphBuilder.exerciseGraph(navController: NavHostController) {
navController: NavHostController,
) {
navigation( navigation(
startDestination = "main_exercise", startDestination = "main_exercise",
route = Screen.Exercises.route route = Screen.Exercises.route
) { ) {
composable("main_exercise") { composable("main_exercise") {
MainExerciseScreen( MainExerciseScreen(navController = navController)
navController = navController,
)
} }
composable("exercise_session") { composable("exercise_session") {
ExerciseSessionScreen( ExerciseSessionScreen(navController = navController)
navController = navController,
)
} }
composable("youtube_exercise") { composable("youtube_exercise") {
YouTubeExerciseScreen( YouTubeExerciseScreen(navController = navController)
navController = navController
)
} }
composable("youtube_browse") { composable("youtube_browse") {
YouTubeBrowserScreen( YouTubeBrowserScreen(navController = navController)
navController = navController,
)
} }
} }
} }

View File

@@ -11,6 +11,7 @@ import androidx.compose.animation.core.spring
import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -77,6 +78,7 @@ sealed class Screen(
object Settings : Screen("settings", R.string.title_settings, AppIcons.SettingsFilled, AppIcons.SettingsOutlined) object Settings : Screen("settings", R.string.title_settings, AppIcons.SettingsFilled, AppIcons.SettingsOutlined)
object More : Screen("more", R.string.label_more, AppIcons.MoreVert, AppIcons.MoreHorizontal) object More : Screen("more", R.string.label_more, AppIcons.MoreVert, AppIcons.MoreHorizontal)
object Dictionary : Screen("dictionary", R.string.label_dictionary, AppIcons.DictionaryFilled, AppIcons.DictionaryOutlined) object Dictionary : Screen("dictionary", R.string.label_dictionary, AppIcons.DictionaryFilled, AppIcons.DictionaryOutlined)
object Corrector : Screen("corrector", R.string.title_corrector, AppIcons.SpellCheck, AppIcons.SpellCheck)
object Exercises : Screen("exercises", R.string.label_exercises, AppIcons.DictionaryFilled, AppIcons.DictionaryOutlined) object Exercises : Screen("exercises", R.string.label_exercises, AppIcons.DictionaryFilled, AppIcons.DictionaryOutlined)
companion object { companion object {
@@ -88,6 +90,7 @@ sealed class Screen(
val items = mutableListOf<Screen>() val items = mutableListOf<Screen>()
items.add(Translation) items.add(Translation)
items.add(Dictionary) items.add(Dictionary)
items.add(Corrector)
items.add(Settings) items.add(Settings)
if (showExperimental) { if (showExperimental) {
items.add(Exercises) items.add(Exercises)
@@ -258,7 +261,7 @@ fun BottomNavigationBar(
.background( .background(
brush = Brush.radialGradient( brush = Brush.radialGradient(
colors = listOf( colors = listOf(
MaterialTheme.colorScheme.primary.copy(alpha = 0.5f), MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f),
Color.Transparent Color.Transparent
) )
), ),
@@ -271,6 +274,12 @@ fun BottomNavigationBar(
modifier = Modifier modifier = Modifier
.size(playButtonSize) .size(playButtonSize)
.clip(CircleShape) .clip(CircleShape)
// CHANGED: Added a border to give the button definition
.border(
width = 4.dp, // Adjust this thickness to your liking
color = MaterialTheme.colorScheme.surfaceVariant, // Creates a nice "cutout" separation
shape = CircleShape
)
.background(MaterialTheme.colorScheme.primaryContainer) .background(MaterialTheme.colorScheme.primaryContainer)
.clickable { .clickable {
haptic.performHapticFeedback(HapticFeedbackType.LongPress) haptic.performHapticFeedback(HapticFeedbackType.LongPress)
@@ -281,7 +290,7 @@ fun BottomNavigationBar(
Icon( Icon(
imageVector = Icons.Filled.PlayArrow, imageVector = Icons.Filled.PlayArrow,
contentDescription = "Play", contentDescription = "Play",
tint = Color.White, tint = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier.size(32.dp) modifier = Modifier.size(32.dp)
) )
} }
@@ -400,4 +409,4 @@ fun BottomNavigationBarPreview() {
) )
} }
} }
} }

View File

@@ -60,12 +60,16 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavController
import eu.gaudian.translator.R import eu.gaudian.translator.R
import eu.gaudian.translator.ui.theme.semanticColors import eu.gaudian.translator.ui.theme.semanticColors
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.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.AppSwitch import eu.gaudian.translator.view.composable.AppSwitch
import eu.gaudian.translator.view.composable.AppTopAppBar
import eu.gaudian.translator.view.composable.DictionaryLanguageDropDown import eu.gaudian.translator.view.composable.DictionaryLanguageDropDown
import eu.gaudian.translator.view.composable.DropdownDefaults import eu.gaudian.translator.view.composable.DropdownDefaults
import eu.gaudian.translator.view.composable.LargeDropdownMenuItem import eu.gaudian.translator.view.composable.LargeDropdownMenuItem
@@ -73,12 +77,15 @@ import eu.gaudian.translator.viewmodel.CorrectionViewModel
import eu.gaudian.translator.viewmodel.LanguageViewModel import eu.gaudian.translator.viewmodel.LanguageViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
// 1. STATEFUL COMPONENT (Connects to ViewModels)
@Composable @Composable
fun CorrectionScreen( fun CorrectionScreen(
correctionViewModel: CorrectionViewModel, navController: NavController
languageViewModel: LanguageViewModel
) { ) {
val activity = LocalContext.current.findActivity()
val correctionViewModel: CorrectionViewModel = hiltViewModel(activity)
val languageViewModel : LanguageViewModel = hiltViewModel(activity)
val textFieldValue by correctionViewModel.textFieldValue.collectAsState() val textFieldValue by correctionViewModel.textFieldValue.collectAsState()
val explanation by correctionViewModel.explanation.collectAsState() val explanation by correctionViewModel.explanation.collectAsState()
val isLoading by correctionViewModel.isLoading.collectAsState() val isLoading by correctionViewModel.isLoading.collectAsState()
@@ -89,6 +96,15 @@ fun CorrectionScreen(
val successColor = MaterialTheme.semanticColors.success val successColor = MaterialTheme.semanticColors.success
Column(){
AppTopAppBar(
title = stringResource(R.string.label_correction),
onNavigateBack = {
navController.popBackStack()
},
)
CorrectionScreenContent( CorrectionScreenContent(
textFieldValue = textFieldValue, textFieldValue = textFieldValue,
explanation = explanation, explanation = explanation,
@@ -113,6 +129,7 @@ fun CorrectionScreen(
) )
} }
) )
}
} }
// 2. STATELESS COMPONENT (Handles UI Layout) // 2. STATELESS COMPONENT (Handles UI Layout)
@@ -304,7 +321,6 @@ fun CorrectionScreenContent(
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@@ -575,4 +591,4 @@ private fun CorrectionScreenResultsPreview() {
} }
) )
} }
} }

View File

@@ -1,12 +1,16 @@
package eu.gaudian.translator.view.dictionary package eu.gaudian.translator.view.dictionary
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import eu.gaudian.translator.R
import eu.gaudian.translator.utils.findActivity import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.AppTopAppBar
import eu.gaudian.translator.viewmodel.DictionaryViewModel import eu.gaudian.translator.viewmodel.DictionaryViewModel
import eu.gaudian.translator.viewmodel.LanguageViewModel import eu.gaudian.translator.viewmodel.LanguageViewModel
@@ -20,15 +24,21 @@ fun DictionaryScreen(
val dictionaryViewModel: DictionaryViewModel = hiltViewModel(viewModelStoreOwner = activity) val dictionaryViewModel: DictionaryViewModel = hiltViewModel(viewModelStoreOwner = activity)
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity) val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
Column {
AppTopAppBar(
title = stringResource(R.string.label_dictionary),
onNavigateBack = { navController.popBackStack() }
)
// Use the new refactored component // Use the new refactored component
DictionaryScreenContent( DictionaryScreenContent(
navController = navController, navController = navController,
onEntryClick = onEntryClick, onEntryClick = onEntryClick,
dictionaryViewModel = dictionaryViewModel, dictionaryViewModel = dictionaryViewModel,
languageViewModel = languageViewModel, languageViewModel = languageViewModel,
onNavigateToOptions = onNavigateToOptions onNavigateToOptions = onNavigateToOptions
) )
}
} }
@Preview @Preview
@@ -40,4 +50,4 @@ fun DictionaryScreenPreview() {
onEntryClick = {}, onEntryClick = {},
onNavigateToOptions = {} onNavigateToOptions = {}
) )
} }

View File

@@ -1,97 +0,0 @@
@file:Suppress("HardCodedStringLiteral")
package eu.gaudian.translator.view.dictionary
import androidx.compose.foundation.layout.Column
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.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController
import eu.gaudian.translator.R
import eu.gaudian.translator.ui.theme.ThemePreviews
import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppTabLayout
import eu.gaudian.translator.view.composable.Screen
import eu.gaudian.translator.view.composable.TabItem
import eu.gaudian.translator.viewmodel.CorrectionViewModel
import eu.gaudian.translator.viewmodel.DictionaryViewModel
import eu.gaudian.translator.viewmodel.LanguageViewModel
@Composable
private fun getDictionaryTabs(): List<TabItem> {
return listOf(
DictionaryTab(stringResource(R.string.label_dictionary), AppIcons.Dictionary),
DictionaryTab(stringResource(R.string.title_corrector), AppIcons.Check)
)
}
private data class DictionaryTab(override val title: String, override val icon: ImageVector) :
TabItem
@Composable
fun MainDictionaryScreen(
navController: NavController
) {
val activity = LocalContext.current.findActivity()
val dictionaryViewModel: DictionaryViewModel = hiltViewModel(viewModelStoreOwner = activity)
val correctionViewModel: CorrectionViewModel = hiltViewModel(viewModelStoreOwner = activity)
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
val dictionaryTabs = getDictionaryTabs()
var selectedTab by remember { mutableStateOf(dictionaryTabs[0]) }
Column {
AppTabLayout(
tabs = dictionaryTabs,
selectedTab = selectedTab,
onTabSelected = { selectedTab = it },
onNavigateBack = {
if (!navController.popBackStack()) {
navController.navigate(Screen.Home.route) {
launchSingleTop = true
restoreState = false
}
}
}
)
when (selectedTab) {
dictionaryTabs[0] -> DictionaryScreen(
navController = navController,
onEntryClick = { entry ->
// Set flag indicating navigation is from external source (not DictionaryResultScreen)
dictionaryViewModel.setNavigatingFromDictionaryResult(false)
navController.navigate("dictionary_result/${entry.id}")
},
onNavigateToOptions = {
navController.navigate("dictionary_options")
}
)
dictionaryTabs[1] -> CorrectionScreen(
correctionViewModel = correctionViewModel,
languageViewModel = languageViewModel
)
}
}
}
@ThemePreviews
@Composable
fun DictionaryHostScreenPreview() {
val navController = rememberNavController()
MainDictionaryScreen(
navController = navController,
)
}

View File

@@ -1171,4 +1171,6 @@
<!-- Explore Packs Hint --> <!-- Explore Packs Hint -->
<string name="hint_explore_packs_title">About Vocabulary Packs</string> <string name="hint_explore_packs_title">About Vocabulary Packs</string>
<string name="label_import_csv_or_lists">Import Lists or CSV</string> <string name="label_import_csv_or_lists">Import Lists or CSV</string>
<string name="label_corrector">Corrector</string>
<string name="label_correction">Correction</string>
</resources> </resources>