diff --git a/app/src/main/java/eu/gaudian/translator/model/WidgetType.kt b/app/src/main/java/eu/gaudian/translator/model/WidgetType.kt index 7382afd..4e65825 100644 --- a/app/src/main/java/eu/gaudian/translator/model/WidgetType.kt +++ b/app/src/main/java/eu/gaudian/translator/model/WidgetType.kt @@ -9,7 +9,6 @@ import eu.gaudian.translator.R sealed class WidgetType(val id: String, @param:StringRes val titleRes: Int) { data object Status : WidgetType("status", R.string.label_status) data object Streak : WidgetType("streak", R.string.title_widget_streak) - data object StartButtons : WidgetType("start_buttons", R.string.label_start_exercise) data object AllVocabulary : WidgetType("all_vocabulary", R.string.label_all_vocabulary) data object DueToday : WidgetType("due_today", R.string.title_widget_due_today) data object CategoryProgress : WidgetType("category_progress", R.string.label_categories) @@ -23,7 +22,6 @@ sealed class WidgetType(val id: String, @param:StringRes val titleRes: Int) { val DEFAULT_ORDER = listOf( Status, Streak, - StartButtons, AllVocabulary, DueToday, CategoryProgress , diff --git a/app/src/main/java/eu/gaudian/translator/view/MainActivity.kt b/app/src/main/java/eu/gaudian/translator/view/MainActivity.kt index 7535089..2082710 100644 --- a/app/src/main/java/eu/gaudian/translator/view/MainActivity.kt +++ b/app/src/main/java/eu/gaudian/translator/view/MainActivity.kt @@ -256,12 +256,16 @@ fun TranslatorApp( val isBottomBarHidden = currentDestination?.hierarchy?.any { destination -> destination.route in setOf( Screen.Translation.route, - Screen.Vocabulary.route, Screen.Dictionary.route, Screen.Exercises.route, Screen.Settings.route ) - } == true || currentDestination?.route == "start_exercise" + } == true || currentDestination?.route in setOf( + "start_exercise", + "new_word", + "new_word_review", + "vocabulary_detail/{itemId}" + ) val showBottomNavLabels by settingsViewModel.showBottomNavLabels.collectAsStateWithLifecycle(initialValue = false) BottomNavigationBar( @@ -272,7 +276,6 @@ fun TranslatorApp( val inSameSection = currentDestination?.hierarchy?.any { it.route == screen.route } == true val isMoreSection = screen in setOf( Screen.Translation, - Screen.Vocabulary, Screen.Dictionary, Screen.Settings, Screen.Exercises diff --git a/app/src/main/java/eu/gaudian/translator/view/Navigation.kt b/app/src/main/java/eu/gaudian/translator/view/Navigation.kt index bd6abeb..884f66e 100644 --- a/app/src/main/java/eu/gaudian/translator/view/Navigation.kt +++ b/app/src/main/java/eu/gaudian/translator/view/Navigation.kt @@ -40,7 +40,6 @@ import eu.gaudian.translator.view.translation.TranslationScreen import eu.gaudian.translator.view.vocabulary.CategoryDetailScreen import eu.gaudian.translator.view.vocabulary.CategoryListScreen import eu.gaudian.translator.view.vocabulary.LanguageProgressScreen -import eu.gaudian.translator.view.vocabulary.MainVocabularyScreen import eu.gaudian.translator.view.vocabulary.NewWordReviewScreen import eu.gaudian.translator.view.vocabulary.NewWordScreen import eu.gaudian.translator.view.vocabulary.NoGrammarItemsScreen @@ -67,7 +66,6 @@ fun AppNavHost( Screen.Library.route, Screen.Stats.route, Screen.Translation.route, - Screen.Vocabulary.route, Screen.Dictionary.route, Screen.Exercises.route, SettingsRoutes.LIST @@ -132,8 +130,12 @@ fun AppNavHost( HomeScreen(navController = navController) } - composable(Screen.Library.route) { - LibraryScreen(navController = navController) + composable("new_word") { + NewWordScreen(navController = navController) + } + + composable("new_word_review") { + NewWordReviewScreen(navController = navController) } composable("start_exercise") { @@ -142,10 +144,10 @@ fun AppNavHost( // Define all other navigation graphs at the same top level. homeGraph(navController) + libraryGraph(navController) statsGraph(navController) translationGraph(navController) dictionaryGraph(navController) - vocabularyGraph(navController) exerciseGraph(navController) settingsGraph(navController) } @@ -159,177 +161,16 @@ fun NavGraphBuilder.homeGraph(navController: NavHostController) { composable("main_home") { HomeScreen(navController = navController) } - composable("new_word") { - NewWordScreen(navController = navController) - } - composable("new_word_review") { - NewWordReviewScreen(navController = navController) - } } } -fun NavGraphBuilder.statsGraph( - navController: NavHostController, -) { +fun NavGraphBuilder.libraryGraph(navController: NavHostController) { navigation( - startDestination = "main_stats", - route = Screen.Stats.route + startDestination = "main_library", + route = Screen.Library.route ) { - composable("main_stats") { - StatsScreen(navController = navController) - } - composable("stats/vocabulary_sorting") { - VocabularySortingScreen( - navController = navController - ) - } - composable("stats/vocabulary_list/{showDueTodayOnly}/{categoryId}") { backStackEntry -> - val showDueTodayOnly = backStackEntry.arguments?.getString("showDueTodayOnly")?.toBoolean() ?: false - val categoryId = backStackEntry.arguments?.getString("categoryId")?.toIntOrNull() - VocabularyListScreen( - navController = navController, - showDueTodayOnly = showDueTodayOnly, - categoryId = categoryId, - onNavigateToItem = { item -> - navController.navigate("vocabulary_detail/${item.id}") - }, - onNavigateBack = { navController.popBackStack() }, - enableNavigationButtons = true - ) - } - composable("stats/language_progress") { - LanguageProgressScreen( - navController = navController - ) - } - composable("stats/vocabulary_heatmap") { - VocabularyHeatmapScreen( - navController = navController, - ) - } - composable("stats/vocabulary_list/{showDueTodayOnly}/{stage}") { backStackEntry -> - val showDueTodayOnly = backStackEntry.arguments?.getString("showDueTodayOnly")?.toBoolean() ?: false - val stageString = backStackEntry.arguments?.getString("stage") - val stage = stageString?.let { - if (it.equals("null", ignoreCase = true)) null else VocabularyStage.valueOf(it) - } - - VocabularyListScreen( - navController = navController, - showDueTodayOnly = showDueTodayOnly, - stage = stage, - onNavigateToItem = { item -> - navController.navigate("vocabulary_detail/${item.id}") - }, - onNavigateBack = { navController.popBackStack() }, - categoryId = 0, - enableNavigationButtons = true - ) - } - 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}") - }, - navController = navController - ) - } - } - composable("stats/category_list_screen") { - CategoryListScreen( - onNavigateBack = { navController.popBackStack() }, - 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/no_grammar_items") { - NoGrammarItemsScreen( - navController = navController - ) - } - } -} - -@OptIn(androidx.compose.animation.ExperimentalAnimationApi::class) -fun NavGraphBuilder.translationGraph(navController: NavHostController) { - navigation( - startDestination = "main_translation", - route = Screen.Translation.route - ) { - composable("main_translation") { - TranslationScreen(navController = navController) - } - composable("custom_translation_prompt") { - TranslationSettingsScreen(navController = navController) - } - } -} - -@OptIn(androidx.compose.animation.ExperimentalAnimationApi::class) -fun NavGraphBuilder.dictionaryGraph(navController: NavHostController) { - navigation( - startDestination = "main_dictionary", - route = Screen.Dictionary.route - ) { - composable("main_dictionary") { - MainDictionaryScreen(navController = navController) - } - composable("dictionary_result/{entryId}") { backStackEntry -> - val entryId = backStackEntry.arguments?.getString("entryId")?.toIntOrNull() - if (entryId != null) { - DictionaryResultScreen( - entryId = entryId, - navController = navController, - ) - } else { - Text("Error: Invalid Entry ID") - } - } - composable("dictionary_options") { - DictionaryOptionsScreen(navController = navController) - } - 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 - ) - } - - } -} - -fun NavGraphBuilder.vocabularyGraph( - navController: NavHostController, -) { - navigation( - startDestination = "main_vocabulary", - route = Screen.Vocabulary.route - ) { - composable("main_vocabulary") { - MainVocabularyScreen(navController = navController) + composable("main_library") { + LibraryScreen(navController = navController) } composable("vocabulary_sorting") { VocabularySortingScreen( @@ -514,6 +355,159 @@ fun NavGraphBuilder.vocabularyGraph( } } +fun NavGraphBuilder.statsGraph( + navController: NavHostController, +) { + navigation( + startDestination = "main_stats", + route = Screen.Stats.route + ) { + composable("main_stats") { + StatsScreen(navController = navController) + } + composable("stats/vocabulary_sorting") { + VocabularySortingScreen( + navController = navController + ) + } + composable("stats/vocabulary_list/{showDueTodayOnly}/{categoryId}") { backStackEntry -> + val showDueTodayOnly = backStackEntry.arguments?.getString("showDueTodayOnly")?.toBoolean() ?: false + val categoryId = backStackEntry.arguments?.getString("categoryId")?.toIntOrNull() + VocabularyListScreen( + navController = navController, + showDueTodayOnly = showDueTodayOnly, + categoryId = categoryId, + onNavigateToItem = { item -> + navController.navigate("vocabulary_detail/${item.id}") + }, + onNavigateBack = { navController.popBackStack() }, + enableNavigationButtons = true + ) + } + composable("stats/language_progress") { + LanguageProgressScreen( + navController = navController + ) + } + composable("stats/vocabulary_heatmap") { + VocabularyHeatmapScreen( + navController = navController, + ) + } + composable("stats/vocabulary_list/{showDueTodayOnly}/{stage}") { backStackEntry -> + val showDueTodayOnly = backStackEntry.arguments?.getString("showDueTodayOnly")?.toBoolean() ?: false + val stageString = backStackEntry.arguments?.getString("stage") + val stage = stageString?.let { + if (it.equals("null", ignoreCase = true)) null else VocabularyStage.valueOf(it) + } + + VocabularyListScreen( + navController = navController, + showDueTodayOnly = showDueTodayOnly, + stage = stage, + onNavigateToItem = { item -> + navController.navigate("vocabulary_detail/${item.id}") + }, + onNavigateBack = { navController.popBackStack() }, + categoryId = 0, + enableNavigationButtons = true + ) + } + 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}") + }, + navController = navController + ) + } + } + composable("stats/category_list_screen") { + CategoryListScreen( + onNavigateBack = { navController.popBackStack() }, + 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/no_grammar_items") { + NoGrammarItemsScreen( + navController = navController + ) + } + } +} + +@OptIn(androidx.compose.animation.ExperimentalAnimationApi::class) +fun NavGraphBuilder.translationGraph(navController: NavHostController) { + navigation( + startDestination = "main_translation", + route = Screen.Translation.route + ) { + composable("main_translation") { + TranslationScreen(navController = navController) + } + composable("custom_translation_prompt") { + TranslationSettingsScreen(navController = navController) + } + } +} + +@OptIn(androidx.compose.animation.ExperimentalAnimationApi::class) +fun NavGraphBuilder.dictionaryGraph(navController: NavHostController) { + navigation( + startDestination = "main_dictionary", + route = Screen.Dictionary.route + ) { + composable("main_dictionary") { + MainDictionaryScreen(navController = navController) + } + composable("dictionary_result/{entryId}") { backStackEntry -> + val entryId = backStackEntry.arguments?.getString("entryId")?.toIntOrNull() + if (entryId != null) { + DictionaryResultScreen( + entryId = entryId, + navController = navController, + ) + } else { + Text("Error: Invalid Entry ID") + } + } + composable("dictionary_options") { + DictionaryOptionsScreen(navController = navController) + } + 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 + ) + } + + } +} + @OptIn(androidx.compose.animation.ExperimentalAnimationApi::class) fun NavGraphBuilder.exerciseGraph( navController: NavHostController, diff --git a/app/src/main/java/eu/gaudian/translator/view/composable/BottomNavigationBar.kt b/app/src/main/java/eu/gaudian/translator/view/composable/BottomNavigationBar.kt index 2a4b5ae..1e8a290 100644 --- a/app/src/main/java/eu/gaudian/translator/view/composable/BottomNavigationBar.kt +++ b/app/src/main/java/eu/gaudian/translator/view/composable/BottomNavigationBar.kt @@ -74,7 +74,6 @@ sealed class Screen( object Stats : Screen("stats", R.string.label_stats, AppIcons.Statistics, AppIcons.Statistics) object Translation : Screen("translation", R.string.label_translation, AppIcons.TranslateFilled, AppIcons.TranslateOutlined) object Library : Screen("library", R.string.label_library, AppIcons.VocabularyFilled, AppIcons.VocabularyOutlined) - object Vocabulary : Screen("vocabulary", R.string.label_legacy_vocabulary, AppIcons.VocabularyFilled, AppIcons.VocabularyOutlined) 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) @@ -88,7 +87,6 @@ sealed class Screen( fun getMoreMenuItems(showExperimental: Boolean = false): List { val items = mutableListOf() items.add(Translation) - items.add(Vocabulary) items.add(Dictionary) items.add(Settings) if (showExperimental) { diff --git a/app/src/main/java/eu/gaudian/translator/view/dialogs/ImportVocabularyDialog.kt b/app/src/main/java/eu/gaudian/translator/view/dialogs/ImportVocabularyDialog.kt index 0d0e4b4..e69de29 100644 --- a/app/src/main/java/eu/gaudian/translator/view/dialogs/ImportVocabularyDialog.kt +++ b/app/src/main/java/eu/gaudian/translator/view/dialogs/ImportVocabularyDialog.kt @@ -1,219 +0,0 @@ -package eu.gaudian.translator.view.dialogs - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringArrayResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.navigation.NavController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import eu.gaudian.translator.R -import eu.gaudian.translator.utils.findActivity -import eu.gaudian.translator.view.composable.AppDialog -import eu.gaudian.translator.view.composable.AppSlider -import eu.gaudian.translator.view.composable.DialogButton -import eu.gaudian.translator.view.composable.InspiringSearchField -import eu.gaudian.translator.view.composable.SourceLanguageDropdown -import eu.gaudian.translator.view.composable.TargetLanguageDropdown -import eu.gaudian.translator.view.hints.HintDefinition -import eu.gaudian.translator.viewmodel.LanguageViewModel -import eu.gaudian.translator.viewmodel.VocabularyViewModel -import kotlinx.coroutines.launch - -@Composable -fun ImportVocabularyDialog( - onDismiss: () -> Unit, - languageViewModel: LanguageViewModel, - vocabularyViewModel : VocabularyViewModel, - optionalDescription: String? = null, - optionalSearchTerm: String? = null -) { - - val navController = rememberNavController() - NavHost(navController = navController, startDestination = "import") { - composable("import") { - ImportDialogContent( - navController = navController, - onDismiss = onDismiss, - languageViewModel = languageViewModel, - optionalDescription = optionalDescription, - optionalSearchTerm = optionalSearchTerm - ) - } - @Suppress("HardCodedStringLiteral") - composable("review") { - Dialog( - onDismissRequest = onDismiss, - properties = DialogProperties(usePlatformDefaultWidth = false) - ) { - // Full-screen surface to ensure the dialog covers content and stays above the main FAB/menu - Surface(modifier = Modifier.fillMaxSize()) { - VocabularyReviewScreen( - onConfirm = { selectedItems, categoryIds -> - vocabularyViewModel.addVocabularyItems(selectedItems, categoryIds) - onDismiss() - }, - onCancel = onDismiss - ) - } - } - } - } -} - -@Composable -fun ImportDialogContent( - navController: NavController, - onDismiss: () -> Unit, - languageViewModel: LanguageViewModel, - optionalDescription: String? = null, - optionalSearchTerm: String? = null -) { - val activity = LocalContext.current.findActivity() - val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity) - var category by remember { mutableStateOf(optionalSearchTerm ?: "") } - var amount by remember { mutableFloatStateOf(1f) } - val coroutineScope = rememberCoroutineScope() - val descriptionText = optionalDescription ?: stringResource(R.string.text_let_ai_find_vocabulary_for_you) - val isGenerating by vocabularyViewModel.isGenerating.collectAsState() - - AppDialog( - onDismissRequest = onDismiss, - title = { Text(descriptionText) }, - hintContent = HintDefinition.VOCABULARY_GENERATE_AI.hint(), - content = { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(4.dp) - ) { - if (isGenerating) { - Box( - modifier = Modifier.fillMaxWidth().padding(16.dp), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } - } else { - Text( - text = stringResource(R.string.text_search_term), - style = MaterialTheme.typography.labelLarge, - modifier = Modifier.padding(bottom = 8.dp) - ) - // Modern rotating field using XML resource array - InspiringSearchField( - value = category, - hints = stringArrayResource(R.array.vocabulary_hints), - onValueChange = { category = it } - ) - - // The "Dica" string has been removed to keep the interface clean - - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = stringResource(R.string.text_select_languages), - style = MaterialTheme.typography.labelLarge, - modifier = Modifier.padding(bottom = 8.dp) - ) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(1.dp) - ) { - SourceLanguageDropdown(languageViewModel = languageViewModel) - } - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(1.dp) - ) { - TargetLanguageDropdown(languageViewModel = languageViewModel) - } - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = stringResource(R.string.text_select_amount), - style = MaterialTheme.typography.labelLarge, - modifier = Modifier.padding(bottom = 8.dp) - ) - AppSlider( - value = amount, - onValueChange = { amount = it }, - valueRange = 1f..25f, - steps = 24, - modifier = Modifier.fillMaxWidth() - ) - Text( - text = stringResource(R.string.text_amount_2d, amount.toInt()), - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.fillMaxWidth().padding(top = 8.dp) - ) - Spacer(modifier = Modifier.height(8.dp)) - } - - Spacer(modifier = Modifier.height(16.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.End - ) { - DialogButton( - onClick = onDismiss, - content = { Text(stringResource(R.string.label_cancel)) } - ) - if (category.isNotBlank() && !isGenerating) { - Spacer(modifier = Modifier.width(8.dp)) - DialogButton(onClick = { - coroutineScope.launch { - vocabularyViewModel.generateVocabularyItems(category, amount.toInt()) - @Suppress("HardCodedStringLiteral") - navController.navigate("review") - } - }) { Text(stringResource(R.string.text_generate)) } - } - } - } - } - ) -} - -@Suppress("HardCodedStringLiteral") -@Preview -@Composable -fun ImportDialogContentPreview() { - val activity = LocalContext.current.findActivity() - val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity) - - ImportDialogContent( - navController = rememberNavController(), - onDismiss = {}, - languageViewModel = languageViewModel, - optionalDescription = "Let AI find vocabulary for you", - optionalSearchTerm = "Travel" - ) -} diff --git a/app/src/main/java/eu/gaudian/translator/view/dialogs/VocabularyMenu.kt b/app/src/main/java/eu/gaudian/translator/view/dialogs/VocabularyMenu.kt index 99743d4..e69de29 100644 --- a/app/src/main/java/eu/gaudian/translator/view/dialogs/VocabularyMenu.kt +++ b/app/src/main/java/eu/gaudian/translator/view/dialogs/VocabularyMenu.kt @@ -1,73 +0,0 @@ -@file:Suppress("AssignedValueIsNeverRead") - -package eu.gaudian.translator.view.dialogs - -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.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import eu.gaudian.translator.R -import eu.gaudian.translator.utils.findActivity -import eu.gaudian.translator.view.composable.AppFabMenu -import eu.gaudian.translator.view.composable.AppIcons -import eu.gaudian.translator.view.composable.FabMenuItem -import eu.gaudian.translator.viewmodel.LanguageViewModel -import eu.gaudian.translator.viewmodel.VocabularyViewModel - -@Composable -fun VocabularyMenu( - modifier: Modifier = Modifier, - showFabText : Boolean = true -) { - val activity = LocalContext.current.findActivity() - val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity) - val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity) - var showAddVocabularyDialog by remember { mutableStateOf(false) } - var showImportVocabularyDialog by remember { mutableStateOf(false) } - var showAddCategoryDialog by remember { mutableStateOf(false) } - - val menuItems = listOf( - FabMenuItem( - text = stringResource(R.string.label_add_vocabulary), - imageVector = AppIcons.Add, - onClick = { showAddVocabularyDialog = true } - ), - FabMenuItem( - text = stringResource(R.string.menu_import_vocabulary), - imageVector = AppIcons.AI, - onClick = { showImportVocabularyDialog = true } - ), - FabMenuItem( - text = stringResource(R.string.label_add_category), - imageVector = AppIcons.Add, - onClick = { showAddCategoryDialog = true } - ) - ) - - AppFabMenu(items = menuItems, modifier = modifier, title = stringResource(R.string.label_add_vocabulary), showFabText = showFabText) - - if (showAddVocabularyDialog) { - AddVocabularyDialog( - onDismissRequest = { showAddVocabularyDialog = false } - ) - } - - if (showImportVocabularyDialog) { - ImportVocabularyDialog( - languageViewModel = languageViewModel, - vocabularyViewModel = vocabularyViewModel, - onDismiss = { showImportVocabularyDialog = false } - ) - } - - if (showAddCategoryDialog) { - AddCategoryDialog( - onDismiss = { showAddCategoryDialog = false } - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/eu/gaudian/translator/view/home/HomeScreen.kt b/app/src/main/java/eu/gaudian/translator/view/home/HomeScreen.kt index beeba03..4d758f4 100644 --- a/app/src/main/java/eu/gaudian/translator/view/home/HomeScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/home/HomeScreen.kt @@ -17,12 +17,10 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AddCircle -import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.icons.filled.LocalFireDepartment import androidx.compose.material.icons.filled.Psychology import androidx.compose.material.icons.filled.Settings -import androidx.compose.material.icons.filled.TrendingUp import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -110,7 +108,7 @@ fun HomeScreen( ) } item { WeeklyProgressSection(navController = navController) } - item { BottomStatsSection() } + item { BottomStatsSection(navController = navController) } // Bottom padding for edge-to-edge screens item { Spacer(modifier = Modifier.height(24.dp)) } @@ -387,7 +385,14 @@ fun WeeklyProgressSection( } @Composable -fun BottomStatsSection() { +fun BottomStatsSection( + navController: NavHostController +) { + val activity = LocalContext.current.findActivity() + val viewModel: ProgressViewModel = hiltViewModel(activity) + val totalWords by viewModel.totalWords.collectAsState() + val learnedWords by viewModel.totalWordsCompleted.collectAsState() + Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp) @@ -395,34 +400,26 @@ fun BottomStatsSection() { // Total Words AppCard( modifier = Modifier.weight(1f), + 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)) Spacer(modifier = Modifier.height(8.dp)) - Text(text = "1,284", style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold) + Text(text = totalWords.toString(), style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold) Spacer(modifier = Modifier.height(8.dp)) - Row(verticalAlignment = Alignment.CenterVertically) { - Icon(Icons.Default.TrendingUp, contentDescription = null, tint = Color.Green, modifier = Modifier.size(16.dp)) - Spacer(modifier = Modifier.width(4.dp)) - Text(text = "+12 today", style = MaterialTheme.typography.labelSmall, color = Color.Green) - } } } - // Accuracy + // Learned AppCard( modifier = Modifier.weight(1f), + onClick = { navController.navigate("stats/language_progress") } ) { Column(modifier = Modifier.padding(20.dp)) { - Text(text = "ACCURACY", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)) + Text(text = "LEARNED", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)) Spacer(modifier = Modifier.height(8.dp)) - Text(text = "92%", style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold) + Text(text = learnedWords.toString(), style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold) Spacer(modifier = Modifier.height(8.dp)) - Row(verticalAlignment = Alignment.CenterVertically) { - Icon(Icons.Default.CheckCircle, contentDescription = null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(16.dp)) - Spacer(modifier = Modifier.width(4.dp)) - Text(text = "Master level", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.primary) - } } } } diff --git a/app/src/main/java/eu/gaudian/translator/view/stats/StatsScreen.kt b/app/src/main/java/eu/gaudian/translator/view/stats/StatsScreen.kt index c6482fb..8052251 100644 --- a/app/src/main/java/eu/gaudian/translator/view/stats/StatsScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/stats/StatsScreen.kt @@ -66,7 +66,6 @@ import eu.gaudian.translator.view.vocabulary.widgets.AllVocabularyWidget import eu.gaudian.translator.view.vocabulary.widgets.CategoryProgressWidget import eu.gaudian.translator.view.vocabulary.widgets.DueTodayWidget import eu.gaudian.translator.view.vocabulary.widgets.LevelWidget -import eu.gaudian.translator.view.vocabulary.widgets.ModernStartButtons import eu.gaudian.translator.view.vocabulary.widgets.StatusWidget import eu.gaudian.translator.view.vocabulary.widgets.StreakWidget import eu.gaudian.translator.view.vocabulary.widgets.WeeklyActivityChartWidget @@ -539,17 +538,7 @@ private fun LazyWidget( onMissingLanguage: (Int) -> Unit ) { when (widgetType) { - WidgetType.StartButtons -> ModernStartButtons( - onCustomClick = onShowCustomExerciseDialog, - onDailyClick = { isSpelling -> - if (isSpelling) { - onShowWordPairExerciseDialog() - } else { - startDailyExercise(true) - Log.d("DailyExercise", "Starting daily exercise") - } - } - ) + WidgetType.Status -> LazyStatusWidget( vocabularyViewModel = vocabularyViewModel, diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/DashboardContent.kt b/app/src/main/java/eu/gaudian/translator/view/vocabulary/DashboardContent.kt index b725b86..915c8d5 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/DashboardContent.kt +++ b/app/src/main/java/eu/gaudian/translator/view/vocabulary/DashboardContent.kt @@ -65,7 +65,6 @@ import eu.gaudian.translator.view.vocabulary.widgets.AllVocabularyWidget import eu.gaudian.translator.view.vocabulary.widgets.CategoryProgressWidget import eu.gaudian.translator.view.vocabulary.widgets.DueTodayWidget import eu.gaudian.translator.view.vocabulary.widgets.LevelWidget -import eu.gaudian.translator.view.vocabulary.widgets.ModernStartButtons import eu.gaudian.translator.view.vocabulary.widgets.StatusWidget import eu.gaudian.translator.view.vocabulary.widgets.StreakWidget import eu.gaudian.translator.view.vocabulary.widgets.WeeklyActivityChartWidget @@ -530,17 +529,7 @@ private fun LazyWidget( onMissingLanguage: (Int) -> Unit ) { when (widgetType) { - WidgetType.StartButtons -> ModernStartButtons( - onCustomClick = onShowCustomExerciseDialog, - onDailyClick = { isSpelling -> - if (isSpelling) { - onShowWordPairExerciseDialog() - } else { - startDailyExercise(true) - Log.d("DailyExercise", "Starting daily exercise") - } - } - ) + WidgetType.Status -> LazyStatusWidget( vocabularyViewModel = vocabularyViewModel, diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/MainVocabularyScreen.kt b/app/src/main/java/eu/gaudian/translator/view/vocabulary/MainVocabularyScreen.kt deleted file mode 100644 index c951851..0000000 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/MainVocabularyScreen.kt +++ /dev/null @@ -1,483 +0,0 @@ -@file:Suppress("HardCodedStringLiteral", "AssignedValueIsNeverRead") - -package eu.gaudian.translator.view.vocabulary - -import androidx.activity.ComponentActivity -import androidx.activity.compose.LocalActivity -import androidx.compose.animation.animateContentSize -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -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 -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.navigation.NavController -import androidx.navigation.NavGraph.Companion.findStartDestination -import androidx.navigation.NavHostController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.currentBackStackEntryAsState -import androidx.navigation.compose.rememberNavController -import eu.gaudian.translator.R -import eu.gaudian.translator.model.Exercise -import eu.gaudian.translator.model.MatchingPairsQuestion -import eu.gaudian.translator.model.Question -import eu.gaudian.translator.model.VocabularyCategory -import eu.gaudian.translator.model.VocabularyStage -import eu.gaudian.translator.ui.theme.ThemePreviews -import eu.gaudian.translator.utils.Log -import eu.gaudian.translator.view.composable.AppDialog -import eu.gaudian.translator.view.composable.AppIcons -import eu.gaudian.translator.view.composable.AppOutlinedCard -import eu.gaudian.translator.view.composable.AppSlider -import eu.gaudian.translator.view.composable.AppTabLayout -import eu.gaudian.translator.view.composable.OptionItemSwitch -import eu.gaudian.translator.view.composable.Screen -import eu.gaudian.translator.view.composable.TabItem -import eu.gaudian.translator.view.dialogs.StartExerciseDialog -import eu.gaudian.translator.view.dialogs.VocabularyMenu -import eu.gaudian.translator.viewmodel.ExerciseViewModel -import eu.gaudian.translator.viewmodel.VocabularyViewModel -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch - - -@Suppress("HardCodedStringLiteral") -enum class VocabularyTab( - override val title: String, - override val icon: ImageVector, - val route: String -) : TabItem { - Dashboard(title = "title_dashboard", icon = AppIcons.Dashboard, route = "dashboard"), - Statistics(title = "label_all_vocabulary", icon = AppIcons.BarChart, route = "statistics") -} - -@Suppress("unused", "HardCodedStringLiteral", "UnusedVariable") -@Composable -fun Dummy() { - - val dummy = listOf( - stringResource(id = R.string.title_dashboard), - stringResource(id = R.string.label_all_vocabulary), - ) -} - -@Composable -fun MainVocabularyScreen( - navController: NavController -) { - - val activity = LocalActivity.current as ComponentActivity - val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity) - val exerciseViewModel: ExerciseViewModel = hiltViewModel(activity) - val vocabularyNavController = rememberNavController() - val coroutineScope = rememberCoroutineScope() - var showCustomExerciseDialog by remember { mutableStateOf(false) } - var startDailyExercise by remember { mutableStateOf(false) } - var showWordPairExerciseDialog by remember { mutableStateOf(false) } - - // Word Pair settings and temporary selections - var showWordPairSettingsDialog by remember { mutableStateOf(false) } - var tempWpCategories by remember { mutableStateOf>(emptyList()) } - var tempWpStages by remember { mutableStateOf>(emptyList()) } - var tempWpLanguageIds by remember { mutableStateOf>(emptyList()) } - - var wpQuestionCount by remember { mutableIntStateOf(5) } - var wpShuffleQuestions by remember { mutableStateOf(true) } - var wpShuffleWordOrder by remember { mutableStateOf(true) } - var wpTrainingMode by remember { mutableStateOf(false) } - var wpDueTodayOnly by remember { mutableStateOf(false) } - - var isScrolling by remember { mutableStateOf(false) } - - - if (showCustomExerciseDialog) { - StartExerciseDialog( - onDismiss = { showCustomExerciseDialog = false }, - onConfirm = { categories, stages, languageIds -> - showCustomExerciseDialog = false - val categoryIds = categories.joinToString(",") { it.id.toString() } - val stageNames = stages.joinToString(",") { it.name } - val languageIdsStr = languageIds.joinToString(",") { it.toString() } - @Suppress("HardCodedStringLiteral") - navController.navigate("vocabulary_exercise/false?categories=$categoryIds&stages=$stageNames&languages=$languageIdsStr") - } - ) - } - - if (showWordPairExerciseDialog) { - StartExerciseDialog( - onDismiss = { showWordPairExerciseDialog = false }, - onConfirm = { categories, stages, languageIds -> - // Store selections and open settings dialog instead of starting immediately - tempWpCategories = categories - tempWpStages = stages - tempWpLanguageIds = languageIds - showWordPairExerciseDialog = false - showWordPairSettingsDialog = true - } - ) - } - - val textWordPairSettings = stringResource(R.string.text_word_pair_settings) - - // Settings dialog for Word Pair Exercise - if (showWordPairSettingsDialog) { - AppDialog( - onDismissRequest = { showWordPairSettingsDialog = false }, - title = { Text(textWordPairSettings) } - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - // Amount of questions - Text( - stringResource( - R.string.text_amount_of_questions_2d, - wpQuestionCount - )) - AppSlider( - value = wpQuestionCount.toFloat(), - onValueChange = { wpQuestionCount = it.toInt().coerceIn(1, 20) }, - valueRange = 1f..20f, - steps = 18 - ) - - // Toggles - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - OptionItemSwitch( - title = stringResource(R.string.text_shuffle_questions), - checked = wpShuffleQuestions, - onCheckedChange = { wpShuffleQuestions = it }, - ) - } - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - OptionItemSwitch( - title = stringResource(R.string.text_shuffle_card_order), - description = stringResource(R.string.text_swap_sides), - checked = wpShuffleWordOrder, - onCheckedChange = { wpShuffleWordOrder = it }, - ) - } - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - OptionItemSwitch( - title = stringResource(R.string.tetx_training_mode), - description = stringResource(R.string.text_no_progress), - checked = wpTrainingMode, - onCheckedChange = { wpTrainingMode = it }, - ) - } - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - OptionItemSwitch( - title = stringResource(R.string.text_due_today_only), - checked = wpDueTodayOnly, - onCheckedChange = { wpDueTodayOnly = it }, - ) - } - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp), - horizontalArrangement = Arrangement.End - ) { - TextButton(onClick = { showWordPairSettingsDialog = false }) { - Text(stringResource(id = R.string.label_cancel)) - } - val textMatchThePairs = stringResource(R.string.text_match_the_pairs) - val textWordPairExercise = stringResource(R.string.text_word_pair_exercise) - val textTrainingModeDescription = stringResource(R.string.text_training_mode_description) - val labelTrainingMode = stringResource(R.string.label_training_mode) - TextButton(onClick = { - showWordPairSettingsDialog = false - // Build a Word Pair Exercise using matching pairs from selected vocabulary with options - coroutineScope.launch { - val items = vocabularyViewModel.filterVocabularyItems( - languages = tempWpLanguageIds, - query = null, - categoryIds = tempWpCategories.map { it.id }, - stage = tempWpStages.firstOrNull(), - sortOrder = VocabularyViewModel.SortOrder.NEWEST_FIRST, - dueTodayOnly = wpDueTodayOnly - ).first() - - val maxPairsPerQuestion = 5 - var pairsList = items.mapNotNull { item -> - val k = item.wordFirst.trim() - val v = item.wordSecond.trim() - if (k.isNotBlank() && v.isNotBlank()) k to v else null - } - if (wpShuffleWordOrder) { - pairsList = pairsList.map { (a, b) -> if ((0..1).random() == 0) a to b else b to a } - } - if (pairsList.isEmpty()) return@launch - - - val shuffledPairs = if (wpShuffleQuestions) pairsList.shuffled() else pairsList - - val chunked = shuffledPairs.chunked(maxPairsPerQuestion) - val limitedChunks = chunked.take(wpQuestionCount) - val questions = mutableListOf() - var qId = 1 - limitedChunks.forEach { chunk -> - if (chunk.size >= 2) { - questions.add( - MatchingPairsQuestion( - id = qId++, - name = textMatchThePairs, - pairs = chunk.toMap() - ) - ) - } - } - if (questions.isEmpty()) return@launch - - @Suppress("HardCodedStringLiteral") val exercise = Exercise( - id = "wordpair-" + System.currentTimeMillis().toString(), - title = textWordPairExercise, - questions = questions.map { it.id }, - contextTitle = if (wpTrainingMode) labelTrainingMode else null, - contextText = if (wpTrainingMode) textTrainingModeDescription else null - ) - exerciseViewModel.startAdHocExercise(exercise, questions) - @Suppress("HardCodedStringLiteral") - navController.navigate("exercise_session") - } - }) { - Text(stringResource(id = R.string.label_start_exercise)) - } - } - } - } - } - - // Use LaunchedEffect to handle the navigation side effect - LaunchedEffect(startDailyExercise) { - if (startDailyExercise) { - @Suppress("HardCodedStringLiteral") - Log.d("DailyExercise", "Starting daily exercise") - @Suppress("HardCodedStringLiteral") - navController.navigate("vocabulary_exercise/false?categories=&stages=&languages=&dailyOnly=true") - startDailyExercise = false - } - } - - val navBackStackEntry by vocabularyNavController.currentBackStackEntryAsState() - val currentRoute = navBackStackEntry?.destination?.route - val selectedTab = remember(currentRoute) { - VocabularyTab.entries.find { it.route == currentRoute } ?: VocabularyTab.Dashboard - } - - val rawShowFabText = selectedTab == VocabularyTab.Dashboard && !isScrolling - var showFabText by remember { mutableStateOf(rawShowFabText) } - - LaunchedEffect(rawShowFabText) { - if (rawShowFabText) { - // Only delay when showing (true), hide immediately - kotlinx.coroutines.delay(2000) - showFabText = true - } else { - showFabText = false - } - } - - val repoEmpty = - vocabularyViewModel.vocabularyItems.collectAsState(initial = emptyList()).value.isEmpty() - - if (repoEmpty) { - NoVocabularyScreen() - return - } - - Box(modifier = Modifier.fillMaxSize()) { - Column(modifier = Modifier.fillMaxSize()) { - AppTabLayout( - tabs = VocabularyTab.entries, - selectedTab = selectedTab, - onTabSelected = { tab -> - vocabularyNavController.navigate(tab.route) { - popUpTo(vocabularyNavController.graph.findStartDestination().id) { - saveState = true - } - launchSingleTop = true - restoreState = true - } - }, - onNavigateBack = { - if (!navController.popBackStack()) { - navController.navigate(Screen.Home.route) { - launchSingleTop = true - restoreState = false - } - } - } - ) - - NavHost( - navController = vocabularyNavController, - startDestination = VocabularyTab.Dashboard.route, - modifier = Modifier.weight(1f) - ) { - composable(VocabularyTab.Dashboard.route) { - DashboardContent( - navController = navController, - onShowCustomExerciseDialog = { showCustomExerciseDialog = true }, - onNavigateToCategoryDetail = { categoryId -> - navController.navigate("category_detail/$categoryId") - }, - startDailyExercise = { startDailyExercise = true }, - onNavigateToCategoryList = { - navController.navigate("category_list_screen") - }, - onShowWordPairExerciseDialog = { showWordPairExerciseDialog = true }, - onScroll = { isScrolling = it } - ) - } - composable(VocabularyTab.Statistics.route) { - StatisticsContent(navController = navController) - } - composable("category_detail/{categoryId}") { backStackEntry -> - - val categoryId = backStackEntry.arguments?.getString("categoryId")?.toIntOrNull() - - if (categoryId != null) { - CategoryDetailScreen( - categoryId = categoryId, - onBackClick = { vocabularyNavController.popBackStack() }, - onNavigateToItem = { item -> - navController.navigate("vocabulary_detail/${item.id}") - }, - navController = navController as NavHostController - ) - } - } - composable("vocabulary_exercise/{isSpelling}") { backStackEntry -> - backStackEntry.arguments?.getString("isSpelling")?.toBooleanStrict() ?: false - VocabularyExerciseHostScreen( - categoryIdsAsJson = null, - stageNamesAsJson = null, - languageIdsAsJson = null, - onClose = { navController.popBackStack() }, - navController = navController, - dailyOnlyAsJson = null - ) - } - composable("vocabulary_exercise/{dailyOnly}") { backStackEntry -> - backStackEntry.arguments?.getString("dailyOnly")?.toBooleanStrict() ?: false - VocabularyExerciseHostScreen( - categoryIdsAsJson = null, - stageNamesAsJson = null, - languageIdsAsJson = null, - onClose = { navController.popBackStack() }, - navController = navController, - dailyOnlyAsJson = "{\"dailyOnly\": true}" - ) - } - } - } - - var menuHeightPx by remember { mutableIntStateOf(0) } - val density = LocalDensity.current - val menuHeightDp = (menuHeightPx / density.density).dp - val animatedBottomPadding by animateDpAsState(targetValue = 16.dp + 8.dp + menuHeightDp, label = "FBottomPadding") - - Column( - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(16.dp), - horizontalAlignment = Alignment.End - ) { - VocabularyMenu(modifier = Modifier.onSizeChanged { menuHeightPx = it.height }, showFabText = showFabText) - } - - // Place the FAB separately and animate its bottom padding based on the menu height - FloatingActionButton( - onClick = { showCustomExerciseDialog = true }, - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary, - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(end = 16.dp, bottom = animatedBottomPadding) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier - .padding(horizontal = 16.dp) - .animateContentSize() - ) { - Icon( - imageVector = AppIcons.Quiz, - contentDescription = null - ) - if(showFabText) { - Text( - text = stringResource(R.string.label_start_exercise), - style = MaterialTheme.typography.labelLarge - )} - } - } - } -} - -@Composable -fun StatisticsContent( - navController: NavController -) { - - AppOutlinedCard { - VocabularyListScreen( - categoryId = null, - showDueTodayOnly = false, - onNavigateToItem = { item -> - navController.navigate("vocabulary_detail/${item.id}") - }, - onNavigateBack = null, - navController = navController as NavHostController, - enableNavigationButtons = true - ) - } -} - -@ThemePreviews -@Composable -fun VocabularyDashboardScreenPreview() { - val navController = rememberNavController() - MainVocabularyScreen(navController = navController) -} - - -@ThemePreviews -@Composable -fun StatisticsContentPreview() { - val navController = rememberNavController() - StatisticsContent(navController = navController) -} \ No newline at end of file diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/NoVocabularyScreen.kt b/app/src/main/java/eu/gaudian/translator/view/vocabulary/NoVocabularyScreen.kt index 1ef5276..e69de29 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/NoVocabularyScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/vocabulary/NoVocabularyScreen.kt @@ -1,98 +0,0 @@ -@file:Suppress("AssignedValueIsNeverRead") - -package eu.gaudian.translator.view.vocabulary - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -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.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import eu.gaudian.translator.R -import eu.gaudian.translator.utils.findActivity -import eu.gaudian.translator.view.LocalConnectionConfigured -import eu.gaudian.translator.view.composable.AppButton -import eu.gaudian.translator.view.dialogs.AddVocabularyDialog -import eu.gaudian.translator.view.dialogs.ImportVocabularyDialog -import eu.gaudian.translator.viewmodel.LanguageViewModel -import eu.gaudian.translator.viewmodel.VocabularyViewModel - -@Composable -fun NoVocabularyScreen(){ - - val activity = LocalContext.current.findActivity() - - val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity) - val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity) - - - var showAddVocabularyDialog by remember { mutableStateOf(false) } - var showImportVocabularyDialog by remember { mutableStateOf(false) } - - val connectionConfigured = LocalConnectionConfigured.current - - - if (showAddVocabularyDialog) { - AddVocabularyDialog( - onDismissRequest = { showAddVocabularyDialog = false } - ) - } - - if (showImportVocabularyDialog) { - ImportVocabularyDialog( - languageViewModel = languageViewModel, - vocabularyViewModel = vocabularyViewModel, - onDismiss = { showImportVocabularyDialog = false } - ) - } - - Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - - Image( - modifier = Modifier.size(200.dp), - painter = painterResource(id = R.drawable.ic_empty), - contentDescription = stringResource(id = R.string.text_vocab_empty) - ) - Spacer(modifier = Modifier.size(16.dp)) - Text( - text = stringResource(id = R.string.text_vocab_empty), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onBackground, - textAlign = TextAlign.Center - ) - Spacer(modifier = Modifier.size(16.dp)) - - AppButton(modifier = Modifier.fillMaxWidth(), onClick = { showAddVocabularyDialog = true }) { - Text(stringResource(R.string.label_add_vocabulary)) - } - if(connectionConfigured) { - AppButton(modifier = Modifier.fillMaxWidth(), onClick = { showImportVocabularyDialog = true }) { - Text(stringResource(R.string.label_create_vocabulary_with_ai)) - } - } - - } -} \ No newline at end of file diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularyCardHost.kt b/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularyCardHost.kt index 7551477..4f09e55 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularyCardHost.kt +++ b/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularyCardHost.kt @@ -48,7 +48,6 @@ import eu.gaudian.translator.view.composable.AppIcons import eu.gaudian.translator.view.composable.AppScaffold import eu.gaudian.translator.view.composable.AppTopAppBar import eu.gaudian.translator.view.dialogs.CategorySelectionDialog -import eu.gaudian.translator.view.dialogs.ImportVocabularyDialog import eu.gaudian.translator.view.dialogs.StageSelectionDialog import eu.gaudian.translator.view.vocabulary.card.VocabularyDisplayCard import eu.gaudian.translator.view.vocabulary.card.VocabularyExerciseCard @@ -315,16 +314,6 @@ fun VocabularyCardHost( ) } - if (showImportDialog) { - ImportVocabularyDialog( - onDismiss = { showImportDialog = false }, - languageViewModel = languageViewModel, - optionalDescription = stringResource(R.string.generate_related_vocabulary_items), - optionalSearchTerm = currentVocabularyItem.wordFirst, - vocabularyViewModel = vocabularyViewModel - ) - } - LaunchedEffect(spellingMode) { @Suppress("ControlFlowWithEmptyBody") if (spellingMode) { diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/widgets/ModernStartButton.kt b/app/src/main/java/eu/gaudian/translator/view/vocabulary/widgets/ModernStartButton.kt index 2ca96db..e69de29 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/widgets/ModernStartButton.kt +++ b/app/src/main/java/eu/gaudian/translator/view/vocabulary/widgets/ModernStartButton.kt @@ -1,200 +0,0 @@ -package eu.gaudian.translator.view.vocabulary.widgets - -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.collectIsPressedAsState -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.scale -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import eu.gaudian.translator.R -import eu.gaudian.translator.ui.theme.ThemePreviews -import eu.gaudian.translator.view.composable.AppIcons - -/** - * A modern, visually appealing set of start buttons for exercises. - * The public signature is identical to the original for drop-in replacement. - * - * @param onCustomClick Lambda for the primary custom exercise action. - * @param onDailyClick Lambda for daily exercises. It's called with `false` for a - * normal daily exercise and `true` for a daily spelling exercise. - */ -@Composable -fun ModernStartButtons( - onCustomClick: () -> Unit, - onDailyClick: (isSpelling: Boolean) -> Unit -) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp, vertical = 16.dp), - horizontalArrangement = Arrangement.spacedBy(20.dp), - verticalAlignment = Alignment.CenterVertically - ) { - // A large, prominent "feature button" for the main call to action. - FeatureButton( - text = stringResource(R.string.text_custom_exercise), - icon = AppIcons.PlayCircleFilled, - onClick = onCustomClick, - modifier = Modifier.weight(1f) - ) - - // A column for the two secondary "daily" actions. - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - SecondaryButton( - text = stringResource(R.string.text_daily_exercise), - icon = AppIcons.Today, - onClick = { onDailyClick(false) } - ) - - SecondaryButton( - text = stringResource(R.string.quick_word_pairs), - icon = AppIcons.SwapHoriz, - onClick = { onDailyClick(true) } - ) - } - } -} - -/** - * A visually rich feature button with a gradient background and a subtle - * press animation. Designed to be the primary call to action. - */ -@Composable -private fun FeatureButton( - text: String, - icon: ImageVector, - onClick: () -> Unit, - modifier: Modifier = Modifier -) { - val interactionSource = remember { MutableInteractionSource() } - val isPressed by interactionSource.collectIsPressedAsState() - @Suppress("HardCodedStringLiteral") val scale by animateFloatAsState(targetValue = if (isPressed) 0.95f else 1f, label = "label_scale" - ) - - Card( - modifier = modifier - .aspectRatio(1f) - .scale(scale) - .clickable( - interactionSource = interactionSource, - indication = null, - onClick = onClick - ), - shape = MaterialTheme.shapes.extraLarge, - elevation = CardDefaults.cardElevation(defaultElevation = 8.dp) - ) { - Box( - modifier = Modifier - .fillMaxSize() - .background( - Brush.linearGradient( - colors = listOf( - MaterialTheme.colorScheme.primaryContainer, - MaterialTheme.colorScheme.primary - ) - ) - ), - contentAlignment = Alignment.Center - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - modifier = Modifier.padding(16.dp) - ) { - Icon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.onPrimary - ) - Spacer(modifier = Modifier.height(6.dp)) - Text( - text = text, - style = MaterialTheme.typography.titleMedium.copy( - fontWeight = FontWeight.Bold, - fontSize = 14.sp - ), - color = MaterialTheme.colorScheme.onPrimary, - textAlign = TextAlign.Center - ) - } - } - } -} - -/** - * A clean and simple OutlinedButton for secondary actions, with an icon and text. - */ -@Composable -private fun SecondaryButton( - text: String, - icon: ImageVector, - onClick: () -> Unit, - modifier: Modifier = Modifier -) { - OutlinedButton( - onClick = onClick, - modifier = modifier.fillMaxWidth(), - shape = MaterialTheme.shapes.large, - contentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp), - border = BorderStroke(1.dp, MaterialTheme.colorScheme.primary.copy(alpha = 0.5f)) - ) { - Icon( - imageVector = icon, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(20.dp) - ) - Spacer(modifier = Modifier.size(8.dp)) - Text( - text = text, - style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.primary, - modifier = Modifier.weight(1f) - ) - } -} - - -@ThemePreviews -@Composable -private fun ModernStartButtonsPreview() { - ModernStartButtons( - onCustomClick = {}, - onDailyClick = {} - ) -} \ No newline at end of file diff --git a/app/src/main/java/eu/gaudian/translator/viewmodel/ProgressViewModel.kt b/app/src/main/java/eu/gaudian/translator/viewmodel/ProgressViewModel.kt index 02dc862..ce789f5 100644 --- a/app/src/main/java/eu/gaudian/translator/viewmodel/ProgressViewModel.kt +++ b/app/src/main/java/eu/gaudian/translator/viewmodel/ProgressViewModel.kt @@ -90,6 +90,9 @@ class ProgressViewModel @Inject constructor( private val _totalWordsInProgress = MutableStateFlow(0) val totalWordsInProgress: StateFlow = _totalWordsInProgress.asStateFlow() + private val _totalWords = MutableStateFlow(0) + val totalWords: StateFlow = _totalWords.asStateFlow() + private val _weeklyActivityStats = MutableStateFlow>(emptyList()) val weeklyActivityStats: StateFlow> = _weeklyActivityStats.asStateFlow() @@ -284,6 +287,8 @@ class ProgressViewModel @Inject constructor( .filter { it.stage != VocabularyStage.LEARNED && it.stage != VocabularyStage.NEW } .sumOf { it.itemCount } + _totalWords.value = stageList.sumOf { it.itemCount } + if (_selectedCategories.value.isEmpty() && progressList.isNotEmpty()) { val initialCategory = setOf(progressList.first().vocabularyCategory.id) _selectedCategories.value = initialCategory diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 79bc19f..3fc82ce 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1119,6 +1119,5 @@ Oops, something went wrong :( Stats Library - Legacy Vocabulary Edit