Refactor the project structure by reorganizing exercise, category, and statistics components, and extract AppCard into a dedicated file.

This commit is contained in:
jonasgaudian
2026-02-18 20:54:18 +01:00
parent 8f42fa79ef
commit 37d8c2a6c5
40 changed files with 324 additions and 964 deletions

View File

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

View File

@@ -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,

View File

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

View File

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

View File

@@ -52,6 +52,7 @@ data class FabMenuItem(
)
@Deprecated("We don't want to use floating butto menus anymore")
@Composable
fun AppFabMenu(
items: List<FabMenuItem>,

View File

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

View File

@@ -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,

View File

@@ -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(

View File

@@ -1,4 +1,4 @@
package eu.gaudian.translator.view.vocabulary
package eu.gaudian.translator.view.exercises
import androidx.compose.animation.core.animateFloatAsState

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<Int?>(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<WidgetType>() }
// 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<Float>(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")
}
}
}

View File

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

View File

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

View File

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

View File

@@ -282,7 +282,7 @@
<string name="label_start_exercise_2d">Übung starten (%1$d)</string>
<string name="number_of_cards">Anzahl der Karten: %1$d / %2$d</string>
<string name="no_cards_found_for_the_selected_filters">Keine Karten für die gewählten Filter gefunden.</string>
<string name="label_choose_exercise_types">Die richtige Antwort wählen</string>
<string name="label_multiple_choice_desc">Die richtige Antwort wählen</string>
<string name="options">Optionen</string>
<string name="shuffle_cards">Karten mischen</string>
<string name="quit">Beenden</string>

View File

@@ -281,7 +281,6 @@
<string name="label_start_exercise_2d">Iniciar Exercício (%1$d)</string>
<string name="number_of_cards">Número de Cartões: %1$d / %2$d</string>
<string name="no_cards_found_for_the_selected_filters">Nenhum cartão encontrado para os filtros selecionados.</string>
<string name="label_choose_exercise_types">Escolher Tipos de Exercício</string>
<string name="options">Opções</string>
<string name="shuffle_cards">Embaralhar Cartões</string>
<string name="quit">Sair</string>
@@ -326,7 +325,6 @@
<string name="statistics_are_loading">Carregando estatísticas…</string>
<string name="to_d">para %1$s</string>
<string name="label_translate_from_2d">Traduzir de %1$s</string>
<string name="text_assemble_the_word_here">Monte a palavra aqui</string>
<string name="correct_answer">Resposta correta: %1$s</string>
<string name="label_quit_exercise_qm">Sair do Exercício?</string>
<string name="text_are_you_sure_you_want_to_quit_your_progress_in_this_session_will_be_lost">Tem certeza de que quer sair? O seu progresso nesta sessão será perdido.</string>

View File

@@ -19,7 +19,7 @@
<string name="cd_toggle_menu">Toggle Menu</string>
<string name="cd_translation_history">Translation History</string>
<string name="label_choose_exercise_types">Choose Exercise Types</string>
<string name="label_multiple_choice_desc">Choose the right translation</string>
<string name="label_clear_all">Clear All</string>
@@ -694,7 +694,7 @@
<string name="text_are_you_sure_you_want_to_delete_this_category">Are you sure you want to delete this category?</string>
<string name="text_are_you_sure_you_want_to_quit">Are you sure you want to quit?</string>
<string name="text_are_you_sure_you_want_to_quit_your_progress_in_this_session_will_be_lost">Are you sure you want to quit? Your progress in this session will be lost.</string>
<string name="text_assemble_the_word_here">Assemble the word here</string>
<string name="text_assemble_the_word_here">Bring the letters into the right order</string>
<string name="text_assign_a_different_language_items">Assign a different language to these items.</string>
<string name="text_assign_these_items_2d">Assign these items:</string>
<string name="text_authentication_is_required_and_has_failed">Authentication is required and has failed or has not yet been provided.</string>