Compare commits

...

4 Commits

12 changed files with 250 additions and 40 deletions

View File

@@ -253,19 +253,23 @@ fun TranslatorApp(
val currentDestination = navBackStackEntry?.destination
val selectedScreen = Screen.fromDestination(currentDestination)
@Suppress("HardCodedStringLiteral") val isBottomBarHidden = currentDestination?.hierarchy?.any { destination ->
@Suppress("HardCodedStringLiteral")
val currentRoute = currentDestination?.route
val isHiddenByHierarchy = currentDestination?.hierarchy?.any { destination ->
destination.route in setOf(
Screen.Translation.route,
Screen.Dictionary.route,
Screen.Exercises.route,
Screen.Settings.route
)
} == true || currentDestination?.route in setOf(
"start_exercise",
} == true
val isBottomBarHidden = isHiddenByHierarchy || currentRoute in setOf(
"new_word",
"new_word_review",
"vocabulary_detail/{itemId}"
)
"vocabulary_detail/{itemId}",
"daily_review"
) || currentRoute?.startsWith("start_exercise") == true
|| currentRoute?.startsWith("vocabulary_exercise") == true
val showBottomNavLabels by settingsViewModel.showBottomNavLabels.collectAsStateWithLifecycle(initialValue = false)
BottomNavigationBar(
@@ -440,4 +444,4 @@ private fun AppTheme(
}
}
}

View File

