Remove the legacy MainVocabularyScreen and its associated components, consolidating vocabulary management into the new LibraryScreen and StatsScreen architectures.
This commit is contained in:
@@ -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 ,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<Screen> {
|
||||
val items = mutableListOf<Screen>()
|
||||
items.add(Translation)
|
||||
items.add(Vocabulary)
|
||||
items.add(Dictionary)
|
||||
items.add(Settings)
|
||||
if (showExperimental) {
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 = {}
|
||||
)
|
||||
}
|
||||
@@ -90,6 +90,9 @@ class ProgressViewModel @Inject constructor(
|
||||
private val _totalWordsInProgress = MutableStateFlow(0)
|
||||
val totalWordsInProgress: StateFlow<Int> = _totalWordsInProgress.asStateFlow()
|
||||
|
||||
private val _totalWords = MutableStateFlow(0)
|
||||
val totalWords: StateFlow<Int> = _totalWords.asStateFlow()
|
||||
|
||||
private val _weeklyActivityStats = MutableStateFlow<List<WeeklyActivityStat>>(emptyList())
|
||||
val weeklyActivityStats: StateFlow<List<WeeklyActivityStat>> = _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
|
||||
|
||||
@@ -1119,6 +1119,5 @@
|
||||
<string name="message_test_error">Oops, something went wrong :(</string>
|
||||
<string name="label_stats">Stats</string>
|
||||
<string name="label_library">Library</string>
|
||||
<string name="label_legacy_vocabulary">Legacy Vocabulary</string>
|
||||
<string name="label_edit">Edit</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user