implement statsGraph and refactor StatsScreen with drag-and-drop widget reordering
This commit is contained in:
@@ -253,7 +253,15 @@ fun TranslatorApp(
|
||||
val currentDestination = navBackStackEntry?.destination
|
||||
val selectedScreen = Screen.fromDestination(currentDestination)
|
||||
|
||||
@Suppress("HardCodedStringLiteral") val isBottomBarHidden = currentDestination?.route?.startsWith("vocabulary_exercise") == true
|
||||
val isBottomBarHidden = currentDestination?.hierarchy?.any { destination ->
|
||||
destination.route in setOf(
|
||||
Screen.Translation.route,
|
||||
Screen.Vocabulary.route,
|
||||
Screen.Dictionary.route,
|
||||
Screen.Exercises.route,
|
||||
Screen.Settings.route
|
||||
)
|
||||
} == true || currentDestination?.route == "start_exercise"
|
||||
val showBottomNavLabels by settingsViewModel.showBottomNavLabels.collectAsStateWithLifecycle(initialValue = false)
|
||||
|
||||
BottomNavigationBar(
|
||||
@@ -262,6 +270,13 @@ fun TranslatorApp(
|
||||
showLabels = showBottomNavLabels,
|
||||
onItemSelected = { screen ->
|
||||
val inSameSection = currentDestination?.hierarchy?.any { it.route == screen.route } == true
|
||||
val isMoreSection = screen in setOf(
|
||||
Screen.Translation,
|
||||
Screen.Vocabulary,
|
||||
Screen.Dictionary,
|
||||
Screen.Settings,
|
||||
Screen.Exercises
|
||||
)
|
||||
|
||||
// Always reset the selected section to its root and clear back stack between sections
|
||||
if (inSameSection) {
|
||||
@@ -274,6 +289,11 @@ fun TranslatorApp(
|
||||
launchSingleTop = true
|
||||
restoreState = false
|
||||
}
|
||||
} else if (isMoreSection) {
|
||||
navController.navigate(screen.route) {
|
||||
launchSingleTop = true
|
||||
restoreState = false
|
||||
}
|
||||
} else {
|
||||
// Switching sections: clear entire back stack to start to avoid back navigation results
|
||||
navController.navigate(screen.route) {
|
||||
@@ -287,14 +307,7 @@ fun TranslatorApp(
|
||||
}
|
||||
},
|
||||
onPlayClicked = {
|
||||
navController.navigate("start_exercise") {
|
||||
popUpTo(0) {
|
||||
inclusive = true
|
||||
saveState = false
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = false
|
||||
}
|
||||
navController.navigate("start_exercise")
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
@@ -130,10 +130,6 @@ fun AppNavHost(
|
||||
HomeScreen(navController = navController)
|
||||
}
|
||||
|
||||
composable(Screen.Stats.route) {
|
||||
StatsScreen(navController = navController)
|
||||
}
|
||||
|
||||
composable(Screen.Library.route) {
|
||||
LibraryScreen(navController = navController)
|
||||
}
|
||||
@@ -144,6 +140,7 @@ fun AppNavHost(
|
||||
|
||||
// Define all other navigation graphs at the same top level.
|
||||
homeGraph(navController)
|
||||
statsGraph(navController)
|
||||
translationGraph(navController)
|
||||
dictionaryGraph(navController)
|
||||
vocabularyGraph(navController)
|
||||
@@ -163,6 +160,108 @@ fun NavGraphBuilder.homeGraph(navController: NavHostController) {
|
||||
}
|
||||
}
|
||||
|
||||
fun NavGraphBuilder.statsGraph(
|
||||
navController: NavHostController,
|
||||
) {
|
||||
navigation(
|
||||
startDestination = "main_stats",
|
||||
route = Screen.Stats.route
|
||||
) {
|
||||
composable("main_stats") {
|
||||
StatsScreen(navController = navController)
|
||||
}
|
||||
composable("stats/vocabulary_sorting") {
|
||||
VocabularySortingScreen(
|
||||
navController = navController
|
||||
)
|
||||
}
|
||||
composable("stats/vocabulary_list/{showDueTodayOnly}/{categoryId}") { backStackEntry ->
|
||||
val showDueTodayOnly = backStackEntry.arguments?.getString("showDueTodayOnly")?.toBoolean() ?: false
|
||||
val categoryId = backStackEntry.arguments?.getString("categoryId")?.toIntOrNull()
|
||||
VocabularyListScreen(
|
||||
navController = navController,
|
||||
showDueTodayOnly = showDueTodayOnly,
|
||||
categoryId = categoryId,
|
||||
onNavigateToItem = { item ->
|
||||
navController.navigate("vocabulary_detail/${item.id}")
|
||||
},
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
enableNavigationButtons = true
|
||||
)
|
||||
}
|
||||
composable("stats/language_progress") {
|
||||
LanguageProgressScreen(
|
||||
navController = navController
|
||||
)
|
||||
}
|
||||
composable("stats/vocabulary_heatmap") {
|
||||
VocabularyHeatmapScreen(
|
||||
navController = navController,
|
||||
)
|
||||
}
|
||||
composable("stats/vocabulary_list/{showDueTodayOnly}/{stage}") { backStackEntry ->
|
||||
val showDueTodayOnly = backStackEntry.arguments?.getString("showDueTodayOnly")?.toBoolean() ?: false
|
||||
val stageString = backStackEntry.arguments?.getString("stage")
|
||||
val stage = stageString?.let {
|
||||
if (it.equals("null", ignoreCase = true)) null else VocabularyStage.valueOf(it)
|
||||
}
|
||||
|
||||
VocabularyListScreen(
|
||||
navController = navController,
|
||||
showDueTodayOnly = showDueTodayOnly,
|
||||
stage = stage,
|
||||
onNavigateToItem = { item ->
|
||||
navController.navigate("vocabulary_detail/${item.id}")
|
||||
},
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
categoryId = 0,
|
||||
enableNavigationButtons = true
|
||||
)
|
||||
}
|
||||
composable("stats/category_detail/{categoryId}") { backStackEntry ->
|
||||
val categoryId = backStackEntry.arguments?.getString("categoryId")?.toIntOrNull()
|
||||
|
||||
if (categoryId != null) {
|
||||
CategoryDetailScreen(
|
||||
categoryId = categoryId,
|
||||
onBackClick = { navController.popBackStack() },
|
||||
onNavigateToItem = { item ->
|
||||
navController.navigate("vocabulary_detail/${item.id}")
|
||||
},
|
||||
navController = navController
|
||||
)
|
||||
}
|
||||
}
|
||||
composable("stats/category_list_screen") {
|
||||
CategoryListScreen(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onCategoryClicked = { categoryId ->
|
||||
navController.navigate("stats/category_detail/$categoryId")
|
||||
}
|
||||
)
|
||||
}
|
||||
composable(
|
||||
route = "stats/vocabulary_sorting?mode={mode}",
|
||||
arguments = listOf(
|
||||
navArgument("mode") {
|
||||
type = NavType.StringType
|
||||
nullable = true
|
||||
}
|
||||
)
|
||||
) { backStackEntry ->
|
||||
VocabularySortingScreen(
|
||||
navController = navController,
|
||||
initialFilterMode = backStackEntry.arguments?.getString("mode")
|
||||
)
|
||||
}
|
||||
composable("stats/no_grammar_items") {
|
||||
NoGrammarItemsScreen(
|
||||
navController = navController
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(androidx.compose.animation.ExperimentalAnimationApi::class)
|
||||
fun NavGraphBuilder.translationGraph(navController: NavHostController) {
|
||||
navigation(
|
||||
|
||||
@@ -20,9 +20,13 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -37,6 +41,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
||||
import eu.gaudian.translator.R
|
||||
|
||||
/**
|
||||
* An interface that defines the required properties for any item
|
||||
@@ -60,14 +65,42 @@ fun <T : TabItem> AppTabLayout(
|
||||
tabs: List<T>,
|
||||
selectedTab: T,
|
||||
onTabSelected: (T) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
modifier: Modifier = Modifier,
|
||||
onNavigateBack: (() -> Unit)? = null
|
||||
) {
|
||||
val selectedIndex = tabs.indexOf(selectedTab)
|
||||
|
||||
BoxWithConstraints(
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp, horizontal = 8.dp)
|
||||
.padding(vertical = 8.dp, horizontal = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (onNavigateBack != null) {
|
||||
IconButton(
|
||||
onClick = onNavigateBack,
|
||||
modifier = Modifier
|
||||
.padding(end = 8.dp)
|
||||
.size(40.dp)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
||||
shape = CircleShape
|
||||
),
|
||||
colors = IconButtonDefaults.iconButtonColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
||||
contentColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = AppIcons.ArrowBack,
|
||||
contentDescription = stringResource(R.string.cd_navigate_back)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(56.dp)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
||||
@@ -139,6 +172,7 @@ fun <T : TabItem> AppTabLayout(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
|
||||
@@ -21,6 +21,7 @@ import eu.gaudian.translator.view.LocalConnectionConfigured
|
||||
import eu.gaudian.translator.view.NoConnectionScreen
|
||||
import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppTabLayout
|
||||
import eu.gaudian.translator.view.composable.Screen
|
||||
import eu.gaudian.translator.view.composable.TabItem
|
||||
import eu.gaudian.translator.view.settings.SettingsRoutes
|
||||
import eu.gaudian.translator.viewmodel.CorrectionViewModel
|
||||
@@ -63,7 +64,15 @@ fun MainDictionaryScreen(
|
||||
AppTabLayout(
|
||||
tabs = dictionaryTabs,
|
||||
selectedTab = selectedTab,
|
||||
onTabSelected = { selectedTab = it }
|
||||
onTabSelected = { selectedTab = it },
|
||||
onNavigateBack = {
|
||||
if (!navController.popBackStack()) {
|
||||
navController.navigate(Screen.Home.route) {
|
||||
launchSingleTop = true
|
||||
restoreState = false
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
when (selectedTab) {
|
||||
|
||||
@@ -38,6 +38,7 @@ import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppOutlinedCard
|
||||
import eu.gaudian.translator.view.composable.AppTabLayout
|
||||
import eu.gaudian.translator.view.composable.DialogButton
|
||||
import eu.gaudian.translator.view.composable.Screen
|
||||
import eu.gaudian.translator.view.composable.TabItem
|
||||
import eu.gaudian.translator.viewmodel.AiGenerationState
|
||||
import eu.gaudian.translator.viewmodel.ExerciseViewModel
|
||||
@@ -76,7 +77,15 @@ fun MainExerciseScreen(
|
||||
AppTabLayout(
|
||||
tabs = ExerciseTab.entries,
|
||||
selectedTab = selectedTab,
|
||||
onTabSelected = { selectedTab = it }
|
||||
onTabSelected = { selectedTab = it },
|
||||
onNavigateBack = {
|
||||
if (!navController.popBackStack()) {
|
||||
navController.navigate(Screen.Home.route) {
|
||||
launchSingleTop = true
|
||||
restoreState = false
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
|
||||
@@ -78,6 +78,7 @@ import eu.gaudian.translator.view.composable.AppTabLayout
|
||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||
import eu.gaudian.translator.view.composable.ClickableText
|
||||
import eu.gaudian.translator.view.composable.PrimaryButton
|
||||
import eu.gaudian.translator.view.composable.Screen
|
||||
import eu.gaudian.translator.view.composable.SecondaryButton
|
||||
import eu.gaudian.translator.view.composable.TabItem
|
||||
import eu.gaudian.translator.view.hints.HintDefinition
|
||||
@@ -133,7 +134,15 @@ fun ApiKeyScreen(navController: NavController) {
|
||||
AppTabLayout(
|
||||
tabs = apiTabs,
|
||||
selectedTab = selectedTab,
|
||||
onTabSelected = { selectedTab = it }
|
||||
onTabSelected = { selectedTab = it },
|
||||
onNavigateBack = {
|
||||
if (!navController.popBackStack()) {
|
||||
navController.navigate(Screen.Home.route) {
|
||||
launchSingleTop = true
|
||||
restoreState = false
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Tab Content
|
||||
|
||||
@@ -27,6 +27,7 @@ import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppOutlinedCard
|
||||
import eu.gaudian.translator.view.composable.AppScaffold
|
||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||
import eu.gaudian.translator.view.composable.Screen
|
||||
|
||||
private data class Setting(val titleRes: Int, val icon: ImageVector, val route: String)
|
||||
|
||||
@@ -84,7 +85,15 @@ fun MainSettingsScreen(
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
AppTopAppBar(
|
||||
title =stringResource(R.string.title_settings)
|
||||
title =stringResource(R.string.title_settings),
|
||||
onNavigateBack = {
|
||||
if (!navController.popBackStack()) {
|
||||
navController.navigate(Screen.Home.route) {
|
||||
launchSingleTop = true
|
||||
restoreState = false
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
|
||||
@@ -1,26 +1,692 @@
|
||||
@file:Suppress("HardCodedStringLiteral", "AssignedValueIsNeverRead", "UnusedMaterial3ScaffoldPaddingParameter")
|
||||
|
||||
package eu.gaudian.translator.view.stats
|
||||
|
||||
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.NavHostController
|
||||
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.Log
|
||||
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.ModernStartButtons
|
||||
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 StatsScreen(
|
||||
navController: NavHostController,
|
||||
onShowCustomExerciseDialog: () -> Unit = {},
|
||||
startDailyExercise: (Boolean) -> Unit = {},
|
||||
onNavigateToCategoryDetail: ((Int) -> Unit)? = null,
|
||||
onNavigateToCategoryList: (() -> Unit)? = null,
|
||||
onShowWordPairExerciseDialog: () -> Unit = {},
|
||||
onScroll: (Boolean) -> Unit = {},
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
val handleNavigateToCategoryDetail = onNavigateToCategoryDetail ?: { categoryId ->
|
||||
navController.navigate("stats/category_detail/$categoryId")
|
||||
}
|
||||
val handleNavigateToCategoryList = onNavigateToCategoryList ?: {
|
||||
navController.navigate("stats/category_list_screen")
|
||||
}
|
||||
|
||||
AppOutlinedCard(modifier = modifier) {
|
||||
// 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(),
|
||||
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 = handleNavigateToCategoryDetail,
|
||||
onNavigateToCategoryList = handleNavigateToCategoryList,
|
||||
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 = "Stats Screen",
|
||||
style = MaterialTheme.typography.headlineMedium
|
||||
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.StartButtons -> ModernStartButtons(
|
||||
onCustomClick = onShowCustomExerciseDialog,
|
||||
onDailyClick = { isSpelling ->
|
||||
if (isSpelling) {
|
||||
onShowWordPairExerciseDialog()
|
||||
} else {
|
||||
startDailyExercise(true)
|
||||
Log.d("DailyExercise", "Starting daily exercise")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
WidgetType.Status -> LazyStatusWidget(
|
||||
vocabularyViewModel = vocabularyViewModel,
|
||||
onNavigateToNew = { navController.navigate("stats/vocabulary_sorting?mode=NEW") },
|
||||
onNavigateToDuplicates = { navController.navigate("stats/vocabulary_sorting?mode=DUPLICATES") },
|
||||
onNavigateToFaulty = { navController.navigate("stats/vocabulary_sorting?mode=FAULTY") },
|
||||
onNavigateToNoGrammar = { navController.navigate("stats/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("stats/vocabulary_heatmap") }
|
||||
)
|
||||
|
||||
WidgetType.WeeklyActivityChart -> WeeklyActivityChartWidget(
|
||||
weeklyStats = progressViewModel.weeklyActivityStats.collectAsState().value
|
||||
)
|
||||
|
||||
WidgetType.AllVocabulary -> AllVocabularyWidget(
|
||||
vocabularyViewModel = vocabularyViewModel,
|
||||
onOpenAllVocabulary = { navController.navigate("stats/vocabulary_list/false/null") },
|
||||
onStageClicked = { stage -> navController.navigate("stats/vocabulary_list/false/$stage") }
|
||||
)
|
||||
|
||||
WidgetType.DueToday -> DueTodayWidget(
|
||||
vocabularyViewModel = vocabularyViewModel,
|
||||
onStageClicked = { stage -> navController.navigate("stats/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("stats/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 StatsScreenPreview() {
|
||||
val navController = rememberNavController()
|
||||
StatsScreen(
|
||||
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,9 +67,23 @@ fun ActionBar(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TopBarActions(languageViewModel: LanguageViewModel, onSettingsClick: () -> Unit, hintContent: (@Composable () -> Unit)? = null) {
|
||||
fun TopBarActions(
|
||||
languageViewModel: LanguageViewModel,
|
||||
onSettingsClick: () -> Unit,
|
||||
onNavigateBack: (() -> Unit)? = null,
|
||||
hintContent: (@Composable () -> Unit)? = null
|
||||
) {
|
||||
|
||||
ActionBar(modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp)) {
|
||||
if (onNavigateBack != null) {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(
|
||||
imageVector = AppIcons.ArrowBack,
|
||||
contentDescription = stringResource(R.string.cd_navigate_back)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (hintContent != null) {
|
||||
WithHint(hintContent = hintContent) {
|
||||
}
|
||||
|
||||
@@ -106,6 +106,14 @@ fun TranslationScreen(
|
||||
settingsViewModel = settingsViewModel,
|
||||
onHistoryClick = onHistoryClick,
|
||||
onSettingsClick = onSettingsClick,
|
||||
onNavigateBack = {
|
||||
if (!navController.popBackStack()) {
|
||||
navController.navigate(eu.gaudian.translator.view.composable.Screen.Home.route) {
|
||||
launchSingleTop = true
|
||||
restoreState = false
|
||||
}
|
||||
}
|
||||
},
|
||||
context = context
|
||||
)
|
||||
}
|
||||
@@ -119,6 +127,7 @@ private fun LoadedTranslationContent(
|
||||
settingsViewModel: SettingsViewModel,
|
||||
onHistoryClick: () -> Unit,
|
||||
onSettingsClick: () -> Unit,
|
||||
onNavigateBack: () -> Unit,
|
||||
context: Context
|
||||
) {
|
||||
val inputText by translationViewModel.inputText.collectAsState()
|
||||
@@ -167,6 +176,7 @@ private fun LoadedTranslationContent(
|
||||
TopBarActions(
|
||||
languageViewModel = languageViewModel,
|
||||
onSettingsClick = onSettingsClick,
|
||||
onNavigateBack = onNavigateBack,
|
||||
hintContent = { HintDefinition.TRANSLATION.Render() }
|
||||
)
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ import eu.gaudian.translator.view.composable.AppOutlinedCard
|
||||
import eu.gaudian.translator.view.composable.AppSlider
|
||||
import eu.gaudian.translator.view.composable.AppTabLayout
|
||||
import eu.gaudian.translator.view.composable.OptionItemSwitch
|
||||
import eu.gaudian.translator.view.composable.Screen
|
||||
import eu.gaudian.translator.view.composable.TabItem
|
||||
import eu.gaudian.translator.view.dialogs.StartExerciseDialog
|
||||
import eu.gaudian.translator.view.dialogs.VocabularyMenu
|
||||
@@ -329,6 +330,14 @@ fun MainVocabularyScreen(
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
},
|
||||
onNavigateBack = {
|
||||
if (!navController.popBackStack()) {
|
||||
navController.navigate(Screen.Home.route) {
|
||||
launchSingleTop = true
|
||||
restoreState = false
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
<string-array name="changelog_entries">
|
||||
<item>Version 0.3.0 \n• Enabled CSV Import for Vocabulary\n• Option to use a translation server for translations instead of AI models for some supported langugaes\n• UI bug fixes \n• Show word frequency \n• Performance optimizations \n• Improved translations (German and Portuguese)</item>
|
||||
<item>Version 0.4.0 \n• Added dictionary download (beta) \n• UI enhancements \n• Bugfixes \n• Re-designed vocabulary card with improved UI \n• More pre-configured providers \n• Improved performance</item>
|
||||
<item>Version 0.5.0 \n• Reworked UI with new focus on Flashcards and Exercises \n• </item>
|
||||
<item>Version 0.5.0 \n• Reworked UI with new focus on Flashcards and Exercises \n• Adding vocabulary is easier and more intuitive now </item>
|
||||
<item> </item>
|
||||
|
||||
</string-array>
|
||||
|
||||
Reference in New Issue
Block a user