Remove the legacy MainVocabularyScreen and its associated components, consolidating vocabulary management into the new LibraryScreen and StatsScreen architectures.

This commit is contained in:
jonasgaudian
2026-02-17 15:46:56 +01:00
parent 85c407481d
commit 02530dafbf
15 changed files with 193 additions and 1305 deletions

View File

@@ -9,7 +9,6 @@ import eu.gaudian.translator.R
sealed class WidgetType(val id: String, @param:StringRes val titleRes: Int) { sealed class WidgetType(val id: String, @param:StringRes val titleRes: Int) {
data object Status : WidgetType("status", R.string.label_status) data object Status : WidgetType("status", R.string.label_status)
data object Streak : WidgetType("streak", R.string.title_widget_streak) 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 AllVocabulary : WidgetType("all_vocabulary", R.string.label_all_vocabulary)
data object DueToday : WidgetType("due_today", R.string.title_widget_due_today) data object DueToday : WidgetType("due_today", R.string.title_widget_due_today)
data object CategoryProgress : WidgetType("category_progress", R.string.label_categories) 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( val DEFAULT_ORDER = listOf(
Status, Status,
Streak, Streak,
StartButtons,
AllVocabulary, AllVocabulary,
DueToday, DueToday,
CategoryProgress , CategoryProgress ,

View File

@@ -256,12 +256,16 @@ fun TranslatorApp(
val isBottomBarHidden = currentDestination?.hierarchy?.any { destination -> val isBottomBarHidden = currentDestination?.hierarchy?.any { destination ->
destination.route in setOf( destination.route in setOf(
Screen.Translation.route, Screen.Translation.route,
Screen.Vocabulary.route,
Screen.Dictionary.route, Screen.Dictionary.route,
Screen.Exercises.route, Screen.Exercises.route,
Screen.Settings.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) val showBottomNavLabels by settingsViewModel.showBottomNavLabels.collectAsStateWithLifecycle(initialValue = false)
BottomNavigationBar( BottomNavigationBar(
@@ -272,7 +276,6 @@ fun TranslatorApp(
val inSameSection = currentDestination?.hierarchy?.any { it.route == screen.route } == true val inSameSection = currentDestination?.hierarchy?.any { it.route == screen.route } == true
val isMoreSection = screen in setOf( val isMoreSection = screen in setOf(
Screen.Translation, Screen.Translation,
Screen.Vocabulary,
Screen.Dictionary, Screen.Dictionary,
Screen.Settings, Screen.Settings,
Screen.Exercises Screen.Exercises

View File

@@ -40,7 +40,6 @@ import eu.gaudian.translator.view.translation.TranslationScreen
import eu.gaudian.translator.view.vocabulary.CategoryDetailScreen import eu.gaudian.translator.view.vocabulary.CategoryDetailScreen
import eu.gaudian.translator.view.vocabulary.CategoryListScreen import eu.gaudian.translator.view.vocabulary.CategoryListScreen
import eu.gaudian.translator.view.vocabulary.LanguageProgressScreen 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.NewWordReviewScreen
import eu.gaudian.translator.view.vocabulary.NewWordScreen import eu.gaudian.translator.view.vocabulary.NewWordScreen
import eu.gaudian.translator.view.vocabulary.NoGrammarItemsScreen import eu.gaudian.translator.view.vocabulary.NoGrammarItemsScreen
@@ -67,7 +66,6 @@ fun AppNavHost(
Screen.Library.route, Screen.Library.route,
Screen.Stats.route, Screen.Stats.route,
Screen.Translation.route, Screen.Translation.route,
Screen.Vocabulary.route,
Screen.Dictionary.route, Screen.Dictionary.route,
Screen.Exercises.route, Screen.Exercises.route,
SettingsRoutes.LIST SettingsRoutes.LIST
@@ -132,8 +130,12 @@ fun AppNavHost(
HomeScreen(navController = navController) HomeScreen(navController = navController)
} }
composable(Screen.Library.route) { composable("new_word") {
LibraryScreen(navController = navController) NewWordScreen(navController = navController)
}
composable("new_word_review") {
NewWordReviewScreen(navController = navController)
} }
composable("start_exercise") { composable("start_exercise") {
@@ -142,10 +144,10 @@ fun AppNavHost(
// Define all other navigation graphs at the same top level. // Define all other navigation graphs at the same top level.
homeGraph(navController) homeGraph(navController)
libraryGraph(navController)
statsGraph(navController) statsGraph(navController)
translationGraph(navController) translationGraph(navController)
dictionaryGraph(navController) dictionaryGraph(navController)
vocabularyGraph(navController)
exerciseGraph(navController) exerciseGraph(navController)
settingsGraph(navController) settingsGraph(navController)
} }
@@ -159,177 +161,16 @@ fun NavGraphBuilder.homeGraph(navController: NavHostController) {
composable("main_home") { composable("main_home") {
HomeScreen(navController = navController) HomeScreen(navController = navController)
} }
composable("new_word") {
NewWordScreen(navController = navController)
}
composable("new_word_review") {
NewWordReviewScreen(navController = navController)
}
} }
} }
fun NavGraphBuilder.statsGraph( fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
navController: NavHostController,
) {
navigation( navigation(
startDestination = "main_stats", startDestination = "main_library",
route = Screen.Stats.route route = Screen.Library.route
) { ) {
composable("main_stats") { composable("main_library") {
StatsScreen(navController = navController) LibraryScreen(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("vocabulary_sorting") { composable("vocabulary_sorting") {
VocabularySortingScreen( 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) @OptIn(androidx.compose.animation.ExperimentalAnimationApi::class)
fun NavGraphBuilder.exerciseGraph( fun NavGraphBuilder.exerciseGraph(
navController: NavHostController, navController: NavHostController,

View File

@@ -74,7 +74,6 @@ sealed class Screen(
object Stats : Screen("stats", R.string.label_stats, AppIcons.Statistics, AppIcons.Statistics) 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 Translation : Screen("translation", R.string.label_translation, AppIcons.TranslateFilled, AppIcons.TranslateOutlined)
object Library : Screen("library", R.string.label_library, AppIcons.VocabularyFilled, AppIcons.VocabularyOutlined) 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 Settings : Screen("settings", R.string.title_settings, AppIcons.SettingsFilled, AppIcons.SettingsOutlined)
object More : Screen("more", R.string.label_more, AppIcons.MoreVert, AppIcons.MoreHorizontal) object More : Screen("more", R.string.label_more, AppIcons.MoreVert, AppIcons.MoreHorizontal)
object Dictionary : Screen("dictionary", R.string.label_dictionary, AppIcons.DictionaryFilled, AppIcons.DictionaryOutlined) object Dictionary : Screen("dictionary", R.string.label_dictionary, AppIcons.DictionaryFilled, AppIcons.DictionaryOutlined)
@@ -88,7 +87,6 @@ sealed class Screen(
fun getMoreMenuItems(showExperimental: Boolean = false): List<Screen> { fun getMoreMenuItems(showExperimental: Boolean = false): List<Screen> {
val items = mutableListOf<Screen>() val items = mutableListOf<Screen>()
items.add(Translation) items.add(Translation)
items.add(Vocabulary)
items.add(Dictionary) items.add(Dictionary)
items.add(Settings) items.add(Settings)
if (showExperimental) { if (showExperimental) {

View File

@@ -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"
)
}

View File

@@ -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 }
)
}
}

View File

@@ -17,12 +17,10 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AddCircle 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.ChevronRight
import androidx.compose.material.icons.filled.LocalFireDepartment import androidx.compose.material.icons.filled.LocalFireDepartment
import androidx.compose.material.icons.filled.Psychology import androidx.compose.material.icons.filled.Psychology
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.TrendingUp
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@@ -110,7 +108,7 @@ fun HomeScreen(
) )
} }
item { WeeklyProgressSection(navController = navController) } item { WeeklyProgressSection(navController = navController) }
item { BottomStatsSection() } item { BottomStatsSection(navController = navController) }
// Bottom padding for edge-to-edge screens // Bottom padding for edge-to-edge screens
item { Spacer(modifier = Modifier.height(24.dp)) } item { Spacer(modifier = Modifier.height(24.dp)) }
@@ -387,7 +385,14 @@ fun WeeklyProgressSection(
} }
@Composable @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( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp) horizontalArrangement = Arrangement.spacedBy(16.dp)
@@ -395,34 +400,26 @@ fun BottomStatsSection() {
// Total Words // Total Words
AppCard( AppCard(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
onClick = { navController.navigate(Screen.Library.route) }
) { ) {
Column(modifier = Modifier.padding(20.dp)) { Column(modifier = Modifier.padding(20.dp)) {
Text(text = "TOTAL WORDS", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)) Text(text = "TOTAL WORDS", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f))
Spacer(modifier = Modifier.height(8.dp)) 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)) 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( AppCard(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
onClick = { navController.navigate("stats/language_progress") }
) { ) {
Column(modifier = Modifier.padding(20.dp)) { 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)) 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)) 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)
}
} }
} }
} }

View File

@@ -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.CategoryProgressWidget
import eu.gaudian.translator.view.vocabulary.widgets.DueTodayWidget import eu.gaudian.translator.view.vocabulary.widgets.DueTodayWidget
import eu.gaudian.translator.view.vocabulary.widgets.LevelWidget 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.StatusWidget
import eu.gaudian.translator.view.vocabulary.widgets.StreakWidget import eu.gaudian.translator.view.vocabulary.widgets.StreakWidget
import eu.gaudian.translator.view.vocabulary.widgets.WeeklyActivityChartWidget import eu.gaudian.translator.view.vocabulary.widgets.WeeklyActivityChartWidget
@@ -539,17 +538,7 @@ private fun LazyWidget(
onMissingLanguage: (Int) -> Unit onMissingLanguage: (Int) -> Unit
) { ) {
when (widgetType) { when (widgetType) {
WidgetType.StartButtons -> ModernStartButtons(
onCustomClick = onShowCustomExerciseDialog,
onDailyClick = { isSpelling ->
if (isSpelling) {
onShowWordPairExerciseDialog()
} else {
startDailyExercise(true)
Log.d("DailyExercise", "Starting daily exercise")
}
}
)
WidgetType.Status -> LazyStatusWidget( WidgetType.Status -> LazyStatusWidget(
vocabularyViewModel = vocabularyViewModel, vocabularyViewModel = vocabularyViewModel,

View File

@@ -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.CategoryProgressWidget
import eu.gaudian.translator.view.vocabulary.widgets.DueTodayWidget import eu.gaudian.translator.view.vocabulary.widgets.DueTodayWidget
import eu.gaudian.translator.view.vocabulary.widgets.LevelWidget 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.StatusWidget
import eu.gaudian.translator.view.vocabulary.widgets.StreakWidget import eu.gaudian.translator.view.vocabulary.widgets.StreakWidget
import eu.gaudian.translator.view.vocabulary.widgets.WeeklyActivityChartWidget import eu.gaudian.translator.view.vocabulary.widgets.WeeklyActivityChartWidget
@@ -530,17 +529,7 @@ private fun LazyWidget(
onMissingLanguage: (Int) -> Unit onMissingLanguage: (Int) -> Unit
) { ) {
when (widgetType) { when (widgetType) {
WidgetType.StartButtons -> ModernStartButtons(
onCustomClick = onShowCustomExerciseDialog,
onDailyClick = { isSpelling ->
if (isSpelling) {
onShowWordPairExerciseDialog()
} else {
startDailyExercise(true)
Log.d("DailyExercise", "Starting daily exercise")
}
}
)
WidgetType.Status -> LazyStatusWidget( WidgetType.Status -> LazyStatusWidget(
vocabularyViewModel = vocabularyViewModel, vocabularyViewModel = vocabularyViewModel,

View File

@@ -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<List<VocabularyCategory>>(emptyList()) }
var tempWpStages by remember { mutableStateOf<List<VocabularyStage>>(emptyList()) }
var tempWpLanguageIds by remember { mutableStateOf<List<Int>>(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<Question>()
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)
}

View File

@@ -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))
}
}
}
}

View File

@@ -48,7 +48,6 @@ import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppScaffold import eu.gaudian.translator.view.composable.AppScaffold
import eu.gaudian.translator.view.composable.AppTopAppBar import eu.gaudian.translator.view.composable.AppTopAppBar
import eu.gaudian.translator.view.dialogs.CategorySelectionDialog 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.dialogs.StageSelectionDialog
import eu.gaudian.translator.view.vocabulary.card.VocabularyDisplayCard import eu.gaudian.translator.view.vocabulary.card.VocabularyDisplayCard
import eu.gaudian.translator.view.vocabulary.card.VocabularyExerciseCard 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) { LaunchedEffect(spellingMode) {
@Suppress("ControlFlowWithEmptyBody") @Suppress("ControlFlowWithEmptyBody")
if (spellingMode) { if (spellingMode) {

View File

@@ -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 = {}
)
}

View File

@@ -90,6 +90,9 @@ class ProgressViewModel @Inject constructor(
private val _totalWordsInProgress = MutableStateFlow(0) private val _totalWordsInProgress = MutableStateFlow(0)
val totalWordsInProgress: StateFlow<Int> = _totalWordsInProgress.asStateFlow() val totalWordsInProgress: StateFlow<Int> = _totalWordsInProgress.asStateFlow()
private val _totalWords = MutableStateFlow(0)
val totalWords: StateFlow<Int> = _totalWords.asStateFlow()
private val _weeklyActivityStats = MutableStateFlow<List<WeeklyActivityStat>>(emptyList()) private val _weeklyActivityStats = MutableStateFlow<List<WeeklyActivityStat>>(emptyList())
val weeklyActivityStats: StateFlow<List<WeeklyActivityStat>> = _weeklyActivityStats.asStateFlow() val weeklyActivityStats: StateFlow<List<WeeklyActivityStat>> = _weeklyActivityStats.asStateFlow()
@@ -284,6 +287,8 @@ class ProgressViewModel @Inject constructor(
.filter { it.stage != VocabularyStage.LEARNED && it.stage != VocabularyStage.NEW } .filter { it.stage != VocabularyStage.LEARNED && it.stage != VocabularyStage.NEW }
.sumOf { it.itemCount } .sumOf { it.itemCount }
_totalWords.value = stageList.sumOf { it.itemCount }
if (_selectedCategories.value.isEmpty() && progressList.isNotEmpty()) { if (_selectedCategories.value.isEmpty() && progressList.isNotEmpty()) {
val initialCategory = setOf(progressList.first().vocabularyCategory.id) val initialCategory = setOf(progressList.first().vocabularyCategory.id)
_selectedCategories.value = initialCategory _selectedCategories.value = initialCategory

View File

@@ -1119,6 +1119,5 @@
<string name="message_test_error">Oops, something went wrong :(</string> <string name="message_test_error">Oops, something went wrong :(</string>
<string name="label_stats">Stats</string> <string name="label_stats">Stats</string>
<string name="label_library">Library</string> <string name="label_library">Library</string>
<string name="label_legacy_vocabulary">Legacy Vocabulary</string>
<string name="label_edit">Edit</string> <string name="label_edit">Edit</string>
</resources> </resources>