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.navArgument
import androidx.navigation.navigation import androidx.navigation.navigation
import eu.gaudian.translator.model.VocabularyStage 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.composable.Screen
import eu.gaudian.translator.view.dictionary.DictionaryResultScreen import eu.gaudian.translator.view.dictionary.DictionaryResultScreen
import eu.gaudian.translator.view.dictionary.EtymologyResultScreen import eu.gaudian.translator.view.dictionary.EtymologyResultScreen
import eu.gaudian.translator.view.dictionary.MainDictionaryScreen 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.DailyReviewScreen
import eu.gaudian.translator.view.home.HomeScreen import eu.gaudian.translator.view.home.HomeScreen
import eu.gaudian.translator.view.library.LibraryScreen 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.DictionaryOptionsScreen
import eu.gaudian.translator.view.settings.TranslationSettingsScreen import eu.gaudian.translator.view.settings.TranslationSettingsScreen
import eu.gaudian.translator.view.settings.settingsGraph import eu.gaudian.translator.view.settings.settingsGraph
import eu.gaudian.translator.view.stats.StatsScreen import eu.gaudian.translator.view.stats.StatsScreen
import eu.gaudian.translator.view.translation.TranslationScreen import eu.gaudian.translator.view.translation.TranslationScreen
import eu.gaudian.translator.view.vocabulary.AllCardsListScreen import eu.gaudian.translator.view.vocabulary.AllCardsListScreen
import eu.gaudian.translator.view.vocabulary.CategoryDetailScreen import eu.gaudian.translator.view.vocabulary.LanguageJourneyScreen
import eu.gaudian.translator.view.vocabulary.CategoryListScreen
import eu.gaudian.translator.view.vocabulary.LanguageProgressScreen
import eu.gaudian.translator.view.vocabulary.NewWordReviewScreen import eu.gaudian.translator.view.vocabulary.NewWordReviewScreen
import eu.gaudian.translator.view.vocabulary.NewWordScreen import eu.gaudian.translator.view.vocabulary.NewWordScreen
import eu.gaudian.translator.view.vocabulary.NoGrammarItemsScreen import eu.gaudian.translator.view.vocabulary.NoGrammarItemsScreen
@@ -269,7 +269,7 @@ fun NavGraphBuilder.libraryGraph(navController: NavHostController) {
) )
} }
composable("language_progress") { composable("language_progress") {
LanguageProgressScreen( LanguageJourneyScreen(
navController = navController navController = navController
) )
@@ -439,7 +439,7 @@ fun NavGraphBuilder.statsGraph(
) )
} }
composable(NavigationRoutes.STATS_LANGUAGE_PROGRESS) { composable(NavigationRoutes.STATS_LANGUAGE_PROGRESS) {
LanguageProgressScreen( LanguageJourneyScreen(
navController = navController navController = navController
) )
} }

View File

