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
class MainActivity : ComponentActivity() {
private val settingsViewModel: SettingsViewModel by viewModels()
private var isReady = false
private var isUiLoaded = false
private var isInitializing = true
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen().apply {
// The splash screen will now correctly wait until isReady is true
setKeepOnScreenCondition { !isReady }
}
super.onCreate(savedInstanceState)
lifecycleScope.launch {
@@ -104,28 +99,22 @@ class MainActivity : ComponentActivity() {
WindowCompat.setDecorFitsSystemWindows(window, false)
// Show UI immediately and load data in background
setContent {
AppTheme(settingsViewModel = settingsViewModel) {
TranslatorApp(settingsViewModel = settingsViewModel)
}
}
// Mark UI as loaded immediately after setContent
isUiLoaded = true
// Start initialization in background without blocking UI
initializeData()
}
private fun initializeData() {
lifecycleScope.launch(Dispatchers.IO) {
// Get repositories from the Application instance (lazy initialization)
val myApp = application as MyApplication
val languageRepository = myApp.languageRepository
val apiRepository = myApp.apiRepository
// Perform initialization in parallel where possible
val languageJob = launch {
languageRepository.initializeDefaultLanguages()
languageRepository.initializeAllLanguages()
@@ -135,13 +124,10 @@ class MainActivity : ComponentActivity() {
apiRepository.initialInit()
}
// Wait for both to complete
languageJob.join()
apiJob.join()
// Signal readiness after all work is done.
isReady = true
isInitializing = false
}
}
}
@@ -149,10 +135,7 @@ class MainActivity : ComponentActivity() {
@Suppress("AssignedValueIsNeverRead")
@SuppressLint("LocalContextResourcesRead")
@Composable
fun TranslatorApp(
settingsViewModel: SettingsViewModel
) {
fun TranslatorApp(settingsViewModel: SettingsViewModel) {
val activity = LocalContext.current.findActivity()
val statusViewModel: StatusViewModel = hiltViewModel(activity)
val statusMessageService = StatusMessageService
@@ -179,7 +162,6 @@ fun TranslatorApp(
showExitDialog = true
}
if (showExitDialog) {
AppAlertDialog(
onDismissRequest = { showExitDialog = false },
@@ -188,7 +170,6 @@ fun TranslatorApp(
confirmButton = {
TextButton(onClick = {
showExitDialog = false
// Minimize the app similar to default back at root behavior
activity.moveTaskToBack(true)
}) {
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) }
val context = LocalContext.current
val changelogEntries = context.resources.getStringArray(R.array.changelog_entries)
@@ -210,7 +190,6 @@ fun TranslatorApp(
LaunchedEffect(Unit) {
try {
// Only check for updates if the intro is completed
if (introCompleted) {
val currentVersion = BuildConfig.VERSION_NAME
val hasSeenCurrentVersion = settingsViewModel.hasSeenCurrentVersion(currentVersion)
@@ -260,7 +239,8 @@ fun TranslatorApp(
Screen.Translation.route,
Screen.Dictionary.route,
Screen.Exercises.route,
Screen.Settings.route
Screen.Settings.route,
Screen.Corrector.route
)
} == true
val isBottomBarHidden = isHiddenByHierarchy || currentRoute in setOf(
@@ -283,12 +263,11 @@ fun TranslatorApp(
Screen.Translation,
Screen.Dictionary,
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 already within the same section, ensure we are at its graph root
navController.navigate(screen.route) {
popUpTo(screen.route) {
inclusive = false
@@ -303,9 +282,8 @@ fun TranslatorApp(
restoreState = false
}
} else {
// Switching sections: clear entire back stack to start to avoid back navigation results
navController.navigate(screen.route) {
popUpTo(0) { // Pop everything
popUpTo(0) {
inclusive = true
saveState = false
}
@@ -340,8 +318,7 @@ fun TranslatorApp(
statusState = statusState,
navController = navController,
onDismiss = { statusMessageService.trigger(StatusAction.HideMessageBar) },
modifier = Modifier
.fillMaxWidth()
modifier = Modifier.fillMaxWidth()
)
AppNavHost(
navController = navController,
@@ -393,10 +370,8 @@ private fun AppTheme(
val window = (view.context as Activity).window
val windowInsetsController = WindowInsetsControllerCompat(window, view)
// We must keep this for older Android version!!!
@Suppress("DEPRECATION")
window.statusBarColor = colorScheme.surface.toArgb()
//Elevation must be the same as BottomNavigationBar
@Suppress("DEPRECATION")
window.navigationBarColor = colorScheme.surfaceColorAtElevation(8.dp).toArgb()
@@ -443,6 +418,4 @@ private fun AppTheme(
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.CategoryListScreen
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.DictionaryScreen
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.HomeScreen
import eu.gaudian.translator.view.library.LibraryScreen
@@ -78,22 +79,14 @@ fun AppNavHost(
navController: NavHostController,
modifier: Modifier = Modifier
) {
// 1. Define your Main Tab "Leaf" Routes (the actual start destinations of your graphs)
val mainTabRoutes = setOf(
Screen.Home.route,
Screen.Library.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 {
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) ||
mainTabRoutes.any { route ->
initial == "main_${route}" || initial.startsWith("${route}_")
@@ -109,45 +102,33 @@ fun AppNavHost(
navController = navController,
startDestination = Screen.Home.route,
modifier = modifier,
// ENTER TRANSITION
enterTransition = {
if (isTabTransition(initialState.destination.route, targetState.destination.route)) {
// Tab Switch: Just Fade In (Subtle Scale for modern feel)
fadeIn(animationSpec = tween(TRANSITION_DURATION)) +
scaleIn(initialScale = 0.96f, animationSpec = tween(TRANSITION_DURATION))
} else {
// Detail Screen: Slide in from Right
slideInHorizontally(
initialOffsetX = { fullWidth -> (fullWidth * 0.1f).toInt() },
animationSpec = tween(TRANSITION_DURATION, easing = FastOutSlowInEasing)
) + fadeIn(animationSpec = tween(TRANSITION_DURATION))
}
},
// EXIT TRANSITION
exitTransition = {
if (isTabTransition(initialState.destination.route, targetState.destination.route)) {
// Tab Switch: Just Fade Out
fadeOut(animationSpec = tween(TRANSITION_DURATION))
} else {
// Detail Screen: Slide out to Left
slideOutHorizontally(
targetOffsetX = { fullWidth -> -(fullWidth * 0.1f).toInt() },
animationSpec = tween(TRANSITION_DURATION, easing = FastOutSlowInEasing)
) + fadeOut(animationSpec = tween(TRANSITION_DURATION))
}
},
// POP ENTER (Pressing Back) -> Always Slide back from left
popEnterTransition = {
slideInHorizontally(
initialOffsetX = { fullWidth -> -(fullWidth * 0.1f).toInt() },
animationSpec = tween(TRANSITION_DURATION, easing = FastOutSlowInEasing)
) + fadeIn(animationSpec = tween(TRANSITION_DURATION))
},
// POP EXIT (Pressing Back) -> Always Slide away to right
popExitTransition = {
slideOutHorizontally(
targetOffsetX = { fullWidth -> (fullWidth * 0.1f).toInt() },
@@ -158,23 +139,18 @@ fun AppNavHost(
composable(Screen.Home.route) {
HomeScreen(navController = navController)
}
composable(NavigationRoutes.DAILY_REVIEW) {
DailyReviewScreen(navController = navController)
}
composable(NavigationRoutes.NEW_WORD) {
NewWordScreen(navController = navController)
}
composable(NavigationRoutes.NEW_WORD_REVIEW) {
NewWordReviewScreen(navController = navController)
}
composable(NavigationRoutes.EXPLORE_PACKS) {
ExplorePacksScreen(navController = navController)
}
composable(
route = "${NavigationRoutes.START_EXERCISE}?categoryId={categoryId}",
arguments = listOf(
@@ -193,7 +169,6 @@ fun AppNavHost(
dueTodayOnly = false
)
}
composable(NavigationRoutes.START_EXERCISE_DAILY) {
StartExerciseScreen(
navController = navController,
@@ -201,13 +176,12 @@ fun AppNavHost(
dueTodayOnly = true
)
}
// Define all other navigation graphs at the same top level.
homeGraph(navController)
libraryGraph(navController)
statsGraph(navController)
translationGraph(navController)
dictionaryGraph(navController)
correctorGraph(navController)
exerciseGraph(navController)
settingsGraph(navController)
}
@@ -233,9 +207,7 @@ fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
LibraryScreen(navController = navController)
}
composable("vocabulary_sorting") {
VocabularySortingScreen(
navController = navController
)
VocabularySortingScreen(navController = navController)
}
composable("vocabulary_detail/{itemId}") { backStackEntry ->
val itemId = backStackEntry.arguments?.getString("itemId")?.toIntOrNull()
@@ -252,10 +224,7 @@ fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
composable("dictionary_result/{entryId}") { backStackEntry ->
val entryId = backStackEntry.arguments?.getString("entryId")?.toIntOrNull()
if (entryId != null) {
DictionaryResultScreen(
entryId = entryId,
navController = navController,
)
DictionaryResultScreen(entryId = entryId, navController = navController)
} else {
Text("Error: Invalid Entry ID")
}
@@ -267,23 +236,16 @@ fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
navController = navController,
showDueTodayOnly = showDueTodayOnly,
categoryId = categoryId,
onNavigateToItem = { item ->
navController.navigate("vocabulary_detail/${item.id}")
},
onNavigateToItem = { item -> navController.navigate("vocabulary_detail/${item.id}") },
onNavigateBack = { navController.popBackStack() },
enableNavigationButtons = true
)
}
composable("language_progress") {
LanguageJourneyScreen(
navController = navController
)
LanguageJourneyScreen(navController = navController)
}
composable(NavigationRoutes.STATS_VOCABULARY_HEATMAP) {
VocabularyHeatmapScreen(
navController = navController,
)
VocabularyHeatmapScreen(navController = navController)
}
composable("vocabulary_list/{showDueTodayOnly}/{stage}") { backStackEntry ->
val showDueTodayOnly = backStackEntry.arguments?.getString("showDueTodayOnly")?.toBoolean() ?: false
@@ -291,14 +253,11 @@ fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
val stage = stageString?.let {
if (it.equals("null", ignoreCase = true)) null else VocabularyStage.valueOf(it)
}
AllCardsListScreen(
navController = navController,
showDueTodayOnly = showDueTodayOnly,
stage = stage,
onNavigateToItem = { item ->
navController.navigate("vocabulary_detail/${item.id}")
},
onNavigateToItem = { item -> navController.navigate("vocabulary_detail/${item.id}") },
onNavigateBack = { navController.popBackStack() },
categoryId = 0,
enableNavigationButtons = true
@@ -311,22 +270,15 @@ fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
navArgument("categories") { type = NavType.StringType; nullable = true },
navArgument("stages") { type = NavType.StringType; nullable = true },
navArgument("languages") { type = NavType.StringType; nullable = true },
navArgument("dailyOnly") {
type = NavType.BoolType
defaultValue = false
},
navArgument("dailyOnly") { type = NavType.BoolType; defaultValue = false }
)
) { backStackEntry ->
val arguments = backStackEntry.arguments
val dailyOnly = arguments?.getBoolean("dailyOnly") ?: false
val categoryIds = arguments?.getString("categories")
val stageNames = arguments?.getString("stages")
val languageIds = arguments?.getString("languages")
val dailyOnlyJson = "{\"dailyOnly\": $dailyOnly}"
VocabularyExerciseHostScreen(
categoryIdsAsJson = categoryIds,
stageNamesAsJson = stageNames,
@@ -336,13 +288,7 @@ fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
navController = navController
)
}
composable(
route = "vocabulary_exercise/{dailyOnly}?",
arguments = listOf(
navArgument("dailyOnly") { type = NavType.BoolType },
)
) { _ ->
composable("vocabulary_exercise/{dailyOnly}?", arguments = listOf(navArgument("dailyOnly") { type = NavType.BoolType })) { _ ->
VocabularyExerciseHostScreen(
categoryIdsAsJson = null,
stageNamesAsJson = null,
@@ -352,34 +298,18 @@ fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
dailyOnlyAsJson = "{\"dailyOnly\": true}"
)
}
composable(
"stage_detail/{stage}",
arguments = listOf(
navArgument("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("stage_detail/{stage}", arguments = listOf(navArgument("stage") { type = NavType.EnumType(VocabularyStage::class.java) })) { backStackEntry ->
@Suppress("DEPRECATION")
val stage = backStackEntry.arguments?.getSerializable("stage") as VocabularyStage
StageDetailScreen(navController = navController, stage = stage)
}
composable("category_detail/{categoryId}") { backStackEntry ->
val categoryId = backStackEntry.arguments?.getString("categoryId")?.toIntOrNull()
if (categoryId != null) {
CategoryDetailScreen(
categoryId = categoryId,
onBackClick = { navController.popBackStack() },
onNavigateToItem = { item ->
navController.navigate("vocabulary_detail/${item.id}")
},
onNavigateToItem = { item -> navController.navigate("vocabulary_detail/${item.id}") },
navController = navController
)
}
@@ -387,37 +317,22 @@ fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
composable("category_list_screen") {
CategoryListScreen(
onNavigateBack = { navController.popBackStack() },
onCategoryClicked = { categoryId ->
navController.navigate("category_detail/$categoryId")
}
onCategoryClicked = { categoryId -> navController.navigate("category_detail/$categoryId") }
)
}
composable(
route = "vocabulary_sorting?mode={mode}", // Route now accepts an optional 'mode'
arguments = listOf(
navArgument("mode") { // Define the argument
type = NavType.StringType
nullable = true
}
)
) { backStackEntry ->
composable("vocabulary_sorting?mode={mode}", arguments = listOf(navArgument("mode") { type = NavType.StringType; nullable = true })) { backStackEntry ->
VocabularySortingScreen(
navController = navController,
// Pass the argument to the screen
initialFilterMode = backStackEntry.arguments?.getString("mode")
)
}
composable("no_grammar_items") {
NoGrammarItemsScreen(
navController = navController
)
NoGrammarItemsScreen(navController = navController)
}
}
}
fun NavGraphBuilder.statsGraph(
navController: NavHostController,
) {
fun NavGraphBuilder.statsGraph(navController: NavHostController) {
navigation(
startDestination = "main_stats",
route = Screen.Stats.route
@@ -426,9 +341,7 @@ fun NavGraphBuilder.statsGraph(
StatsScreen(navController = navController)
}
composable("stats/vocabulary_sorting") {
VocabularySortingScreen(
navController = navController
)
VocabularySortingScreen(navController = navController)
}
composable("stats/vocabulary_list/{showDueTodayOnly}/{categoryId}") { backStackEntry ->
val showDueTodayOnly = backStackEntry.arguments?.getString("showDueTodayOnly")?.toBoolean() ?: false
@@ -437,22 +350,16 @@ fun NavGraphBuilder.statsGraph(
navController = navController,
showDueTodayOnly = showDueTodayOnly,
categoryId = categoryId,
onNavigateToItem = { item ->
navController.navigate("vocabulary_detail/${item.id}")
},
onNavigateToItem = { item -> navController.navigate("vocabulary_detail/${item.id}") },
onNavigateBack = { navController.popBackStack() },
enableNavigationButtons = true
)
}
composable(NavigationRoutes.STATS_LANGUAGE_PROGRESS) {
LanguageJourneyScreen(
navController = navController
)
LanguageJourneyScreen(navController = navController)
}
composable(NavigationRoutes.STATS_VOCABULARY_HEATMAP) {
VocabularyHeatmapScreen(
navController = navController,
)
VocabularyHeatmapScreen(navController = navController)
}
composable("stats/vocabulary_list/{showDueTodayOnly}/{stage}") { backStackEntry ->
val showDueTodayOnly = backStackEntry.arguments?.getString("showDueTodayOnly")?.toBoolean() ?: false
@@ -460,14 +367,11 @@ fun NavGraphBuilder.statsGraph(
val stage = stageString?.let {
if (it.equals("null", ignoreCase = true)) null else VocabularyStage.valueOf(it)
}
AllCardsListScreen(
navController = navController,
showDueTodayOnly = showDueTodayOnly,
stage = stage,
onNavigateToItem = { item ->
navController.navigate("vocabulary_detail/${item.id}")
},
onNavigateToItem = { item -> navController.navigate("vocabulary_detail/${item.id}") },
onNavigateBack = { navController.popBackStack() },
categoryId = 0,
enableNavigationButtons = true
@@ -475,14 +379,11 @@ fun NavGraphBuilder.statsGraph(
}
composable("stats/category_detail/{categoryId}") { backStackEntry ->
val categoryId = backStackEntry.arguments?.getString("categoryId")?.toIntOrNull()
if (categoryId != null) {
CategoryDetailScreen(
categoryId = categoryId,
onBackClick = { navController.popBackStack() },
onNavigateToItem = { item ->
navController.navigate("vocabulary_detail/${item.id}")
},
onNavigateToItem = { item -> navController.navigate("vocabulary_detail/${item.id}") },
navController = navController
)
}
@@ -490,29 +391,14 @@ fun NavGraphBuilder.statsGraph(
composable("stats/category_list_screen") {
CategoryListScreen(
onNavigateBack = { navController.popBackStack() },
onCategoryClicked = { categoryId ->
navController.navigate("stats/category_detail/$categoryId")
}
onCategoryClicked = { categoryId -> navController.navigate("stats/category_detail/$categoryId") }
)
}
composable(
route = "stats/vocabulary_sorting?mode={mode}",
arguments = listOf(
navArgument("mode") {
type = NavType.StringType
nullable = true
}
)
) { backStackEntry ->
VocabularySortingScreen(
navController = navController,
initialFilterMode = backStackEntry.arguments?.getString("mode")
)
composable("stats/vocabulary_sorting?mode={mode}", arguments = listOf(navArgument("mode") { type = NavType.StringType; nullable = true })) { backStackEntry ->
VocabularySortingScreen(navController = navController, initialFilterMode = backStackEntry.arguments?.getString("mode"))
}
composable("stats/no_grammar_items") {
NoGrammarItemsScreen(
navController = navController
)
NoGrammarItemsScreen(navController = navController)
}
}
}
@@ -539,15 +425,16 @@ fun NavGraphBuilder.dictionaryGraph(navController: NavHostController) {
route = Screen.Dictionary.route
) {
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 ->
val entryId = backStackEntry.arguments?.getString("entryId")?.toIntOrNull()
if (entryId != null) {
DictionaryResultScreen(
entryId = entryId,
navController = navController,
)
DictionaryResultScreen(entryId = entryId, navController = navController)
} else {
Text("Error: Invalid Entry ID")
}
@@ -558,43 +445,39 @@ fun NavGraphBuilder.dictionaryGraph(navController: NavHostController) {
composable("etymology_result/{word}/{languageCode}") { backStackEntry ->
val word = backStackEntry.arguments?.getString("word") ?: ""
val languageCode = backStackEntry.arguments?.getString("languageCode")?.toIntOrNull() ?: 1
EtymologyResultScreen(
navController = navController,
word = word,
languageCode = languageCode
)
EtymologyResultScreen(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)
fun NavGraphBuilder.exerciseGraph(
navController: NavHostController,
) {
fun NavGraphBuilder.exerciseGraph(navController: NavHostController) {
navigation(
startDestination = "main_exercise",
route = Screen.Exercises.route
) {
composable("main_exercise") {
MainExerciseScreen(
navController = navController,
)
MainExerciseScreen(navController = navController)
}
composable("exercise_session") {
ExerciseSessionScreen(
navController = navController,
)
ExerciseSessionScreen(navController = navController)
}
composable("youtube_exercise") {
YouTubeExerciseScreen(
navController = navController
)
YouTubeExerciseScreen(navController = navController)
}
composable("youtube_browse") {
YouTubeBrowserScreen(
navController = navController,
)
YouTubeBrowserScreen(navController = navController)
}
}
}

View File

@@ -11,6 +11,7 @@ import androidx.compose.animation.core.spring
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
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 More : Screen("more", R.string.label_more, AppIcons.MoreVert, AppIcons.MoreHorizontal)
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)
companion object {
@@ -88,6 +90,7 @@ sealed class Screen(
val items = mutableListOf<Screen>()
items.add(Translation)
items.add(Dictionary)
items.add(Corrector)
items.add(Settings)
if (showExperimental) {
items.add(Exercises)
@@ -258,7 +261,7 @@ fun BottomNavigationBar(
.background(
brush = Brush.radialGradient(
colors = listOf(
MaterialTheme.colorScheme.primary.copy(alpha = 0.5f),
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f),
Color.Transparent
)
),
@@ -271,6 +274,12 @@ fun BottomNavigationBar(
modifier = Modifier
.size(playButtonSize)
.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)
.clickable {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
@@ -281,7 +290,7 @@ fun BottomNavigationBar(
Icon(
imageVector = Icons.Filled.PlayArrow,
contentDescription = "Play",
tint = Color.White,
tint = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier.size(32.dp)
)
}

View File

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

View File

@@ -1,12 +1,16 @@
package eu.gaudian.translator.view.dictionary
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
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.utils.findActivity
import eu.gaudian.translator.view.composable.AppTopAppBar
import eu.gaudian.translator.viewmodel.DictionaryViewModel
import eu.gaudian.translator.viewmodel.LanguageViewModel
@@ -20,6 +24,11 @@ fun DictionaryScreen(
val dictionaryViewModel: DictionaryViewModel = 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
DictionaryScreenContent(
@@ -29,6 +38,7 @@ fun DictionaryScreen(
languageViewModel = languageViewModel,
onNavigateToOptions = onNavigateToOptions
)
}
}
@Preview

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 -->
<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_corrector">Corrector</string>
<string name="label_correction">Correction</string>
</resources>