Refactor the project structure by reorganizing exercise, category, and statistics components, and extract AppCard into a dedicated file.
This commit is contained in:
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -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
|
||||
|
||||
@@ -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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,7 @@ data class FabMenuItem(
|
||||
)
|
||||
|
||||
|
||||
@Deprecated("We don't want to use floating butto menus anymore")
|
||||
@Composable
|
||||
fun AppFabMenu(
|
||||
items: List<FabMenuItem>,
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
@@ -1,4 +1,4 @@
|
||||
package eu.gaudian.translator.view.vocabulary
|
||||
package eu.gaudian.translator.view.exercises
|
||||
|
||||
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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) }
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user