@@ -1,8 +1,13 @@
@file:Suppress("HardCodedStringLiteral") @file:Suppress("HardCodedStringLiteral")
package eu.gaudian.translator.view.vocabulary package eu.gaudian.translator.view.categories
import android.annotation.SuppressLint 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.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column 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.DeleteCategoryDialog
import eu.gaudian.translator.view.dialogs.DeleteItemsDialog import eu.gaudian.translator.view.dialogs.DeleteItemsDialog
import eu.gaudian.translator.view.dialogs.EditCategoryDialog import eu.gaudian.translator.view.dialogs.EditCategoryDialog
import eu.gaudian.translator.view.vocabulary.widgets.CategoryProgressCircle import eu.gaudian.translator.view.vocabulary.AllCardsListScreen
import eu.gaudian.translator.view.vocabulary.widgets.ChartLegend 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.CategoryProgress
import eu.gaudian.translator.viewmodel.CategoryViewModel import eu.gaudian.translator.viewmodel.CategoryViewModel
import eu.gaudian.translator.viewmodel.ExportImportViewModel import eu.gaudian.translator.viewmodel.ExportImportViewModel
@@ -244,10 +250,10 @@ fun CategoryDetailScreen(
) )
// Category Header Card with Progress and Action Buttons (animated) // Category Header Card with Progress and Action Buttons (animated)
androidx.compose.animation.AnimatedVisibility( AnimatedVisibility(
visible = isHeaderVisible, visible = isHeaderVisible,
enter = androidx.compose.animation.fadeIn() + androidx.compose.animation.expandVertically(), enter = fadeIn() + expandVertically(),
exit = androidx.compose.animation.fadeOut() + androidx.compose.animation.shrinkVertically() exit = fadeOut() + shrinkVertically()
) { ) {
CategoryHeaderCard( CategoryHeaderCard(
subtitle = subtitle, subtitle = subtitle,

View File

@@ -1,6 +1,6 @@
@file:Suppress("AssignedValueIsNeverRead") @file:Suppress("AssignedValueIsNeverRead")
package eu.gaudian.translator.view.vocabulary package eu.gaudian.translator.view.categories
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background 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.composable.AppTopAppBar
import eu.gaudian.translator.view.dialogs.AddCategoryDialog import eu.gaudian.translator.view.dialogs.AddCategoryDialog
import eu.gaudian.translator.view.dialogs.DeleteMultipleCategoriesDialog import eu.gaudian.translator.view.dialogs.DeleteMultipleCategoriesDialog
import eu.gaudian.translator.view.vocabulary.widgets.CategoryCircleType import eu.gaudian.translator.view.stats.widgets.CategoryCircleType
import eu.gaudian.translator.view.vocabulary.widgets.CategoryProgressCircle import eu.gaudian.translator.view.stats.widgets.CategoryProgressCircle
import eu.gaudian.translator.viewmodel.CategoryViewModel import eu.gaudian.translator.viewmodel.CategoryViewModel
import eu.gaudian.translator.viewmodel.ProgressViewModel 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 @Composable
fun AppFabMenu( fun AppFabMenu(
items: List<FabMenuItem>, items: List<FabMenuItem>,

View File

@@ -49,6 +49,7 @@ interface TabItem {
val title: String val title: String
val icon: ImageVector val icon: ImageVector
} }
@Deprecated("Migrate to new (like used in LibraryScreen")
@SuppressLint("UnusedBoxWithConstraintsScope", "LocalContextResourcesRead", "DiscouragedApi", @SuppressLint("UnusedBoxWithConstraintsScope", "LocalContextResourcesRead", "DiscouragedApi",
"SuspiciousIndentation" "SuspiciousIndentation"
) )

View File

@@ -2,23 +2,17 @@
package eu.gaudian.translator.view.composable 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.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors import androidx.compose.material3.ButtonColors
@@ -28,26 +22,19 @@ import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.CheckboxDefaults import androidx.compose.material3.CheckboxDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedCard import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.Color
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.vector.ImageVector 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.R
import eu.gaudian.translator.ui.theme.ThemePreviews import eu.gaudian.translator.ui.theme.ThemePreviews
import eu.gaudian.translator.ui.theme.semanticColors 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 { object ComponentDefaults {
@@ -90,218 +73,6 @@ object ComponentDefaults {
const val ALPHA_LOW = 0.3f 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. * The primary button for the most important actions.
* *
@@ -636,6 +407,7 @@ fun WrongOutlinedButtonPreview(){
WrongOutlinedButton(onClick = { }, text = stringResource(R.string.label_continue)) WrongOutlinedButton(onClick = { }, text = stringResource(R.string.label_continue))
} }
//This is basically just a wrapper for screens to control width (tablet mode) etc.
@Composable @Composable
fun AppOutlinedCard( fun AppOutlinedCard(
modifier: Modifier = Modifier, 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.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row 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.AppOutlinedTextField
import eu.gaudian.translator.view.composable.CorrectButton import eu.gaudian.translator.view.composable.CorrectButton
import eu.gaudian.translator.view.composable.WrongButton import eu.gaudian.translator.view.composable.WrongButton
import eu.gaudian.translator.view.vocabulary.VocabularyExerciseAction
import eu.gaudian.translator.view.vocabulary.VocabularyExerciseState
@Composable @Composable
fun ExerciseControls( 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 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.AppCard
import eu.gaudian.translator.view.composable.Screen import eu.gaudian.translator.view.composable.Screen
import eu.gaudian.translator.view.settings.SettingsRoutes 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 import eu.gaudian.translator.viewmodel.ProgressViewModel
@Composable @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.FastOutSlowInEasing
import androidx.compose.animation.core.LinearEasing 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.Arrangement
import androidx.compose.foundation.layout.Column 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.runtime.Composable
import androidx.compose.ui.Modifier 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.AnimatedVisibility
import androidx.compose.animation.core.tween 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.AppButton
import eu.gaudian.translator.view.composable.AppIcons import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.ComponentDefaults 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.AnswerResult
import eu.gaudian.translator.viewmodel.ExerciseSessionState import eu.gaudian.translator.viewmodel.ExerciseSessionState
import eu.gaudian.translator.viewmodel.ExerciseViewModel 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.Box
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth

View File

@@ -1,6 +1,6 @@
@file:Suppress("AssignedValueIsNeverRead") @file:Suppress("AssignedValueIsNeverRead")
package eu.gaudian.translator.view.exercises package eu.gaudian.translator.view.new_ecercises
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.compose.foundation.background import androidx.compose.foundation.background

View File

@@ -1,6 +1,6 @@
@file:Suppress("HardCodedStringLiteral") @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.Image
import androidx.compose.foundation.layout.Arrangement 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.BorderStroke
import androidx.compose.foundation.background 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.BorderStroke
import androidx.compose.foundation.background import androidx.compose.foundation.background
@@ -737,9 +737,9 @@ fun NumberOfCardsSection(
availableQuickSelections.forEach { value -> availableQuickSelections.forEach { value ->
AppOutlinedButton( AppOutlinedButton(
onClick = { onAmountChanged(value) }, 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)) Spacer(modifier = Modifier.height(12.dp))
QuestionTypeCard( QuestionTypeCard(
title = stringResource(R.string.label_multiple_choice_exercise), 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, icon = AppIcons.CheckList,
isSelected = selectedTypes.contains(VocabularyExerciseType.MULTIPLE_CHOICE), isSelected = selectedTypes.contains(VocabularyExerciseType.MULTIPLE_CHOICE),
onClick = { onTypeSelected(VocabularyExerciseType.MULTIPLE_CHOICE) } onClick = { onTypeSelected(VocabularyExerciseType.MULTIPLE_CHOICE) }

View File

@@ -1,6 +1,6 @@
@file:Suppress("AssignedValueIsNeverRead") @file:Suppress("AssignedValueIsNeverRead")
package eu.gaudian.translator.view.exercises package eu.gaudian.translator.view.new_ecercises
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.graphics.Bitmap 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.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box

View File

@@ -1,6 +1,6 @@
@file:Suppress("AssignedValueIsNeverRead") @file:Suppress("AssignedValueIsNeverRead")
package eu.gaudian.translator.view.exercises package eu.gaudian.translator.view.new_ecercises
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.widget.Toast 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.AppIcons
import eu.gaudian.translator.view.composable.AppOutlinedCard import eu.gaudian.translator.view.composable.AppOutlinedCard
import eu.gaudian.translator.view.dialogs.MissingLanguageDialog import eu.gaudian.translator.view.dialogs.MissingLanguageDialog
import eu.gaudian.translator.view.vocabulary.widgets.AllVocabularyWidget import eu.gaudian.translator.view.stats.widgets.AllVocabularyWidget
import eu.gaudian.translator.view.vocabulary.widgets.CategoryProgressWidget import eu.gaudian.translator.view.stats.widgets.CategoryProgressWidget
import eu.gaudian.translator.view.vocabulary.widgets.DueTodayWidget import eu.gaudian.translator.view.stats.widgets.DueTodayWidget
import eu.gaudian.translator.view.vocabulary.widgets.LevelWidget import eu.gaudian.translator.view.stats.widgets.LevelWidget
import eu.gaudian.translator.view.vocabulary.widgets.StatusWidget import eu.gaudian.translator.view.stats.widgets.StatusWidget
import eu.gaudian.translator.view.vocabulary.widgets.StreakWidget import eu.gaudian.translator.view.stats.widgets.StreakWidget
import eu.gaudian.translator.view.vocabulary.widgets.WeeklyActivityChartWidget import eu.gaudian.translator.view.stats.widgets.WeeklyActivityChartWidget
import eu.gaudian.translator.viewmodel.LanguageViewModel import eu.gaudian.translator.viewmodel.LanguageViewModel
import eu.gaudian.translator.viewmodel.ProgressViewModel import eu.gaudian.translator.viewmodel.ProgressViewModel
import eu.gaudian.translator.viewmodel.SettingsViewModel 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.Arrangement
import androidx.compose.foundation.layout.Box 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.animateFloatAsState
import androidx.compose.animation.core.tween 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 android.annotation.SuppressLint
import androidx.compose.foundation.clickable 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.Image
import androidx.compose.foundation.layout.Column 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.Image
import androidx.compose.foundation.clickable 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.animateFloatAsState
import androidx.compose.animation.core.tween 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.clickable
import androidx.compose.foundation.layout.Arrangement 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.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement

View File

@@ -1,6 +1,6 @@
@file:Suppress("HardCodedStringLiteral") @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.animateFloatAsState
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween

View File

@@ -1,6 +1,6 @@
@file:Suppress("HardCodedStringLiteral") @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.BorderStroke
import androidx.compose.foundation.layout.Arrangement 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 import eu.gaudian.translator.viewmodel.ProgressViewModel
@Composable @Composable
fun LanguageProgressScreen(navController: NavController) { fun LanguageJourneyScreen(navController: NavController) {
val activity = LocalContext.current.findActivity() val activity = LocalContext.current.findActivity()
val progressViewModel : ProgressViewModel = hiltViewModel(activity) val progressViewModel : ProgressViewModel = hiltViewModel(activity)
@@ -379,6 +379,6 @@ private fun LevelDetailDialog(level: MyAppLanguageLevel, onDismiss: () -> Unit)
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
fun LanguageProgressScreenPreview() { fun LanguageJourneyScreenPreview() {
LanguageProgressScreen(navController = NavController(LocalContext.current)) 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.utils.findActivity
import eu.gaudian.translator.view.composable.AppScaffold import eu.gaudian.translator.view.composable.AppScaffold
import eu.gaudian.translator.view.composable.AppTopAppBar import eu.gaudian.translator.view.composable.AppTopAppBar
import eu.gaudian.translator.view.vocabulary.widgets.DetailedStageProgressBar import eu.gaudian.translator.view.stats.widgets.DetailedStageProgressBar
import eu.gaudian.translator.viewmodel.VocabularyViewModel import eu.gaudian.translator.viewmodel.VocabularyViewModel
@Composable @Composable

View File

@@ -33,6 +33,8 @@ import eu.gaudian.translator.utils.Log
import eu.gaudian.translator.utils.findActivity import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.AppAlertDialog import eu.gaudian.translator.view.composable.AppAlertDialog
import eu.gaudian.translator.view.composable.Screen 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.ScreenState
import eu.gaudian.translator.viewmodel.VocabularyExerciseViewModel import eu.gaudian.translator.viewmodel.VocabularyExerciseViewModel
import eu.gaudian.translator.viewmodel.VocabularyViewModel 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="label_start_exercise_2d">Übung starten (%1$d)</string>
<string name="number_of_cards">Anzahl der Karten: %1$d / %2$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="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="options">Optionen</string>
<string name="shuffle_cards">Karten mischen</string> <string name="shuffle_cards">Karten mischen</string>
<string name="quit">Beenden</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="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="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="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="options">Opções</string>
<string name="shuffle_cards">Embaralhar Cartões</string> <string name="shuffle_cards">Embaralhar Cartões</string>
<string name="quit">Sair</string> <string name="quit">Sair</string>
@@ -326,7 +325,6 @@
<string name="statistics_are_loading">Carregando estatísticas…</string> <string name="statistics_are_loading">Carregando estatísticas…</string>
<string name="to_d">para %1$s</string> <string name="to_d">para %1$s</string>
<string name="label_translate_from_2d">Traduzir de %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="correct_answer">Resposta correta: %1$s</string>
<string name="label_quit_exercise_qm">Sair do Exercício?</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> <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_toggle_menu">Toggle Menu</string>
<string name="cd_translation_history">Translation History</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> <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_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">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_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_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_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> <string name="text_authentication_is_required_and_has_failed">Authentication is required and has failed or has not yet been provided.</string>