@@ -29,10 +29,10 @@ import eu.gaudian.translator.view.exercises.MainExerciseScreen
import eu.gaudian.translator.view.exercises.StartExerciseScreen
import eu.gaudian.translator.view.exercises.YouTubeBrowserScreen
import eu.gaudian.translator.view.exercises.YouTubeExerciseScreen
import eu.gaudian.translator.view.home.DailyReviewScreen
import eu.gaudian.translator.view.home.HomeScreen
import eu.gaudian.translator.view.library.LibraryScreen
import eu.gaudian.translator.view.settings.DictionaryOptionsScreen
import eu.gaudian.translator.view.settings.SettingsRoutes
import eu.gaudian.translator.view.settings.TranslationSettingsScreen
import eu.gaudian.translator.view.settings.settingsGraph
import eu.gaudian.translator.view.stats.StatsScreen
@@ -53,10 +53,12 @@ import eu.gaudian.translator.view.vocabulary.VocabularySortingScreen
private const val TRANSITION_DURATION = 300
object NavigationRoutes {
const val DAILY_REVIEW = "daily_review"
const val NEW_WORD = "new_word"
const val NEW_WORD_REVIEW = "new_word_review"
const val VOCABULARY_DETAIL = "vocabulary_detail"
const val START_EXERCISE = "start_exercise"
const val START_EXERCISE_DAILY = "start_exercise_daily"
const val CATEGORY_DETAIL = "category_detail"
const val CATEGORY_LIST = "category_list_screen"
const val STATS_VOCABULARY_HEATMAP = "stats/vocabulary_heatmap"
@@ -68,6 +70,7 @@ object NavigationRoutes {
const val STATS_VOCABULARY_LIST = "stats/vocabulary_list"
}
@OptIn(androidx.compose.animation.ExperimentalAnimationApi::class)
@Composable
fun AppNavHost(
navController: NavHostController,
@@ -81,15 +84,23 @@ fun AppNavHost(
Screen.Home.route,
Screen.Library.route,
Screen.Stats.route,
Screen.Translation.route,
Screen.Dictionary.route,
Screen.Exercises.route,
SettingsRoutes.LIST
)
// Helper to check if a route is a top-level tab
// Note: Routes can be "main_home", "main_library" etc. but mainTabRoutes contains parent routes
fun isTabTransition(initial: String?, target: String?): Boolean {
return mainTabRoutes.contains(initial) && mainTabRoutes.contains(target)
if (initial == null || target == null) return false
// Check if either the direct route OR a "main_*" variant is in mainTabRoutes
val initialIsTab = mainTabRoutes.contains(initial) ||
mainTabRoutes.any { route ->
initial == "main_${route}" || initial.startsWith("${route}_")
}
val targetIsTab = mainTabRoutes.contains(target) ||
mainTabRoutes.any { route ->
target == "main_${route}" || target.startsWith("${route}_")
}
return initialIsTab && targetIsTab
}
NavHost(
@@ -146,6 +157,10 @@ fun AppNavHost(
HomeScreen(navController = navController)
}
composable(NavigationRoutes.DAILY_REVIEW) {
DailyReviewScreen(navController = navController)
}
composable(NavigationRoutes.NEW_WORD) {
NewWordScreen(navController = navController)
}
@@ -168,7 +183,16 @@ fun AppNavHost(
val categoryId = categoryIdString?.toIntOrNull()
StartExerciseScreen(
navController = navController,
preselectedCategoryId = categoryId
preselectedCategoryId = categoryId,
dueTodayOnly = false
)
}
composable(NavigationRoutes.START_EXERCISE_DAILY) {
StartExerciseScreen(
navController = navController,
preselectedCategoryId = null,
dueTodayOnly = true
)
}

View File

@@ -342,10 +342,12 @@ fun DictionarySimpleTopBar(
languageName: String?,
onNavigateBack: () -> Unit
) {
AppTopAppBar(
title = "TODO",
onNavigateBack = onNavigateBack
)
word?.let {
AppTopAppBar(
title = it,
onNavigateBack = onNavigateBack
)
}
}
@Composable

View File

@@ -93,7 +93,7 @@ fun EtymologyResultScreen(
AppScaffold(
topBar = {
AppTopAppBar(
title = "TODO",
title = "Result",
onNavigateBack = { navController.popBackStack() },
actions = {
etymologyData?.let { data ->

View File

@@ -77,6 +77,7 @@ import kotlinx.coroutines.launch
fun StartExerciseScreen(
navController: NavHostController,
preselectedCategoryId: Int? = null,
dueTodayOnly: Boolean = false,
modifier: Modifier = Modifier
) {
val activity = androidx.compose.ui.platform.LocalContext.current.findActivity()
@@ -84,6 +85,15 @@ fun StartExerciseScreen(
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
val exerciseViewModel: VocabularyExerciseViewModel = hiltViewModel(viewModelStoreOwner = activity)
// Initialize exercise config with dueTodayOnly if specified
androidx.compose.runtime.LaunchedEffect(dueTodayOnly) {
if (dueTodayOnly) {
exerciseViewModel.updatePendingExerciseConfig(
exerciseViewModel.pendingExerciseConfig.value.copy(dueTodayOnly = true)
)
}
}
val exerciseConfig by exerciseViewModel.pendingExerciseConfig.collectAsState()
val allCategories by categoryViewModel.categories.collectAsState(initial = emptyList())
var selectedLanguagePairs by remember { mutableStateOf<List<Pair<Language, Language>>>(emptyList()) }

View File

@@ -0,0 +1,168 @@
package eu.gaudian.translator.view.home
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
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.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController
import eu.gaudian.translator.R
import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.NavigationRoutes
import eu.gaudian.translator.view.composable.AppButton
import eu.gaudian.translator.view.composable.AppScaffold
import eu.gaudian.translator.view.composable.AppTopAppBar
import eu.gaudian.translator.view.library.VocabularyCard
import eu.gaudian.translator.viewmodel.LanguageViewModel
import eu.gaudian.translator.viewmodel.VocabularyViewModel
@Composable
fun DailyReviewScreen(
navController: NavHostController,
modifier: Modifier = Modifier
) {
val context = LocalContext.current
val activity = context.findActivity()
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
val dueTodayItems by vocabularyViewModel.dueTodayItems.collectAsStateWithLifecycle(initialValue = emptyList())
val allLanguages by languageViewModel.allLanguages.collectAsStateWithLifecycle(initialValue = emptyList())
val listState = rememberLazyListState()
AppScaffold(
topBar = {
AppTopAppBar(
title = stringResource(R.string.label_daily_review),
onNavigateBack = { navController.popBackStack() }
)
},
modifier = modifier.fillMaxSize()
) { paddingValues ->
if (dueTodayItems.isEmpty()) {
Box(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
modifier = Modifier.size(200.dp),
painter = painterResource(id = R.drawable.ic_nothing_found),
contentDescription = null
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(R.string.no_items_due_for_review),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center
)
}
}
} else {
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(
state = listState,
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(16.dp),
contentPadding = PaddingValues(16.dp)
) {
items(
items = dueTodayItems,
key = { it.id }
) { item ->
VocabularyCard(
item = item,
allLanguages = allLanguages,
isSelected = false,
onItemClick = {
vocabularyViewModel.setNavigationContext(dueTodayItems, item.id)
navController.navigate("${NavigationRoutes.VOCABULARY_DETAIL}/${item.id}")
},
onItemLongClick = { },
onDeleteClick = { }
)
}
// Add spacing at the bottom for the button
item {
Spacer(modifier = Modifier.height(80.dp))
}
}
// Start Exercise Button (fixed at bottom)
Box(
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.padding(paddingValues)
.padding(24.dp)
) {
AppButton(
onClick = {
navController.navigate(NavigationRoutes.START_EXERCISE_DAILY)
},
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
enabled = dueTodayItems.isNotEmpty(),
shape = RoundedCornerShape(28.dp)
) {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(R.string.label_start_exercise_2d, dueTodayItems.size),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.width(8.dp))
Icon(
imageVector = Icons.Default.PlayArrow,
contentDescription = stringResource(R.string.cd_play),
modifier = Modifier.size(20.dp)
)
}
}
}
}
}
}
}

View File

@@ -62,6 +62,7 @@ fun HomeScreen(
val streak by viewModel.streak.collectAsState()
val dailyGoal by viewModel.dailyGoal.collectAsState()
val todayCompletedCount by viewModel.todayCompletedCount.collectAsState()
val dueTodayCount by viewModel.dueTodayCount.collectAsState()
// Calculate daily goal progress
val progress = if (dailyGoal > 0) {
@@ -95,13 +96,12 @@ fun HomeScreen(
)
}
item {
//TODO replace with actual implementation
@Suppress("HardCodedStringLiteral")
ActionCard(
title = "Daily Review",
subtitle = "42 words need attention",
title = stringResource(R.string.label_daily_review),
subtitle = stringResource(R.string.desc_daily_review_due, dueTodayCount),
icon = Icons.Default.Psychology,
contentColor = MaterialTheme.colorScheme.onPrimary
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
onClick = { navController.navigate(NavigationRoutes.DAILY_REVIEW) }
)
}
item {
@@ -146,6 +146,9 @@ fun TopProfileSection(navController: NavHostController, context: Context) {
)
}
Spacer(modifier = Modifier.width(8.dp))
IconButton(
onClick = { navController.navigate(Screen.Settings.route) },
modifier = Modifier
@@ -158,6 +161,7 @@ fun TopProfileSection(navController: NavHostController, context: Context) {
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Spacer(modifier = Modifier.width(8.dp))
}
}

View File

@@ -3,7 +3,6 @@
package eu.gaudian.translator.view.vocabulary
import android.annotation.SuppressLint
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -14,9 +13,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
@@ -50,6 +46,7 @@ import eu.gaudian.translator.model.TagCategory
import eu.gaudian.translator.model.VocabularyFilter
import eu.gaudian.translator.model.VocabularyItem
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.AppScaffold
import eu.gaudian.translator.view.composable.AppTopAppBar
@@ -58,6 +55,7 @@ import eu.gaudian.translator.view.dialogs.DeleteCategoryDialog
import eu.gaudian.translator.view.dialogs.DeleteItemsDialog
import eu.gaudian.translator.view.dialogs.EditCategoryDialog
import eu.gaudian.translator.view.vocabulary.widgets.CategoryProgressCircle
import eu.gaudian.translator.view.vocabulary.widgets.ChartLegend
import eu.gaudian.translator.viewmodel.CategoryProgress
import eu.gaudian.translator.viewmodel.CategoryViewModel
import eu.gaudian.translator.viewmodel.ExportImportViewModel
@@ -313,16 +311,10 @@ fun CategoryHeaderCard(
onDeleteClick: () -> Unit,
modifier: Modifier = Modifier
) {
Card(
AppCard(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 16.dp),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.2f)
),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.4f)),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(
modifier = Modifier
@@ -343,6 +335,8 @@ fun CategoryHeaderCard(
// Progress Circle - smaller size
if (categoryProgress != null) {
CategoryProgressCircle(
totalItems = categoryProgress.totalItems,
itemsCompleted = categoryProgress.itemsCompleted,
@@ -350,7 +344,9 @@ fun CategoryHeaderCard(
newItems = categoryProgress.newItems,
circleSize = 100.dp,
)
Spacer(modifier = Modifier.height(20.dp))
Spacer(modifier = Modifier.height(4.dp))
ChartLegend()
Spacer(modifier = Modifier.height(16.dp))
}
// Action Buttons

View File

@@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -85,7 +84,6 @@ fun VocabularyCardHost(
AppScaffold(
topBar = {
AppTopAppBar(
modifier = Modifier.height(56.dp),
title = stringResource(R.string.item_details),
onNavigateBack = { navController.popBackStack() },
actions = {

View File

@@ -290,7 +290,7 @@ fun CategoryProgressCircle(
}
}
@Composable
private fun ChartLegend() {
fun ChartLegend() {
Row(
modifier = Modifier
.fillMaxWidth()

View File

@@ -79,7 +79,7 @@ fun WeeklyActivityChartWidget(
.fillMaxWidth()
.padding(8.dp)
) {
ChartLegend()
WeeklyChartLegend()
Spacer(modifier = Modifier.height(16.dp))
Row(
@@ -164,7 +164,7 @@ private fun RowScope.Bar(value: Int, maxValue: Int, color: Color) {
}
@Composable
private fun ChartLegend() {
private fun WeeklyChartLegend() {
Row(
modifier = Modifier
.fillMaxWidth()

View File

@@ -450,6 +450,7 @@
<string name="no_models_configured">No Models Configured</string>
<string name="no_models_found">No models found</string>
<string name="no_new_vocabulary_to_sort">No New Vocabulary to Sort</string>
<string name="no_items_due_for_review">No items due for review today. Great job!</string>
<string name="no_vocabulary_items_found_perhaps_try_changing_the_filters">No vocabulary items found. Perhaps try changing the filters?</string>
<string name="not_available">Not available</string>
@@ -1088,6 +1089,9 @@
<string name="label_2d_days">%1$d Days</string>
<string name="label_current_streak">Current Streak</string>
<string name="label_daily_goal">Daily Goal</string>
<string name="label_daily_review">Daily Review</string>
<string name="desc_daily_review_due">%1$d words need attention</string>
<string name="text_daily_review_placeholder">Daily review screen - implementation pending</string>
<string name="text_desc_no_activity_data_available">No activity data available</string>
<string name="label_see_history">See History</string>
<string name="label_weekly_progress">Weekly Progress</string>