From 37d8c2a6c5c272404883504bd7da1f6b67eedfe3 Mon Sep 17 00:00:00 2001 From: jonasgaudian <43753916+jonasgaudian@users.noreply.github.com> Date: Wed, 18 Feb 2026 20:54:18 +0100 Subject: [PATCH] Refactor the project structure by reorganizing exercise, category, and statistics components, and extract `AppCard` into a dedicated file. --- .../eu/gaudian/translator/view/Navigation.kt | 20 +- .../CategoryDetailScreen.kt | 18 +- .../CategoryListScreen.kt | 6 +- .../translator/view/composable/AppCard.kt | 249 +++++++ .../translator/view/composable/AppFabMenu.kt | 1 + .../view/composable/AppTabLayout.kt | 1 + .../view/composable/ComponentLibrary.kt | 230 +----- .../ExerciseControls.kt | 4 +- .../ExerciseProgressIndicator.kt | 2 +- .../translator/view/home/HomeScreen.kt | 2 +- .../AiGenerationScreen.kt | 2 +- .../ExerciseListScreen.kt | 2 +- .../ExerciseMenu.kt | 2 +- .../ExerciseSessionScreen.kt | 4 +- .../ExerciseVocabularyScreen.kt | 2 +- .../GenerateExerciseDialog.kt | 2 +- .../MainExerciseScreen.kt | 2 +- .../QuestionUIs.kt | 2 +- .../StartExerciseScreen.kt | 8 +- .../YouTubeBrowserScreen.kt | 2 +- .../YouTubeExerciseDialog.kt | 2 +- .../YouTubeExerciseScreen.kt | 2 +- .../translator/view/stats/StatsScreen.kt | 14 +- .../widgets/AllVocabularyWidget.kt | 2 +- .../widgets/CategoryWidget.kt | 2 +- .../widgets/DueTodayWidget.kt | 2 +- .../widgets/LanguageLevelWidget.kt | 2 +- .../widgets/LevelWidget.kt | 2 +- .../widgets/StageProgressBar.kt | 2 +- .../widgets/StatusWidget.kt | 2 +- .../widgets/StreakWidget.kt | 2 +- .../widgets/WeeklyActivityChartWidget.kt | 2 +- .../widgets/WidgetButtonOutline.kt | 2 +- .../view/vocabulary/DashboardContent.kt | 671 ------------------ ...ressScreen.kt => LanguageJourneyScreen.kt} | 6 +- .../view/vocabulary/StageDetailScreen.kt | 2 +- .../VocabularyExerciseHostScreen.kt | 2 + app/src/main/res/values-de-rDE/strings.xml | 2 +- app/src/main/res/values-pt-rBR/strings.xml | 2 - app/src/main/res/values/strings.xml | 4 +- 40 files changed, 324 insertions(+), 964 deletions(-) rename app/src/main/java/eu/gaudian/translator/view/{vocabulary => categories}/CategoryDetailScreen.kt (96%) rename app/src/main/java/eu/gaudian/translator/view/{vocabulary => categories}/CategoryListScreen.kt (98%) create mode 100644 app/src/main/java/eu/gaudian/translator/view/composable/AppCard.kt rename app/src/main/java/eu/gaudian/translator/view/{vocabulary => exercises}/ExerciseControls.kt (96%) rename app/src/main/java/eu/gaudian/translator/view/{vocabulary => exercises}/ExerciseProgressIndicator.kt (99%) rename app/src/main/java/eu/gaudian/translator/view/{exercises => new_ecercises}/AiGenerationScreen.kt (99%) rename app/src/main/java/eu/gaudian/translator/view/{exercises => new_ecercises}/ExerciseListScreen.kt (98%) rename app/src/main/java/eu/gaudian/translator/view/{exercises => new_ecercises}/ExerciseMenu.kt (95%) rename app/src/main/java/eu/gaudian/translator/view/{exercises => new_ecercises}/ExerciseSessionScreen.kt (99%) rename app/src/main/java/eu/gaudian/translator/view/{exercises => new_ecercises}/ExerciseVocabularyScreen.kt (97%) rename app/src/main/java/eu/gaudian/translator/view/{exercises => new_ecercises}/GenerateExerciseDialog.kt (99%) rename app/src/main/java/eu/gaudian/translator/view/{exercises => new_ecercises}/MainExerciseScreen.kt (99%) rename app/src/main/java/eu/gaudian/translator/view/{exercises => new_ecercises}/QuestionUIs.kt (99%) rename app/src/main/java/eu/gaudian/translator/view/{exercises => new_ecercises}/StartExerciseScreen.kt (99%) rename app/src/main/java/eu/gaudian/translator/view/{exercises => new_ecercises}/YouTubeBrowserScreen.kt (99%) rename app/src/main/java/eu/gaudian/translator/view/{exercises => new_ecercises}/YouTubeExerciseDialog.kt (99%) rename app/src/main/java/eu/gaudian/translator/view/{exercises => new_ecercises}/YouTubeExerciseScreen.kt (99%) rename app/src/main/java/eu/gaudian/translator/view/{vocabulary => stats}/widgets/AllVocabularyWidget.kt (98%) rename app/src/main/java/eu/gaudian/translator/view/{vocabulary => stats}/widgets/CategoryWidget.kt (99%) rename app/src/main/java/eu/gaudian/translator/view/{vocabulary => stats}/widgets/DueTodayWidget.kt (98%) rename app/src/main/java/eu/gaudian/translator/view/{vocabulary => stats}/widgets/LanguageLevelWidget.kt (98%) rename app/src/main/java/eu/gaudian/translator/view/{vocabulary => stats}/widgets/LevelWidget.kt (98%) rename app/src/main/java/eu/gaudian/translator/view/{vocabulary => stats}/widgets/StageProgressBar.kt (99%) rename app/src/main/java/eu/gaudian/translator/view/{vocabulary => stats}/widgets/StatusWidget.kt (99%) rename app/src/main/java/eu/gaudian/translator/view/{vocabulary => stats}/widgets/StreakWidget.kt (99%) rename app/src/main/java/eu/gaudian/translator/view/{vocabulary => stats}/widgets/WeeklyActivityChartWidget.kt (99%) rename app/src/main/java/eu/gaudian/translator/view/{vocabulary => stats}/widgets/WidgetButtonOutline.kt (98%) delete mode 100644 app/src/main/java/eu/gaudian/translator/view/vocabulary/DashboardContent.kt rename app/src/main/java/eu/gaudian/translator/view/vocabulary/{LanguageProgressScreen.kt => LanguageJourneyScreen.kt} (98%) diff --git a/app/src/main/java/eu/gaudian/translator/view/Navigation.kt b/app/src/main/java/eu/gaudian/translator/view/Navigation.kt index e008bc0..700d0ef 100644 --- a/app/src/main/java/eu/gaudian/translator/view/Navigation.kt +++ b/app/src/main/java/eu/gaudian/translator/view/Navigation.kt @@ -20,27 +20,27 @@ import androidx.navigation.compose.composable import androidx.navigation.navArgument import androidx.navigation.navigation import eu.gaudian.translator.model.VocabularyStage +import eu.gaudian.translator.view.categories.CategoryDetailScreen +import eu.gaudian.translator.view.categories.CategoryListScreen import eu.gaudian.translator.view.composable.Screen import eu.gaudian.translator.view.dictionary.DictionaryResultScreen import eu.gaudian.translator.view.dictionary.EtymologyResultScreen import eu.gaudian.translator.view.dictionary.MainDictionaryScreen -import eu.gaudian.translator.view.exercises.ExerciseSessionScreen -import eu.gaudian.translator.view.exercises.MainExerciseScreen -import eu.gaudian.translator.view.exercises.StartExerciseScreen -import eu.gaudian.translator.view.exercises.YouTubeBrowserScreen -import eu.gaudian.translator.view.exercises.YouTubeExerciseScreen import eu.gaudian.translator.view.home.DailyReviewScreen import eu.gaudian.translator.view.home.HomeScreen import eu.gaudian.translator.view.library.LibraryScreen +import eu.gaudian.translator.view.new_ecercises.ExerciseSessionScreen +import eu.gaudian.translator.view.new_ecercises.MainExerciseScreen +import eu.gaudian.translator.view.new_ecercises.StartExerciseScreen +import eu.gaudian.translator.view.new_ecercises.YouTubeBrowserScreen +import eu.gaudian.translator.view.new_ecercises.YouTubeExerciseScreen import eu.gaudian.translator.view.settings.DictionaryOptionsScreen import eu.gaudian.translator.view.settings.TranslationSettingsScreen import eu.gaudian.translator.view.settings.settingsGraph import eu.gaudian.translator.view.stats.StatsScreen import eu.gaudian.translator.view.translation.TranslationScreen import eu.gaudian.translator.view.vocabulary.AllCardsListScreen -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.LanguageJourneyScreen import eu.gaudian.translator.view.vocabulary.NewWordReviewScreen import eu.gaudian.translator.view.vocabulary.NewWordScreen import eu.gaudian.translator.view.vocabulary.NoGrammarItemsScreen @@ -269,7 +269,7 @@ fun NavGraphBuilder.libraryGraph(navController: NavHostController) { ) } composable("language_progress") { - LanguageProgressScreen( + LanguageJourneyScreen( navController = navController ) @@ -439,7 +439,7 @@ fun NavGraphBuilder.statsGraph( ) } composable(NavigationRoutes.STATS_LANGUAGE_PROGRESS) { - LanguageProgressScreen( + LanguageJourneyScreen( navController = navController ) } diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/CategoryDetailScreen.kt b/app/src/main/java/eu/gaudian/translator/view/categories/CategoryDetailScreen.kt similarity index 96% rename from app/src/main/java/eu/gaudian/translator/view/vocabulary/CategoryDetailScreen.kt rename to app/src/main/java/eu/gaudian/translator/view/categories/CategoryDetailScreen.kt index 1113072..5052a54 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/CategoryDetailScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/categories/CategoryDetailScreen.kt @@ -1,8 +1,13 @@ @file:Suppress("HardCodedStringLiteral") -package eu.gaudian.translator.view.vocabulary +package eu.gaudian.translator.view.categories import android.annotation.SuppressLint +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -54,8 +59,9 @@ import eu.gaudian.translator.view.composable.PrimaryButton import eu.gaudian.translator.view.dialogs.DeleteCategoryDialog import eu.gaudian.translator.view.dialogs.DeleteItemsDialog import eu.gaudian.translator.view.dialogs.EditCategoryDialog -import eu.gaudian.translator.view.vocabulary.widgets.CategoryProgressCircle -import eu.gaudian.translator.view.vocabulary.widgets.ChartLegend +import eu.gaudian.translator.view.vocabulary.AllCardsListScreen +import eu.gaudian.translator.view.stats.widgets.CategoryProgressCircle +import eu.gaudian.translator.view.stats.widgets.ChartLegend import eu.gaudian.translator.viewmodel.CategoryProgress import eu.gaudian.translator.viewmodel.CategoryViewModel import eu.gaudian.translator.viewmodel.ExportImportViewModel @@ -244,10 +250,10 @@ fun CategoryDetailScreen( ) // Category Header Card with Progress and Action Buttons (animated) - androidx.compose.animation.AnimatedVisibility( + AnimatedVisibility( visible = isHeaderVisible, - enter = androidx.compose.animation.fadeIn() + androidx.compose.animation.expandVertically(), - exit = androidx.compose.animation.fadeOut() + androidx.compose.animation.shrinkVertically() + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() ) { CategoryHeaderCard( subtitle = subtitle, diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/CategoryListScreen.kt b/app/src/main/java/eu/gaudian/translator/view/categories/CategoryListScreen.kt similarity index 98% rename from app/src/main/java/eu/gaudian/translator/view/vocabulary/CategoryListScreen.kt rename to app/src/main/java/eu/gaudian/translator/view/categories/CategoryListScreen.kt index d1d5735..c69cd7f 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/CategoryListScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/categories/CategoryListScreen.kt @@ -1,6 +1,6 @@ @file:Suppress("AssignedValueIsNeverRead") -package eu.gaudian.translator.view.vocabulary +package eu.gaudian.translator.view.categories import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background @@ -44,8 +44,8 @@ import eu.gaudian.translator.view.composable.AppScaffold import eu.gaudian.translator.view.composable.AppTopAppBar import eu.gaudian.translator.view.dialogs.AddCategoryDialog import eu.gaudian.translator.view.dialogs.DeleteMultipleCategoriesDialog -import eu.gaudian.translator.view.vocabulary.widgets.CategoryCircleType -import eu.gaudian.translator.view.vocabulary.widgets.CategoryProgressCircle +import eu.gaudian.translator.view.stats.widgets.CategoryCircleType +import eu.gaudian.translator.view.stats.widgets.CategoryProgressCircle import eu.gaudian.translator.viewmodel.CategoryViewModel import eu.gaudian.translator.viewmodel.ProgressViewModel diff --git a/app/src/main/java/eu/gaudian/translator/view/composable/AppCard.kt b/app/src/main/java/eu/gaudian/translator/view/composable/AppCard.kt new file mode 100644 index 0000000..86576c3 --- /dev/null +++ b/app/src/main/java/eu/gaudian/translator/view/composable/AppCard.kt @@ -0,0 +1,249 @@ +package eu.gaudian.translator.view.composable + +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +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.draw.clip +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import eu.gaudian.translator.R +import eu.gaudian.translator.view.composable.ComponentDefaults.DefaultElevation +import eu.gaudian.translator.view.hints.Hint +import eu.gaudian.translator.view.hints.HintBottomSheet +import eu.gaudian.translator.view.hints.LocalShowHints + +/** + * A styled card container for displaying content with a consistent floating look. + * + * @param modifier The modifier to be applied to the card. + * @param content The content to be displayed inside the card. + */ +@Composable +fun AppCard( + modifier: Modifier = Modifier, + title: String? = null, + icon: ImageVector? = null, + text: String? = null, + expandable: Boolean = false, + initiallyExpanded: Boolean = false, + onClick: (() -> Unit)? = null, + hintContent : Hint? = null, + content: @Composable ColumnScope.() -> Unit, +) { + var isExpanded by remember { mutableStateOf(initiallyExpanded) } + val showHints = LocalShowHints.current + + val rotationState by animateFloatAsState( + targetValue = if (isExpanded) 180f else 0f, + label = "Chevron Rotation" + ) + + // Check if we need to render the header row + // Updated to include icon in the check + val hasHeader = title != null || text != null || expandable || icon != null + val canClickHeader = expandable || onClick != null + + var showBottomSheet by remember { mutableStateOf(false) } + + if (showBottomSheet) { + hintContent?.let { + HintBottomSheet( + onDismissRequest = { showBottomSheet = false }, + content = it, + sheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true + ) + ) + } + } + + Surface( + modifier = modifier + .fillMaxWidth() + .shadow( + DefaultElevation, + shape = ComponentDefaults.CardShape + ) + .clip(ComponentDefaults.CardClipShape) + // Animate height changes when expanding/collapsing + .animateContentSize(), + shape = ComponentDefaults.CardShape, + color = MaterialTheme.colorScheme.surfaceContainer, + ) { + Column { + // --- Header Row --- + if (hasHeader) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(enabled = canClickHeader) { + if (expandable) { + isExpanded = !isExpanded + } + onClick?.invoke() + } + .padding(ComponentDefaults.CardPadding), + verticalAlignment = Alignment.CenterVertically + ) { + // 1. Optional Icon on the left + if (icon != null) { + Spacer(modifier = Modifier.width(8.dp)) + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(16.dp)) + } + + // 2. Title and Text Column + Column(modifier = Modifier.weight(1f)) { + if (!title.isNullOrBlank()) { + Text( + text = title, + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface + ) + } + + // Only show spacer if both title and text exist + if (!title.isNullOrBlank() && !text.isNullOrBlank()) { + Spacer(Modifier.size(4.dp)) + } + + if (!text.isNullOrBlank()) { + Text( + text = text, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + if (showHints && hintContent != null) { + IconButton(onClick = { showBottomSheet = true }) { + Icon( + imageVector = AppIcons.Help, + contentDescription = stringResource(R.string.show_hint), + tint = MaterialTheme.colorScheme.secondary + ) + } + } + + // 3. Expand Chevron (Far right) + if (expandable) { + Icon( + imageVector = AppIcons.ArrowDropDown, + contentDescription = if (isExpanded) "Collapse" else "Expand", + modifier = Modifier.rotate(rotationState), + tint = MaterialTheme.colorScheme.onSurface + ) + } + } + + } + + // --- Content Area --- + if (!expandable || isExpanded) { + val contentModifier = Modifier + .padding( + start = ComponentDefaults.CardPadding, + end = ComponentDefaults.CardPadding, + bottom = ComponentDefaults.CardPadding, + // If we have a header, remove the top padding so content sits closer to the title. + // If no header (legacy behavior), keep the top padding. + top = if (hasHeader) 0.dp else ComponentDefaults.CardPadding + ) + + if (!hasHeader && onClick != null) { + Column( + modifier = contentModifier.clickable { onClick() }, + content = content + ) + } else { + Column( + modifier = contentModifier, + content = content + ) + } + } + } + } +} + +@Preview +@Composable +fun AppCardPreview() { + AppCard { + Text(stringResource(R.string.this_is_the_content_inside_the_card)) + PrimaryButton(onClick = { }, text = stringResource(R.string.label_continue)) + } +} + +@Preview(showBackground = true) +@Composable +fun AppCardPreview2() { + MaterialTheme { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + // 1. Expandable Card (Initially Collapsed) + AppCard( + title = "Advanced Settings", + text = "Click to reveal more options", + expandable = true, + initiallyExpanded = false + ) { + Text("Here are some hidden settings.") + Text("They are only visible when expanded.") + } + + // 2. Expandable Card (Initially Expanded) + AppCard( + title = "Translation History", + text = "Recent items", + expandable = true, + initiallyExpanded = true + ) { + Text("• Hello -> Hallo") + Text("• World -> Welt") + Text("• Sun -> Sonne") + } + + // 3. Static Card (No Title/Expand logic - Legacy behavior) + AppCard { + Text("This is a standard card without a header.") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/gaudian/translator/view/composable/AppFabMenu.kt b/app/src/main/java/eu/gaudian/translator/view/composable/AppFabMenu.kt index fd5575d..68f4170 100644 --- a/app/src/main/java/eu/gaudian/translator/view/composable/AppFabMenu.kt +++ b/app/src/main/java/eu/gaudian/translator/view/composable/AppFabMenu.kt @@ -52,6 +52,7 @@ data class FabMenuItem( ) +@Deprecated("We don't want to use floating butto menus anymore") @Composable fun AppFabMenu( items: List, diff --git a/app/src/main/java/eu/gaudian/translator/view/composable/AppTabLayout.kt b/app/src/main/java/eu/gaudian/translator/view/composable/AppTabLayout.kt index 99207ea..ab2c06a 100644 --- a/app/src/main/java/eu/gaudian/translator/view/composable/AppTabLayout.kt +++ b/app/src/main/java/eu/gaudian/translator/view/composable/AppTabLayout.kt @@ -49,6 +49,7 @@ interface TabItem { val title: String val icon: ImageVector } +@Deprecated("Migrate to new (like used in LibraryScreen") @SuppressLint("UnusedBoxWithConstraintsScope", "LocalContextResourcesRead", "DiscouragedApi", "SuspiciousIndentation" ) diff --git a/app/src/main/java/eu/gaudian/translator/view/composable/ComponentLibrary.kt b/app/src/main/java/eu/gaudian/translator/view/composable/ComponentLibrary.kt index 9f0443a..433d733 100644 --- a/app/src/main/java/eu/gaudian/translator/view/composable/ComponentLibrary.kt +++ b/app/src/main/java/eu/gaudian/translator/view/composable/ComponentLibrary.kt @@ -2,23 +2,17 @@ package eu.gaudian.translator.view.composable -import androidx.compose.animation.animateContentSize -import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer 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.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button import androidx.compose.material3.ButtonColors @@ -28,26 +22,19 @@ import androidx.compose.material3.CardDefaults import androidx.compose.material3.Checkbox import androidx.compose.material3.CheckboxDefaults import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedCard -import androidx.compose.material3.Surface import androidx.compose.material3.Switch import androidx.compose.material3.SwitchDefaults import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.material3.rememberModalBottomSheetState 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.draw.clip -import androidx.compose.ui.draw.rotate -import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.vector.ImageVector @@ -57,10 +44,6 @@ import androidx.compose.ui.unit.dp import eu.gaudian.translator.R import eu.gaudian.translator.ui.theme.ThemePreviews import eu.gaudian.translator.ui.theme.semanticColors -import eu.gaudian.translator.view.composable.ComponentDefaults.DefaultElevation -import eu.gaudian.translator.view.hints.Hint -import eu.gaudian.translator.view.hints.HintBottomSheet -import eu.gaudian.translator.view.hints.LocalShowHints object ComponentDefaults { @@ -90,218 +73,6 @@ object ComponentDefaults { const val ALPHA_LOW = 0.3f } - - -/** - * A styled card container for displaying content with a consistent floating look. - * - * @param modifier The modifier to be applied to the card. - * @param content The content to be displayed inside the card. - */ -@Composable -fun AppCard( - modifier: Modifier = Modifier, - title: String? = null, - icon: ImageVector? = null, - text: String? = null, - expandable: Boolean = false, - initiallyExpanded: Boolean = false, - onClick: (() -> Unit)? = null, - hintContent : Hint? = null, - content: @Composable ColumnScope.() -> Unit, -) { - var isExpanded by remember { mutableStateOf(initiallyExpanded) } - val showHints = LocalShowHints.current - - val rotationState by animateFloatAsState( - targetValue = if (isExpanded) 180f else 0f, - label = "Chevron Rotation" - ) - - // Check if we need to render the header row - // Updated to include icon in the check - val hasHeader = title != null || text != null || expandable || icon != null - val canClickHeader = expandable || onClick != null - - var showBottomSheet by remember { mutableStateOf(false) } - - if (showBottomSheet) { - hintContent?.let { - HintBottomSheet( - onDismissRequest = { showBottomSheet = false }, - content = it, - sheetState = rememberModalBottomSheetState( - skipPartiallyExpanded = true - ) - ) - } - } - - Surface( - modifier = modifier - .fillMaxWidth() - .shadow( - DefaultElevation, - shape = ComponentDefaults.CardShape - ) - .clip(ComponentDefaults.CardClipShape) - // Animate height changes when expanding/collapsing - .animateContentSize(), - shape = ComponentDefaults.CardShape, - color = MaterialTheme.colorScheme.surfaceContainer, - ) { - Column { - // --- Header Row --- - if (hasHeader) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable(enabled = canClickHeader) { - if (expandable) { - isExpanded = !isExpanded - } - onClick?.invoke() - } - .padding(ComponentDefaults.CardPadding), - verticalAlignment = Alignment.CenterVertically - ) { - // 1. Optional Icon on the left - if (icon != null) { - Spacer(modifier = Modifier.width(8.dp)) - Icon( - imageVector = icon, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(24.dp) - ) - Spacer(modifier = Modifier.width(16.dp)) - } - - // 2. Title and Text Column - Column(modifier = Modifier.weight(1f)) { - if (!title.isNullOrBlank()) { - Text( - text = title, - style = MaterialTheme.typography.headlineSmall, - color = MaterialTheme.colorScheme.onSurface - ) - } - - // Only show spacer if both title and text exist - if (!title.isNullOrBlank() && !text.isNullOrBlank()) { - Spacer(Modifier.size(4.dp)) - } - - if (!text.isNullOrBlank()) { - Text( - text = text, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - - if (showHints && hintContent != null) { - IconButton(onClick = { showBottomSheet = true }) { - Icon( - imageVector = AppIcons.Help, - contentDescription = stringResource(R.string.show_hint), - tint = MaterialTheme.colorScheme.secondary - ) - } - } - - // 3. Expand Chevron (Far right) - if (expandable) { - Icon( - imageVector = AppIcons.ArrowDropDown, - contentDescription = if (isExpanded) "Collapse" else "Expand", - modifier = Modifier.rotate(rotationState), - tint = MaterialTheme.colorScheme.onSurface - ) - } - } - - } - - // --- Content Area --- - if (!expandable || isExpanded) { - val contentModifier = Modifier - .padding( - start = ComponentDefaults.CardPadding, - end = ComponentDefaults.CardPadding, - bottom = ComponentDefaults.CardPadding, - // If we have a header, remove the top padding so content sits closer to the title. - // If no header (legacy behavior), keep the top padding. - top = if (hasHeader) 0.dp else ComponentDefaults.CardPadding - ) - - if (!hasHeader && onClick != null) { - Column( - modifier = contentModifier.clickable { onClick() }, - content = content - ) - } else { - Column( - modifier = contentModifier, - content = content - ) - } - } - } - } -} - -@Preview -@Composable -fun AppCardPreview() { - AppCard { - Text(stringResource(R.string.this_is_the_content_inside_the_card)) - PrimaryButton(onClick = { }, text = stringResource(R.string.label_continue)) - } -} - -@Preview(showBackground = true) -@Composable -fun AppCardPreview2() { - MaterialTheme { - Column( - modifier = Modifier - .padding(16.dp) - .fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - // 1. Expandable Card (Initially Collapsed) - AppCard( - title = "Advanced Settings", - text = "Click to reveal more options", - expandable = true, - initiallyExpanded = false - ) { - Text("Here are some hidden settings.") - Text("They are only visible when expanded.") - } - - // 2. Expandable Card (Initially Expanded) - AppCard( - title = "Translation History", - text = "Recent items", - expandable = true, - initiallyExpanded = true - ) { - Text("• Hello -> Hallo") - Text("• World -> Welt") - Text("• Sun -> Sonne") - } - - // 3. Static Card (No Title/Expand logic - Legacy behavior) - AppCard { - Text("This is a standard card without a header.") - } - } - } -} - /** * The primary button for the most important actions. * @@ -636,6 +407,7 @@ fun WrongOutlinedButtonPreview(){ WrongOutlinedButton(onClick = { }, text = stringResource(R.string.label_continue)) } +//This is basically just a wrapper for screens to control width (tablet mode) etc. @Composable fun AppOutlinedCard( modifier: Modifier = Modifier, diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/ExerciseControls.kt b/app/src/main/java/eu/gaudian/translator/view/exercises/ExerciseControls.kt similarity index 96% rename from app/src/main/java/eu/gaudian/translator/view/vocabulary/ExerciseControls.kt rename to app/src/main/java/eu/gaudian/translator/view/exercises/ExerciseControls.kt index 2b62941..395ad51 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/ExerciseControls.kt +++ b/app/src/main/java/eu/gaudian/translator/view/exercises/ExerciseControls.kt @@ -1,4 +1,4 @@ -package eu.gaudian.translator.view.vocabulary +package eu.gaudian.translator.view.exercises import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -23,6 +23,8 @@ import eu.gaudian.translator.view.composable.AppOutlinedButton import eu.gaudian.translator.view.composable.AppOutlinedTextField import eu.gaudian.translator.view.composable.CorrectButton import eu.gaudian.translator.view.composable.WrongButton +import eu.gaudian.translator.view.vocabulary.VocabularyExerciseAction +import eu.gaudian.translator.view.vocabulary.VocabularyExerciseState @Composable fun ExerciseControls( diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/ExerciseProgressIndicator.kt b/app/src/main/java/eu/gaudian/translator/view/exercises/ExerciseProgressIndicator.kt similarity index 99% rename from app/src/main/java/eu/gaudian/translator/view/vocabulary/ExerciseProgressIndicator.kt rename to app/src/main/java/eu/gaudian/translator/view/exercises/ExerciseProgressIndicator.kt index 8f06a25..1e7d751 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/ExerciseProgressIndicator.kt +++ b/app/src/main/java/eu/gaudian/translator/view/exercises/ExerciseProgressIndicator.kt @@ -1,4 +1,4 @@ -package eu.gaudian.translator.view.vocabulary +package eu.gaudian.translator.view.exercises import androidx.compose.animation.core.animateFloatAsState diff --git a/app/src/main/java/eu/gaudian/translator/view/home/HomeScreen.kt b/app/src/main/java/eu/gaudian/translator/view/home/HomeScreen.kt index 85b4dd9..f244b19 100644 --- a/app/src/main/java/eu/gaudian/translator/view/home/HomeScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/home/HomeScreen.kt @@ -49,7 +49,7 @@ import eu.gaudian.translator.view.NavigationRoutes import eu.gaudian.translator.view.composable.AppCard import eu.gaudian.translator.view.composable.Screen import eu.gaudian.translator.view.settings.SettingsRoutes -import eu.gaudian.translator.view.vocabulary.widgets.WeeklyActivityChartWidget +import eu.gaudian.translator.view.stats.widgets.WeeklyActivityChartWidget import eu.gaudian.translator.viewmodel.ProgressViewModel @Composable diff --git a/app/src/main/java/eu/gaudian/translator/view/exercises/AiGenerationScreen.kt b/app/src/main/java/eu/gaudian/translator/view/new_ecercises/AiGenerationScreen.kt similarity index 99% rename from app/src/main/java/eu/gaudian/translator/view/exercises/AiGenerationScreen.kt rename to app/src/main/java/eu/gaudian/translator/view/new_ecercises/AiGenerationScreen.kt index b561fe6..9566809 100644 --- a/app/src/main/java/eu/gaudian/translator/view/exercises/AiGenerationScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/new_ecercises/AiGenerationScreen.kt @@ -1,4 +1,4 @@ -package eu.gaudian.translator.view.exercises +package eu.gaudian.translator.view.new_ecercises import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.LinearEasing diff --git a/app/src/main/java/eu/gaudian/translator/view/exercises/ExerciseListScreen.kt b/app/src/main/java/eu/gaudian/translator/view/new_ecercises/ExerciseListScreen.kt similarity index 98% rename from app/src/main/java/eu/gaudian/translator/view/exercises/ExerciseListScreen.kt rename to app/src/main/java/eu/gaudian/translator/view/new_ecercises/ExerciseListScreen.kt index 067dfd5..ad90203 100644 --- a/app/src/main/java/eu/gaudian/translator/view/exercises/ExerciseListScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/new_ecercises/ExerciseListScreen.kt @@ -1,4 +1,4 @@ -package eu.gaudian.translator.view.exercises +package eu.gaudian.translator.view.new_ecercises import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/app/src/main/java/eu/gaudian/translator/view/exercises/ExerciseMenu.kt b/app/src/main/java/eu/gaudian/translator/view/new_ecercises/ExerciseMenu.kt similarity index 95% rename from app/src/main/java/eu/gaudian/translator/view/exercises/ExerciseMenu.kt rename to app/src/main/java/eu/gaudian/translator/view/new_ecercises/ExerciseMenu.kt index a24c8ad..7adbc85 100644 --- a/app/src/main/java/eu/gaudian/translator/view/exercises/ExerciseMenu.kt +++ b/app/src/main/java/eu/gaudian/translator/view/new_ecercises/ExerciseMenu.kt @@ -1,4 +1,4 @@ -package eu.gaudian.translator.view.exercises +package eu.gaudian.translator.view.new_ecercises import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier diff --git a/app/src/main/java/eu/gaudian/translator/view/exercises/ExerciseSessionScreen.kt b/app/src/main/java/eu/gaudian/translator/view/new_ecercises/ExerciseSessionScreen.kt similarity index 99% rename from app/src/main/java/eu/gaudian/translator/view/exercises/ExerciseSessionScreen.kt rename to app/src/main/java/eu/gaudian/translator/view/new_ecercises/ExerciseSessionScreen.kt index f97955d..b946ac8 100644 --- a/app/src/main/java/eu/gaudian/translator/view/exercises/ExerciseSessionScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/new_ecercises/ExerciseSessionScreen.kt @@ -1,4 +1,4 @@ -package eu.gaudian.translator.view.exercises +package eu.gaudian.translator.view.new_ecercises import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.tween @@ -57,7 +57,7 @@ import eu.gaudian.translator.utils.findActivity import eu.gaudian.translator.view.composable.AppButton import eu.gaudian.translator.view.composable.AppIcons import eu.gaudian.translator.view.composable.ComponentDefaults -import eu.gaudian.translator.view.vocabulary.ExerciseProgressIndicator +import eu.gaudian.translator.view.exercises.ExerciseProgressIndicator import eu.gaudian.translator.viewmodel.AnswerResult import eu.gaudian.translator.viewmodel.ExerciseSessionState import eu.gaudian.translator.viewmodel.ExerciseViewModel diff --git a/app/src/main/java/eu/gaudian/translator/view/exercises/ExerciseVocabularyScreen.kt b/app/src/main/java/eu/gaudian/translator/view/new_ecercises/ExerciseVocabularyScreen.kt similarity index 97% rename from app/src/main/java/eu/gaudian/translator/view/exercises/ExerciseVocabularyScreen.kt rename to app/src/main/java/eu/gaudian/translator/view/new_ecercises/ExerciseVocabularyScreen.kt index d1f9000..7c09497 100644 --- a/app/src/main/java/eu/gaudian/translator/view/exercises/ExerciseVocabularyScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/new_ecercises/ExerciseVocabularyScreen.kt @@ -1,4 +1,4 @@ -package eu.gaudian.translator.view.exercises +package eu.gaudian.translator.view.new_ecercises import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth diff --git a/app/src/main/java/eu/gaudian/translator/view/exercises/GenerateExerciseDialog.kt b/app/src/main/java/eu/gaudian/translator/view/new_ecercises/GenerateExerciseDialog.kt similarity index 99% rename from app/src/main/java/eu/gaudian/translator/view/exercises/GenerateExerciseDialog.kt rename to app/src/main/java/eu/gaudian/translator/view/new_ecercises/GenerateExerciseDialog.kt index e149083..b398369 100644 --- a/app/src/main/java/eu/gaudian/translator/view/exercises/GenerateExerciseDialog.kt +++ b/app/src/main/java/eu/gaudian/translator/view/new_ecercises/GenerateExerciseDialog.kt @@ -1,6 +1,6 @@ @file:Suppress("AssignedValueIsNeverRead") -package eu.gaudian.translator.view.exercises +package eu.gaudian.translator.view.new_ecercises import android.annotation.SuppressLint import androidx.compose.foundation.background diff --git a/app/src/main/java/eu/gaudian/translator/view/exercises/MainExerciseScreen.kt b/app/src/main/java/eu/gaudian/translator/view/new_ecercises/MainExerciseScreen.kt similarity index 99% rename from app/src/main/java/eu/gaudian/translator/view/exercises/MainExerciseScreen.kt rename to app/src/main/java/eu/gaudian/translator/view/new_ecercises/MainExerciseScreen.kt index 6e2eb74..c89ba38 100644 --- a/app/src/main/java/eu/gaudian/translator/view/exercises/MainExerciseScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/new_ecercises/MainExerciseScreen.kt @@ -1,6 +1,6 @@ @file:Suppress("HardCodedStringLiteral") -package eu.gaudian.translator.view.exercises +package eu.gaudian.translator.view.new_ecercises import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement diff --git a/app/src/main/java/eu/gaudian/translator/view/exercises/QuestionUIs.kt b/app/src/main/java/eu/gaudian/translator/view/new_ecercises/QuestionUIs.kt similarity index 99% rename from app/src/main/java/eu/gaudian/translator/view/exercises/QuestionUIs.kt rename to app/src/main/java/eu/gaudian/translator/view/new_ecercises/QuestionUIs.kt index bc96582..d816a53 100644 --- a/app/src/main/java/eu/gaudian/translator/view/exercises/QuestionUIs.kt +++ b/app/src/main/java/eu/gaudian/translator/view/new_ecercises/QuestionUIs.kt @@ -1,4 +1,4 @@ -package eu.gaudian.translator.view.exercises +package eu.gaudian.translator.view.new_ecercises import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background diff --git a/app/src/main/java/eu/gaudian/translator/view/exercises/StartExerciseScreen.kt b/app/src/main/java/eu/gaudian/translator/view/new_ecercises/StartExerciseScreen.kt similarity index 99% rename from app/src/main/java/eu/gaudian/translator/view/exercises/StartExerciseScreen.kt rename to app/src/main/java/eu/gaudian/translator/view/new_ecercises/StartExerciseScreen.kt index 733163e..30f761c 100644 --- a/app/src/main/java/eu/gaudian/translator/view/exercises/StartExerciseScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/new_ecercises/StartExerciseScreen.kt @@ -1,4 +1,4 @@ -package eu.gaudian.translator.view.exercises +package eu.gaudian.translator.view.new_ecercises import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background @@ -737,9 +737,9 @@ fun NumberOfCardsSection( availableQuickSelections.forEach { value -> AppOutlinedButton( onClick = { onAmountChanged(value) }, - modifier = Modifier.weight(1f) + modifier = Modifier.weight(1f).padding(4.dp) ) { - Text(value.toString()) + Text(text = value.toString()) } } } @@ -773,7 +773,7 @@ fun QuestionTypesSection( Spacer(modifier = Modifier.height(12.dp)) QuestionTypeCard( title = stringResource(R.string.label_multiple_choice_exercise), - subtitle = stringResource(R.string.label_choose_exercise_types), + subtitle = stringResource(R.string.label_multiple_choice_desc), icon = AppIcons.CheckList, isSelected = selectedTypes.contains(VocabularyExerciseType.MULTIPLE_CHOICE), onClick = { onTypeSelected(VocabularyExerciseType.MULTIPLE_CHOICE) } diff --git a/app/src/main/java/eu/gaudian/translator/view/exercises/YouTubeBrowserScreen.kt b/app/src/main/java/eu/gaudian/translator/view/new_ecercises/YouTubeBrowserScreen.kt similarity index 99% rename from app/src/main/java/eu/gaudian/translator/view/exercises/YouTubeBrowserScreen.kt rename to app/src/main/java/eu/gaudian/translator/view/new_ecercises/YouTubeBrowserScreen.kt index 81d1df8..adcd681 100644 --- a/app/src/main/java/eu/gaudian/translator/view/exercises/YouTubeBrowserScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/new_ecercises/YouTubeBrowserScreen.kt @@ -1,6 +1,6 @@ @file:Suppress("AssignedValueIsNeverRead") -package eu.gaudian.translator.view.exercises +package eu.gaudian.translator.view.new_ecercises import android.annotation.SuppressLint import android.graphics.Bitmap diff --git a/app/src/main/java/eu/gaudian/translator/view/exercises/YouTubeExerciseDialog.kt b/app/src/main/java/eu/gaudian/translator/view/new_ecercises/YouTubeExerciseDialog.kt similarity index 99% rename from app/src/main/java/eu/gaudian/translator/view/exercises/YouTubeExerciseDialog.kt rename to app/src/main/java/eu/gaudian/translator/view/new_ecercises/YouTubeExerciseDialog.kt index 867312c..0bf2e8a 100644 --- a/app/src/main/java/eu/gaudian/translator/view/exercises/YouTubeExerciseDialog.kt +++ b/app/src/main/java/eu/gaudian/translator/view/new_ecercises/YouTubeExerciseDialog.kt @@ -1,4 +1,4 @@ -package eu.gaudian.translator.view.exercises +package eu.gaudian.translator.view.new_ecercises import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box diff --git a/app/src/main/java/eu/gaudian/translator/view/exercises/YouTubeExerciseScreen.kt b/app/src/main/java/eu/gaudian/translator/view/new_ecercises/YouTubeExerciseScreen.kt similarity index 99% rename from app/src/main/java/eu/gaudian/translator/view/exercises/YouTubeExerciseScreen.kt rename to app/src/main/java/eu/gaudian/translator/view/new_ecercises/YouTubeExerciseScreen.kt index 7ffd6be..e76144a 100644 --- a/app/src/main/java/eu/gaudian/translator/view/exercises/YouTubeExerciseScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/new_ecercises/YouTubeExerciseScreen.kt @@ -1,6 +1,6 @@ @file:Suppress("AssignedValueIsNeverRead") -package eu.gaudian.translator.view.exercises +package eu.gaudian.translator.view.new_ecercises import android.annotation.SuppressLint import android.widget.Toast diff --git a/app/src/main/java/eu/gaudian/translator/view/stats/StatsScreen.kt b/app/src/main/java/eu/gaudian/translator/view/stats/StatsScreen.kt index 5d75fd0..f74983f 100644 --- a/app/src/main/java/eu/gaudian/translator/view/stats/StatsScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/stats/StatsScreen.kt @@ -65,13 +65,13 @@ import eu.gaudian.translator.view.composable.AppCard import eu.gaudian.translator.view.composable.AppIcons import eu.gaudian.translator.view.composable.AppOutlinedCard import eu.gaudian.translator.view.dialogs.MissingLanguageDialog -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.StatusWidget -import eu.gaudian.translator.view.vocabulary.widgets.StreakWidget -import eu.gaudian.translator.view.vocabulary.widgets.WeeklyActivityChartWidget +import eu.gaudian.translator.view.stats.widgets.AllVocabularyWidget +import eu.gaudian.translator.view.stats.widgets.CategoryProgressWidget +import eu.gaudian.translator.view.stats.widgets.DueTodayWidget +import eu.gaudian.translator.view.stats.widgets.LevelWidget +import eu.gaudian.translator.view.stats.widgets.StatusWidget +import eu.gaudian.translator.view.stats.widgets.StreakWidget +import eu.gaudian.translator.view.stats.widgets.WeeklyActivityChartWidget import eu.gaudian.translator.viewmodel.LanguageViewModel import eu.gaudian.translator.viewmodel.ProgressViewModel import eu.gaudian.translator.viewmodel.SettingsViewModel diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/widgets/AllVocabularyWidget.kt b/app/src/main/java/eu/gaudian/translator/view/stats/widgets/AllVocabularyWidget.kt similarity index 98% rename from app/src/main/java/eu/gaudian/translator/view/vocabulary/widgets/AllVocabularyWidget.kt rename to app/src/main/java/eu/gaudian/translator/view/stats/widgets/AllVocabularyWidget.kt index 5242e5f..0ba368f 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/widgets/AllVocabularyWidget.kt +++ b/app/src/main/java/eu/gaudian/translator/view/stats/widgets/AllVocabularyWidget.kt @@ -1,4 +1,4 @@ -package eu.gaudian.translator.view.vocabulary.widgets +package eu.gaudian.translator.view.stats.widgets import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/widgets/CategoryWidget.kt b/app/src/main/java/eu/gaudian/translator/view/stats/widgets/CategoryWidget.kt similarity index 99% rename from app/src/main/java/eu/gaudian/translator/view/vocabulary/widgets/CategoryWidget.kt rename to app/src/main/java/eu/gaudian/translator/view/stats/widgets/CategoryWidget.kt index 791401b..fb162fd 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/widgets/CategoryWidget.kt +++ b/app/src/main/java/eu/gaudian/translator/view/stats/widgets/CategoryWidget.kt @@ -1,4 +1,4 @@ -package eu.gaudian.translator.view.vocabulary.widgets +package eu.gaudian.translator.view.stats.widgets import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/widgets/DueTodayWidget.kt b/app/src/main/java/eu/gaudian/translator/view/stats/widgets/DueTodayWidget.kt similarity index 98% rename from app/src/main/java/eu/gaudian/translator/view/vocabulary/widgets/DueTodayWidget.kt rename to app/src/main/java/eu/gaudian/translator/view/stats/widgets/DueTodayWidget.kt index 157da94..b961945 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/widgets/DueTodayWidget.kt +++ b/app/src/main/java/eu/gaudian/translator/view/stats/widgets/DueTodayWidget.kt @@ -1,4 +1,4 @@ -package eu.gaudian.translator.view.vocabulary.widgets +package eu.gaudian.translator.view.stats.widgets import android.annotation.SuppressLint import androidx.compose.foundation.clickable diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/widgets/LanguageLevelWidget.kt b/app/src/main/java/eu/gaudian/translator/view/stats/widgets/LanguageLevelWidget.kt similarity index 98% rename from app/src/main/java/eu/gaudian/translator/view/vocabulary/widgets/LanguageLevelWidget.kt rename to app/src/main/java/eu/gaudian/translator/view/stats/widgets/LanguageLevelWidget.kt index 70b3fcb..d3646f5 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/widgets/LanguageLevelWidget.kt +++ b/app/src/main/java/eu/gaudian/translator/view/stats/widgets/LanguageLevelWidget.kt @@ -1,4 +1,4 @@ -package eu.gaudian.translator.view.vocabulary.widgets +package eu.gaudian.translator.view.stats.widgets import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Column diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/widgets/LevelWidget.kt b/app/src/main/java/eu/gaudian/translator/view/stats/widgets/LevelWidget.kt similarity index 98% rename from app/src/main/java/eu/gaudian/translator/view/vocabulary/widgets/LevelWidget.kt rename to app/src/main/java/eu/gaudian/translator/view/stats/widgets/LevelWidget.kt index 8014e62..589839f 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/widgets/LevelWidget.kt +++ b/app/src/main/java/eu/gaudian/translator/view/stats/widgets/LevelWidget.kt @@ -1,4 +1,4 @@ -package eu.gaudian.translator.view.vocabulary.widgets +package eu.gaudian.translator.view.stats.widgets import androidx.compose.foundation.Image import androidx.compose.foundation.clickable diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/widgets/StageProgressBar.kt b/app/src/main/java/eu/gaudian/translator/view/stats/widgets/StageProgressBar.kt similarity index 99% rename from app/src/main/java/eu/gaudian/translator/view/vocabulary/widgets/StageProgressBar.kt rename to app/src/main/java/eu/gaudian/translator/view/stats/widgets/StageProgressBar.kt index 3d4d565..ea0fd43 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/widgets/StageProgressBar.kt +++ b/app/src/main/java/eu/gaudian/translator/view/stats/widgets/StageProgressBar.kt @@ -1,4 +1,4 @@ -package eu.gaudian.translator.view.vocabulary.widgets +package eu.gaudian.translator.view.stats.widgets import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/widgets/StatusWidget.kt b/app/src/main/java/eu/gaudian/translator/view/stats/widgets/StatusWidget.kt similarity index 99% rename from app/src/main/java/eu/gaudian/translator/view/vocabulary/widgets/StatusWidget.kt rename to app/src/main/java/eu/gaudian/translator/view/stats/widgets/StatusWidget.kt index 75377f1..a1fe70b 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/widgets/StatusWidget.kt +++ b/app/src/main/java/eu/gaudian/translator/view/stats/widgets/StatusWidget.kt @@ -1,4 +1,4 @@ -package eu.gaudian.translator.view.vocabulary.widgets +package eu.gaudian.translator.view.stats.widgets import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/widgets/StreakWidget.kt b/app/src/main/java/eu/gaudian/translator/view/stats/widgets/StreakWidget.kt similarity index 99% rename from app/src/main/java/eu/gaudian/translator/view/vocabulary/widgets/StreakWidget.kt rename to app/src/main/java/eu/gaudian/translator/view/stats/widgets/StreakWidget.kt index 0bb5720..eb53160 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/widgets/StreakWidget.kt +++ b/app/src/main/java/eu/gaudian/translator/view/stats/widgets/StreakWidget.kt @@ -1,4 +1,4 @@ -package eu.gaudian.translator.view.vocabulary.widgets +package eu.gaudian.translator.view.stats.widgets import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/widgets/WeeklyActivityChartWidget.kt b/app/src/main/java/eu/gaudian/translator/view/stats/widgets/WeeklyActivityChartWidget.kt similarity index 99% rename from app/src/main/java/eu/gaudian/translator/view/vocabulary/widgets/WeeklyActivityChartWidget.kt rename to app/src/main/java/eu/gaudian/translator/view/stats/widgets/WeeklyActivityChartWidget.kt index a0294ac..61f7f2e 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/widgets/WeeklyActivityChartWidget.kt +++ b/app/src/main/java/eu/gaudian/translator/view/stats/widgets/WeeklyActivityChartWidget.kt @@ -1,6 +1,6 @@ @file:Suppress("HardCodedStringLiteral") -package eu.gaudian.translator.view.vocabulary.widgets +package eu.gaudian.translator.view.stats.widgets import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/widgets/WidgetButtonOutline.kt b/app/src/main/java/eu/gaudian/translator/view/stats/widgets/WidgetButtonOutline.kt similarity index 98% rename from app/src/main/java/eu/gaudian/translator/view/vocabulary/widgets/WidgetButtonOutline.kt rename to app/src/main/java/eu/gaudian/translator/view/stats/widgets/WidgetButtonOutline.kt index daabd91..876c8b9 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/widgets/WidgetButtonOutline.kt +++ b/app/src/main/java/eu/gaudian/translator/view/stats/widgets/WidgetButtonOutline.kt @@ -1,6 +1,6 @@ @file:Suppress("HardCodedStringLiteral") -package eu.gaudian.translator.view.vocabulary.widgets +package eu.gaudian.translator.view.stats.widgets import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/DashboardContent.kt b/app/src/main/java/eu/gaudian/translator/view/vocabulary/DashboardContent.kt deleted file mode 100644 index 016bdc4..0000000 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/DashboardContent.kt +++ /dev/null @@ -1,671 +0,0 @@ -@file:Suppress("HardCodedStringLiteral", "AssignedValueIsNeverRead", "UnusedMaterial3ScaffoldPaddingParameter") - -package eu.gaudian.translator.view.vocabulary - -import android.annotation.SuppressLint -import androidx.compose.animation.animateContentSize -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.FastOutSlowInEasing -import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.VisibilityThreshold -import androidx.compose.animation.core.spring -import androidx.compose.animation.core.tween -import androidx.compose.foundation.gestures.detectDragGestures -import androidx.compose.foundation.gestures.scrollBy -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.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateListOf -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.graphicsLayer -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.dp -import androidx.compose.ui.zIndex -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.navigation.NavController -import androidx.navigation.compose.rememberNavController -import eu.gaudian.translator.R -import eu.gaudian.translator.model.VocabularyStage -import eu.gaudian.translator.model.WidgetType -import eu.gaudian.translator.utils.findActivity -import eu.gaudian.translator.view.composable.AppCard -import eu.gaudian.translator.view.composable.AppIcons -import eu.gaudian.translator.view.composable.AppOutlinedCard -import eu.gaudian.translator.view.dialogs.MissingLanguageDialog -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.StatusWidget -import eu.gaudian.translator.view.vocabulary.widgets.StreakWidget -import eu.gaudian.translator.view.vocabulary.widgets.WeeklyActivityChartWidget -import eu.gaudian.translator.viewmodel.LanguageViewModel -import eu.gaudian.translator.viewmodel.ProgressViewModel -import eu.gaudian.translator.viewmodel.SettingsViewModel -import eu.gaudian.translator.viewmodel.VocabularyViewModel -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.launch - -@SuppressLint("FrequentlyChangingValue") -@Composable -fun DashboardContent( - navController: NavController, - onShowCustomExerciseDialog: () -> Unit, - startDailyExercise: (Boolean) -> Unit, - onNavigateToCategoryDetail: (Int) -> Unit, - onNavigateToCategoryList: () -> Unit, - onShowWordPairExerciseDialog: () -> Unit, - onScroll: (Boolean) -> Unit = {}, -) { - val activity = LocalContext.current.findActivity() - val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity) - val settingsViewModel: SettingsViewModel = hiltViewModel(viewModelStoreOwner = activity) - val progressViewModel: ProgressViewModel = hiltViewModel(viewModelStoreOwner = activity) - - var showMissingLanguageDialog by remember { mutableStateOf(false) } - var selectedMissingLanguageId by remember { mutableStateOf(null) } - val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity) - - val affectedItems by remember(selectedMissingLanguageId) { - selectedMissingLanguageId?.let { - vocabularyViewModel.getItemsForLanguage(it) - } ?: flowOf(emptyList()) - }.collectAsState(initial = emptyList()) - - if (showMissingLanguageDialog && selectedMissingLanguageId != null) { - MissingLanguageDialog( - showDialog = true, - missingLanguageId = selectedMissingLanguageId!!, - affectedItems = affectedItems, - onDismiss = { showMissingLanguageDialog = false }, - onDelete = { items -> - vocabularyViewModel.deleteVocabularyItemsById(items.map { it.id }) - showMissingLanguageDialog = false - }, - onReplace = { oldId, newId -> - vocabularyViewModel.replaceLanguageId(oldId, newId) - showMissingLanguageDialog = false - }, - onCreate = { newLanguage -> - languageViewModel.addCustomLanguage(newLanguage) - }, - languageViewModel = languageViewModel - ) - } - - AppOutlinedCard { - // We collect the order from DB initially - val initialWidgetOrder by settingsViewModel.widgetOrder.collectAsState(initial = null) - val collapsedWidgetIds by settingsViewModel.collapsedWidgetIds.collectAsState(initial = emptySet()) - val dashboardScrollState by settingsViewModel.dashboardScrollState.collectAsState() - val scope = rememberCoroutineScope() - - if (initialWidgetOrder == null) { - Box( - modifier = Modifier - .fillMaxSize() - .padding(vertical = 64.dp), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } - } else { - // BEST PRACTICE: Use a SnapshotStateList for immediate UI updates. - // We only initialize this once, so DB updates don't reset the list while dragging. - val orderedWidgets = remember { mutableStateListOf() } - - // Sync with DB only on first load - LaunchedEffect(initialWidgetOrder) { - if (orderedWidgets.isEmpty() && !initialWidgetOrder.isNullOrEmpty()) { - orderedWidgets.addAll(initialWidgetOrder!!) - } else if (orderedWidgets.isEmpty()) { - orderedWidgets.addAll(WidgetType.DEFAULT_ORDER) - } - } - - val lazyListState = rememberLazyListState( - initialFirstVisibleItemIndex = dashboardScrollState.first, - initialFirstVisibleItemScrollOffset = dashboardScrollState.second - ) - - // Save scroll state - LaunchedEffect(lazyListState.firstVisibleItemIndex, lazyListState.firstVisibleItemScrollOffset) { - settingsViewModel.saveDashboardScrollState( - lazyListState.firstVisibleItemIndex, - lazyListState.firstVisibleItemScrollOffset - ) - } - - // Detect scroll and notify parent - LaunchedEffect(lazyListState.isScrollInProgress) { - onScroll(lazyListState.isScrollInProgress) - } - DisposableEffect(Unit) { - onDispose { - settingsViewModel.saveDashboardScrollState( - lazyListState.firstVisibleItemIndex, - lazyListState.firstVisibleItemScrollOffset - ) - } - } - - // --- Robust Drag and Drop State --- - val dragDropState = rememberDragDropState( - lazyListState = lazyListState, - onSwap = { fromIndex, toIndex -> - // Swap data immediately for responsiveness - orderedWidgets.apply { - add(toIndex, removeAt(fromIndex)) - } - }, - onDragEnd = { - // Persist to DB only when user drops - settingsViewModel.saveWidgetOrder(orderedWidgets.toList()) - } - ) - - LazyColumn( - state = lazyListState, - modifier = Modifier - .fillMaxSize() - .dragContainer(dragDropState), - contentPadding = PaddingValues(bottom = 160.dp) - ) { - itemsIndexed( - items = orderedWidgets, - key = { _, widget -> widget.id } - ) { index, widgetType -> - - val isDragging = index == dragDropState.draggingItemIndex - - // Calculate translation: distinct logic for dragged vs. stationary items - val translationY = if (isDragging) { - dragDropState.draggingItemOffset - } else { - 0f - } - - Box( - modifier = Modifier - .zIndex(if (isDragging) 1f else 0f) - .graphicsLayer { - this.translationY = translationY - this.shadowElevation = if (isDragging) 8.dp.toPx() else 0f - this.scaleX = if (isDragging) 1.02f else 1f - this.scaleY = if (isDragging) 1.02f else 1f - } - // CRITICAL FIX: Only apply animation to items NOT being dragged. - // This prevents the "flicker" by stopping the layout animation - // from fighting your manual drag offset. - .then( - if (!isDragging) { - Modifier.animateItem( - placementSpec = spring( - stiffness = Spring.StiffnessLow, - visibilityThreshold = IntOffset.VisibilityThreshold - ) - ) - } else { - Modifier - } - ) - ) { - WidgetContainer( - widgetType = widgetType, - isExpanded = widgetType.id !in collapsedWidgetIds, - onExpandedChange = { newExpandedState -> - scope.launch { - settingsViewModel.setWidgetExpandedState(widgetType.id, newExpandedState) - } - }, - onDragStart = { dragDropState.onDragStart(index) }, - onDrag = { dragAmount -> dragDropState.onDrag(dragAmount) }, - onDragEnd = { dragDropState.onDragEnd() }, - onDragCancel = { dragDropState.onDragInterrupted() }, - modifier = Modifier.fillMaxWidth() - ) { - LazyWidget( - widgetType = widgetType, - navController = navController, - vocabularyViewModel = vocabularyViewModel, - progressViewModel = progressViewModel, - onShowCustomExerciseDialog = onShowCustomExerciseDialog, - startDailyExercise = startDailyExercise, - onNavigateToCategoryDetail = onNavigateToCategoryDetail, - onNavigateToCategoryList = onNavigateToCategoryList, - onShowWordPairExerciseDialog = onShowWordPairExerciseDialog, - onMissingLanguage = { missingId -> - selectedMissingLanguageId = missingId - showMissingLanguageDialog = true - } - ) - } - } - } - } - } - } -} - -@Composable -private fun WidgetContainer( - widgetType: WidgetType, - isExpanded: Boolean, - onExpandedChange: (Boolean) -> Unit, - modifier: Modifier = Modifier, - onDragStart: () -> Unit, - onDrag: (Float) -> Unit, - onDragEnd: () -> Unit, - onDragCancel: () -> Unit, - content: @Composable () -> Unit -) { - AppCard( - modifier = modifier.padding(horizontal = 8.dp, vertical = 4.dp), - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .animateContentSize(animationSpec = tween(300, easing = FastOutSlowInEasing)) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(start = 12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = stringResource(widgetType.titleRes), - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.weight(1f) - ) - - IconButton(onClick = { onExpandedChange(!isExpanded) }) { - Icon( - imageVector = if (isExpanded) AppIcons.ArrowDropUp - else AppIcons.ArrowDropDown, - contentDescription = if (isExpanded) stringResource(R.string.text_collapse_widget) - else stringResource(R.string.text_expand_widget) - ) - } - - // Drag Handle with specific pointer input - Icon( - imageVector = AppIcons.DragHandle, - contentDescription = stringResource(R.string.text_drag_to_reorder), - tint = if (isExpanded) MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) - else MaterialTheme.colorScheme.onSurface, - modifier = Modifier - .padding(end = 8.dp, start = 8.dp) - .pointerInput(Unit) { - detectDragGestures( - onDragStart = { _ -> onDragStart() }, - onDrag = { change, dragAmount -> - change.consume() - onDrag(dragAmount.y) - }, - onDragEnd = { onDragEnd() }, - onDragCancel = { onDragCancel() } - ) - } - ) - } - if (isExpanded) { - content() - } - } - } -} - -// -------------------------------------------------------------------------------- -// Fixed Drag and Drop Logic -// -------------------------------------------------------------------------------- - -@Composable -fun rememberDragDropState( - lazyListState: LazyListState, - onSwap: (Int, Int) -> Unit, - onDragEnd: () -> Unit -): DragDropState { - val scope = rememberCoroutineScope() - return remember(lazyListState, scope) { - DragDropState( - state = lazyListState, - onSwap = onSwap, - onDragFinished = onDragEnd, - scope = scope - ) - } -} - -fun Modifier.dragContainer(dragDropState: DragDropState): Modifier { - return this.pointerInput(dragDropState) { - // Just allows the modifier to exist in the chain, logic is in the handle - } -} - -class DragDropState( - private val state: LazyListState, - private val onSwap: (Int, Int) -> Unit, - private val onDragFinished: () -> Unit, - private val scope: CoroutineScope -) { - var draggingItemIndex by mutableIntStateOf(-1) - private set - - private val _draggingItemOffset = Animatable(0f) - val draggingItemOffset: Float - get() = _draggingItemOffset.value - - private val scrollChannel = Channel(Channel.CONFLATED) - - init { - scope.launch { - for (scrollAmount in scrollChannel) { - if (scrollAmount != 0f) { - state.scrollBy(scrollAmount) - checkSwap() - } - } - } - } - - fun onDragStart(index: Int) { - draggingItemIndex = index - scope.launch { _draggingItemOffset.snapTo(0f) } - } - - fun onDrag(dragAmount: Float) { - if (draggingItemIndex == -1) return - - scope.launch { - _draggingItemOffset.snapTo(_draggingItemOffset.value + dragAmount) - checkSwap() - checkOverscroll() - } - } - - private fun checkSwap() { - val draggedIndex = draggingItemIndex - if (draggedIndex == -1) return - - val visibleItems = state.layoutInfo.visibleItemsInfo - val draggedItemInfo = visibleItems.find { it.index == draggedIndex } ?: return - - // Calculate the visual center of the dragged item - val draggedCenter = draggedItemInfo.offset + (draggedItemInfo.size / 2) + _draggingItemOffset.value - - // Find a target to swap with - // FIX: We strictly check if we have crossed the CENTER of the target item. - // This acts as a hysteresis buffer to prevent flickering at the edges. - val targetItem = visibleItems.find { item -> - item.index != draggedIndex && - draggedCenter > item.offset && - draggedCenter < (item.offset + item.size) - } - - if (targetItem != null) { - // Extra Check: Ensure we have actually crossed the midpoint of the target - val targetCenter = itemCenter(targetItem.offset, targetItem.size) - val isAboveAndMovingDown = draggedIndex < targetItem.index && draggedCenter > targetCenter - val isBelowAndMovingUp = draggedIndex > targetItem.index && draggedCenter < targetCenter - - if (isAboveAndMovingDown || isBelowAndMovingUp) { - val targetIndex = targetItem.index - - // 1. Swap Data - onSwap(draggedIndex, targetIndex) - - // 2. Adjust Offset - // We calculate the physical distance the item moved in the layout (e.g. 150px). - // We subtract this from the current drag offset to keep the item visually stationary under the finger. - val layoutJumpDistance = (targetItem.offset - draggedItemInfo.offset).toFloat() - - scope.launch { - _draggingItemOffset.snapTo(_draggingItemOffset.value - layoutJumpDistance) - } - - // 3. Update Index - draggingItemIndex = targetIndex - } - } - } - - private fun itemCenter(offset: Int, size: Int): Float { - return offset + (size / 2f) - } - - private fun checkOverscroll() { - val draggedIndex = draggingItemIndex - if (draggedIndex == -1) { - scrollChannel.trySend(0f) - return - } - - val layoutInfo = state.layoutInfo - val visibleItems = layoutInfo.visibleItemsInfo - val draggedItemInfo = visibleItems.find { it.index == draggedIndex } ?: return - - val viewportStart = layoutInfo.viewportStartOffset - val viewportEnd = layoutInfo.viewportEndOffset - // Increased threshold slightly for smoother top-edge scrolling - val boundsStart = viewportStart + (viewportEnd * 0.15f) - val boundsEnd = viewportEnd - (viewportEnd * 0.15f) - - val itemTop = draggedItemInfo.offset + _draggingItemOffset.value - val itemBottom = itemTop + draggedItemInfo.size - - val scrollAmount = when { - itemTop < boundsStart -> -10f // Slower, more controlled scroll speed - itemBottom > boundsEnd -> 10f - else -> 0f - } - - scrollChannel.trySend(scrollAmount) - } - - fun onDragEnd() { - resetDrag() - onDragFinished() - } - - fun onDragInterrupted() { - resetDrag() - } - - private fun resetDrag() { - draggingItemIndex = -1 - scrollChannel.trySend(0f) - scope.launch { _draggingItemOffset.snapTo(0f) } - } -} - -// -------------------------------------------------------------------------------- -// Remainder of your existing components -// -------------------------------------------------------------------------------- - -@Composable -private fun LazyWidget( - widgetType: WidgetType, - navController: NavController, - vocabularyViewModel: VocabularyViewModel, - progressViewModel: ProgressViewModel, - onShowCustomExerciseDialog: () -> Unit, - startDailyExercise: (Boolean) -> Unit, - onNavigateToCategoryDetail: (Int) -> Unit, - onNavigateToCategoryList: () -> Unit, - onShowWordPairExerciseDialog: () -> Unit, - onMissingLanguage: (Int) -> Unit -) { - when (widgetType) { - - - WidgetType.Status -> LazyStatusWidget( - vocabularyViewModel = vocabularyViewModel, - onNavigateToNew = { navController.navigate("vocabulary_sorting?mode=NEW") }, - onNavigateToDuplicates = { navController.navigate("vocabulary_sorting?mode=DUPLICATES") }, - onNavigateToFaulty = { navController.navigate("vocabulary_sorting?mode=FAULTY") }, - onNavigateToNoGrammar = { navController.navigate("no_grammar_items") }, - onNavigateToMissingLanguage = onMissingLanguage - ) - - else -> { - // Regular widgets that load immediately - when (widgetType) { - WidgetType.Streak -> StreakWidget( - streak = progressViewModel.streak.collectAsState(initial = 0).value, - lastSevenDays = progressViewModel.lastSevenDays.collectAsState().value, - dueTodayCount = progressViewModel.dueTodayCount.collectAsState().value, - wordsCompleted = progressViewModel.totalWordsCompleted.collectAsState().value, - onStatisticsClicked = { navController.navigate("vocabulary_heatmap") } - ) - - WidgetType.WeeklyActivityChart -> WeeklyActivityChartWidget( - weeklyStats = progressViewModel.weeklyActivityStats.collectAsState().value - ) - - WidgetType.AllVocabulary -> AllVocabularyWidget( - vocabularyViewModel = vocabularyViewModel, - onOpenAllVocabulary = { navController.navigate("vocabulary_list/false/null") }, - onStageClicked = { stage -> navController.navigate("vocabulary_list/false/$stage") } - ) - - WidgetType.DueToday -> DueTodayWidget( - vocabularyViewModel = vocabularyViewModel, - onStageClicked = { stage -> navController.navigate("vocabulary_list/false/$stage") } - ) - - WidgetType.CategoryProgress -> CategoryProgressWidget( - onCategoryClicked = { category -> - category?.let { onNavigateToCategoryDetail(it.id) } - }, - onViewAllClicked = onNavigateToCategoryList - ) - - WidgetType.Levels -> LevelWidget( - totalWords = vocabularyViewModel.vocabularyItems.collectAsState().value.size, - learnedWords = vocabularyViewModel.stageStats.collectAsState().value - .firstOrNull { it.stage == VocabularyStage.LEARNED }?.itemCount ?: 0, - onNavigateToProgress = { navController.navigate("language_progress") } - ) - - } - } - } -} - -@Composable -private fun LazyStatusWidget( - vocabularyViewModel: VocabularyViewModel, - onNavigateToNew: () -> Unit, - onNavigateToDuplicates: () -> Unit, - onNavigateToFaulty: () -> Unit, - onNavigateToNoGrammar: () -> Unit, - onNavigateToMissingLanguage: (Int) -> Unit -) { - var isLoading by remember { mutableStateOf(true) } - - // Collect all flows asynchronously - val newItemsCount by vocabularyViewModel.newItemsCount.collectAsState() - val duplicateCount by vocabularyViewModel.duplicateItemsCount.collectAsState() - val faultyItemsCount by vocabularyViewModel.faultyItemsCount.collectAsState() - val itemsWithoutGrammarCount by vocabularyViewModel.itemsWithoutGrammarCount.collectAsState() - val missingLanguageInfo by vocabularyViewModel.missingLanguageInfo.collectAsState() - - LaunchedEffect( - newItemsCount, - duplicateCount, - faultyItemsCount, - itemsWithoutGrammarCount, - missingLanguageInfo - ) { - delay(100) - isLoading = false - } - - if (isLoading) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 32.dp), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator(modifier = Modifier.padding(16.dp)) - } - } else { - StatusWidget( - onNavigateToNew = onNavigateToNew, - onNavigateToDuplicates = onNavigateToDuplicates, - onNavigateToFaulty = onNavigateToFaulty, - onNavigateToNoGrammar = onNavigateToNoGrammar, - onNavigateToMissingLanguage = onNavigateToMissingLanguage - ) - } -} - -@Preview -@Composable -fun DashboardContentPreview() { - val navController = rememberNavController() - DashboardContent( - navController = navController, - onShowCustomExerciseDialog = {}, - onNavigateToCategoryDetail = {}, - startDailyExercise = {}, - onNavigateToCategoryList = {}, - onShowWordPairExerciseDialog = {}, - ) -} - -@Preview -@Composable -fun WidgetContainerPreview() { - WidgetContainer( - widgetType = WidgetType.Streak, - isExpanded = true, - onExpandedChange = {}, - onDragStart = { } , - onDrag = { }, - onDragEnd = { }, - onDragCancel = { } - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - contentAlignment = Alignment.Center - ) { - Text("Preview Content") - } - } -} \ No newline at end of file diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/LanguageProgressScreen.kt b/app/src/main/java/eu/gaudian/translator/view/vocabulary/LanguageJourneyScreen.kt similarity index 98% rename from app/src/main/java/eu/gaudian/translator/view/vocabulary/LanguageProgressScreen.kt rename to app/src/main/java/eu/gaudian/translator/view/vocabulary/LanguageJourneyScreen.kt index 9bb562d..df2d736 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/LanguageProgressScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/vocabulary/LanguageJourneyScreen.kt @@ -67,7 +67,7 @@ import eu.gaudian.translator.view.composable.AppTopAppBar import eu.gaudian.translator.viewmodel.ProgressViewModel @Composable -fun LanguageProgressScreen(navController: NavController) { +fun LanguageJourneyScreen(navController: NavController) { val activity = LocalContext.current.findActivity() val progressViewModel : ProgressViewModel = hiltViewModel(activity) @@ -379,6 +379,6 @@ private fun LevelDetailDialog(level: MyAppLanguageLevel, onDismiss: () -> Unit) @Preview(showBackground = true) @Composable -fun LanguageProgressScreenPreview() { - LanguageProgressScreen(navController = NavController(LocalContext.current)) +fun LanguageJourneyScreenPreview() { + LanguageJourneyScreen(navController = NavController(LocalContext.current)) } \ No newline at end of file diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/StageDetailScreen.kt b/app/src/main/java/eu/gaudian/translator/view/vocabulary/StageDetailScreen.kt index d90a83e..e91c832 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/StageDetailScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/vocabulary/StageDetailScreen.kt @@ -16,7 +16,7 @@ import eu.gaudian.translator.model.VocabularyStage import eu.gaudian.translator.utils.findActivity import eu.gaudian.translator.view.composable.AppScaffold import eu.gaudian.translator.view.composable.AppTopAppBar -import eu.gaudian.translator.view.vocabulary.widgets.DetailedStageProgressBar +import eu.gaudian.translator.view.stats.widgets.DetailedStageProgressBar import eu.gaudian.translator.viewmodel.VocabularyViewModel @Composable diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularyExerciseHostScreen.kt b/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularyExerciseHostScreen.kt index 6ea97d0..45fe09a 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularyExerciseHostScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularyExerciseHostScreen.kt @@ -33,6 +33,8 @@ import eu.gaudian.translator.utils.Log import eu.gaudian.translator.utils.findActivity import eu.gaudian.translator.view.composable.AppAlertDialog import eu.gaudian.translator.view.composable.Screen +import eu.gaudian.translator.view.exercises.ExerciseControls +import eu.gaudian.translator.view.exercises.ExerciseProgressIndicator import eu.gaudian.translator.viewmodel.ScreenState import eu.gaudian.translator.viewmodel.VocabularyExerciseViewModel import eu.gaudian.translator.viewmodel.VocabularyViewModel diff --git a/app/src/main/res/values-de-rDE/strings.xml b/app/src/main/res/values-de-rDE/strings.xml index 3ff9e4a..ac91791 100644 --- a/app/src/main/res/values-de-rDE/strings.xml +++ b/app/src/main/res/values-de-rDE/strings.xml @@ -282,7 +282,7 @@ Übung starten (%1$d) Anzahl der Karten: %1$d / %2$d Keine Karten für die gewählten Filter gefunden. - Die richtige Antwort wählen + Die richtige Antwort wählen Optionen Karten mischen Beenden diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 4217e32..7e92e29 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -281,7 +281,6 @@ Iniciar Exercício (%1$d) Número de Cartões: %1$d / %2$d Nenhum cartão encontrado para os filtros selecionados. - Escolher Tipos de Exercício Opções Embaralhar Cartões Sair @@ -326,7 +325,6 @@ Carregando estatísticas… para %1$s Traduzir de %1$s - Monte a palavra aqui Resposta correta: %1$s Sair do Exercício? Tem certeza de que quer sair? O seu progresso nesta sessão será perdido. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0beb2a5..36a0ca0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -19,7 +19,7 @@ Toggle Menu Translation History - Choose Exercise Types + Choose the right translation Clear All @@ -694,7 +694,7 @@ Are you sure you want to delete this category? Are you sure you want to quit? Are you sure you want to quit? Your progress in this session will be lost. - Assemble the word here + Bring the letters into the right order Assign a different language to these items. Assign these items: Authentication is required and has failed or has not yet been provided.