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.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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
@Composable
|
||||||
fun AppFabMenu(
|
fun AppFabMenu(
|
||||||
items: List<FabMenuItem>,
|
items: List<FabMenuItem>,
|
||||||
|
|||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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) }
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
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))
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user