Compare commits

...

11 Commits

54 changed files with 4714 additions and 620 deletions

View File

@@ -32,17 +32,6 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".CorrectActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
</application> </application>

View File

@@ -1,45 +0,0 @@
@file:Suppress("HardCodedStringLiteral")
package eu.gaudian.translator
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.Text
import androidx.compose.ui.res.stringResource
import eu.gaudian.translator.utils.Log
class CorrectActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val intent = intent
val action = intent.action
val type = intent.type
if (Intent.ACTION_SEND == action && type == "text/plain") {
val sharedText = intent.getStringExtra(Intent.EXTRA_TEXT)
if (sharedText != null) {
Log.d("EditActivity", "Received text: $sharedText")
setContent {
Text(stringResource(R.string.editing_text, sharedText))
}
} else {
Log.e("EditActivity", getString(R.string.no_text_received))
setContent {
Text(stringResource(R.string.error_no_text_to_edit))
}
}
} else {
Log.d("EditActivity", "Not launched with ACTION_SEND")
setContent {
Text(stringResource(R.string.not_launched_with_text_to_edit))
}
}
}
}

View File

@@ -253,7 +253,15 @@ fun TranslatorApp(
val currentDestination = navBackStackEntry?.destination val currentDestination = navBackStackEntry?.destination
val selectedScreen = Screen.fromDestination(currentDestination) 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) val showBottomNavLabels by settingsViewModel.showBottomNavLabels.collectAsStateWithLifecycle(initialValue = false)
BottomNavigationBar( BottomNavigationBar(
@@ -262,6 +270,13 @@ fun TranslatorApp(
showLabels = showBottomNavLabels, showLabels = showBottomNavLabels,
onItemSelected = { screen -> onItemSelected = { screen ->
val inSameSection = currentDestination?.hierarchy?.any { it.route == screen.route } == true 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 // Always reset the selected section to its root and clear back stack between sections
if (inSameSection) { if (inSameSection) {
@@ -274,6 +289,11 @@ fun TranslatorApp(
launchSingleTop = true launchSingleTop = true
restoreState = false restoreState = false
} }
} else if (isMoreSection) {
navController.navigate(screen.route) {
launchSingleTop = true
restoreState = false
}
} else { } else {
// Switching sections: clear entire back stack to start to avoid back navigation results // Switching sections: clear entire back stack to start to avoid back navigation results
navController.navigate(screen.route) { navController.navigate(screen.route) {
@@ -285,6 +305,9 @@ fun TranslatorApp(
restoreState = false restoreState = false
} }
} }
},
onPlayClicked = {
navController.navigate("start_exercise")
} }
) )
}, },

View File

@@ -26,12 +26,16 @@ 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.ExerciseSessionScreen
import eu.gaudian.translator.view.exercises.MainExerciseScreen 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.YouTubeBrowserScreen
import eu.gaudian.translator.view.exercises.YouTubeExerciseScreen import eu.gaudian.translator.view.exercises.YouTubeExerciseScreen
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.DictionaryOptionsScreen
import eu.gaudian.translator.view.settings.SettingsRoutes import eu.gaudian.translator.view.settings.SettingsRoutes
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.translation.TranslationScreen import eu.gaudian.translator.view.translation.TranslationScreen
import eu.gaudian.translator.view.vocabulary.CategoryDetailScreen import eu.gaudian.translator.view.vocabulary.CategoryDetailScreen
import eu.gaudian.translator.view.vocabulary.CategoryListScreen import eu.gaudian.translator.view.vocabulary.CategoryListScreen
@@ -57,11 +61,13 @@ fun AppNavHost(
// 1. Define your Main Tab "Leaf" Routes (the actual start destinations of your graphs) // 1. Define your Main Tab "Leaf" Routes (the actual start destinations of your graphs)
val mainTabRoutes = setOf( val mainTabRoutes = setOf(
Screen.Home.route, // "home" or "main_translation" depending on which one you actually navigate to Screen.Home.route,
"main_translation", Screen.Library.route,
"main_dictionary", Screen.Stats.route,
"main_vocabulary", Screen.Translation.route,
"main_exercise", Screen.Vocabulary.route,
Screen.Dictionary.route,
Screen.Exercises.route,
SettingsRoutes.LIST SettingsRoutes.LIST
) )
@@ -121,10 +127,20 @@ fun AppNavHost(
} }
) { ) {
composable(Screen.Home.route) { composable(Screen.Home.route) {
TranslationScreen(navController = navController) HomeScreen(navController = navController)
}
composable(Screen.Library.route) {
LibraryScreen(navController = navController)
}
composable("start_exercise") {
StartExerciseScreen(navController = navController)
} }
// Define all other navigation graphs at the same top level. // Define all other navigation graphs at the same top level.
homeGraph(navController)
statsGraph(navController)
translationGraph(navController) translationGraph(navController)
dictionaryGraph(navController) dictionaryGraph(navController)
vocabularyGraph(navController) vocabularyGraph(navController)
@@ -132,11 +148,125 @@ fun AppNavHost(
settingsGraph(navController) settingsGraph(navController)
} }
} }
fun NavGraphBuilder.homeGraph(navController: NavHostController) {
navigation(
startDestination = "main_home",
route = Screen.Home.route
) {
composable("main_home") {
HomeScreen(navController = navController)
}
}
}
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) @OptIn(androidx.compose.animation.ExperimentalAnimationApi::class)
fun NavGraphBuilder.translationGraph(navController: NavHostController) { fun NavGraphBuilder.translationGraph(navController: NavHostController) {
navigation( navigation(
startDestination = "main_translation", startDestination = "main_translation",
route = Screen.Home.route route = Screen.Translation.route
) { ) {
composable("main_translation") { composable("main_translation") {
TranslationScreen(navController = navController) TranslationScreen(navController = navController)

View File

@@ -10,7 +10,7 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.Icons.Default import androidx.compose.material.icons.Icons.Default
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBackIos
import androidx.compose.material.icons.automirrored.filled.ArrowForward import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.automirrored.filled.DriveFileMove import androidx.compose.material.icons.automirrored.filled.DriveFileMove
import androidx.compose.material.icons.automirrored.filled.ExitToApp import androidx.compose.material.icons.automirrored.filled.ExitToApp
@@ -135,7 +135,7 @@ object AppIcons {
val AI = Default.AutoAwesome val AI = Default.AutoAwesome
val Appearance = Icons.Filled.ColorLens val Appearance = Icons.Filled.ColorLens
val ApiKey = Default.Key val ApiKey = Default.Key
val ArrowBack = Icons.AutoMirrored.Filled.ArrowBack val ArrowBack = Icons.AutoMirrored.Filled.ArrowBackIos
val ArrowCircleUp = Icons.Filled.ArrowCircleUp val ArrowCircleUp = Icons.Filled.ArrowCircleUp
val ArrowDropDown = Icons.Filled.KeyboardArrowDown val ArrowDropDown = Icons.Filled.KeyboardArrowDown
val ArrowDropUp = Icons.Filled.KeyboardArrowUp val ArrowDropUp = Icons.Filled.KeyboardArrowUp

View File

@@ -20,9 +20,13 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable 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.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.gaudian.translator.ui.theme.ThemePreviews import eu.gaudian.translator.ui.theme.ThemePreviews
import eu.gaudian.translator.R
/** /**
* An interface that defines the required properties for any item * An interface that defines the required properties for any item
@@ -60,21 +65,49 @@ fun <T : TabItem> AppTabLayout(
tabs: List<T>, tabs: List<T>,
selectedTab: T, selectedTab: T,
onTabSelected: (T) -> Unit, onTabSelected: (T) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier,
onNavigateBack: (() -> Unit)? = null
) { ) {
val selectedIndex = tabs.indexOf(selectedTab) val selectedIndex = tabs.indexOf(selectedTab)
BoxWithConstraints( Row(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 8.dp, horizontal = 8.dp) .padding(vertical = 8.dp, horizontal = 8.dp),
.height(56.dp) verticalAlignment = Alignment.CenterVertically
.background(
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
shape = ComponentDefaults.CardShape
)
) { ) {
val tabWidth = maxWidth / tabs.size 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),
shape = ComponentDefaults.CardShape
)
) {
val tabWidth = maxWidth / tabs.size
val indicatorOffset by animateDpAsState( val indicatorOffset by animateDpAsState(
targetValue = tabWidth * selectedIndex, targetValue = tabWidth * selectedIndex,
@@ -82,58 +115,59 @@ fun <T : TabItem> AppTabLayout(
label = "IndicatorOffset" label = "IndicatorOffset"
) )
Box( Box(
modifier = Modifier modifier = Modifier
.offset(x = indicatorOffset) .offset(x = indicatorOffset)
.width(tabWidth) .width(tabWidth)
.fillMaxHeight() .fillMaxHeight()
.padding(4.dp) .padding(4.dp)
.background( .background(
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
shape = RoundedCornerShape(12.dp) shape = RoundedCornerShape(12.dp)
) )
) )
Row(modifier = Modifier.fillMaxWidth()) { Row(modifier = Modifier.fillMaxWidth()) {
tabs.forEach { tab -> tabs.forEach { tab ->
val isSelected = tab == selectedTab val isSelected = tab == selectedTab
val contentColor by animateColorAsState( val contentColor by animateColorAsState(
targetValue = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant, targetValue = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
animationSpec = spring(stiffness = Spring.StiffnessLow) animationSpec = spring(stiffness = Spring.StiffnessLow)
) )
Box( Box(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.fillMaxHeight() .fillMaxHeight()
.clickable( .clickable(
onClick = { onTabSelected(tab) }, onClick = { onTabSelected(tab) },
indication = null, indication = null,
interactionSource = remember { MutableInteractionSource() } interactionSource = remember { MutableInteractionSource() }
), ),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) { ) {
val context = LocalContext.current
val resolvedTitle = run { Row(
val resId = context.resources.getIdentifier(tab.title, "string", context.packageName) verticalAlignment = Alignment.CenterVertically,
if (resId != 0) stringResource(resId) else tab.title horizontalArrangement = Arrangement.Center
) {
val context = LocalContext.current
val resolvedTitle = run {
val resId = context.resources.getIdentifier(tab.title, "string", context.packageName)
if (resId != 0) stringResource(resId) else tab.title
}
Icon(
modifier = Modifier.padding(4.dp),
imageVector = tab.icon,
contentDescription = resolvedTitle,
tint = contentColor
)
Spacer(modifier = Modifier.width(2.dp))
Text(
text = resolvedTitle,
color = contentColor,
)
} }
Icon(
modifier = Modifier.padding(4.dp),
imageVector = tab.icon,
contentDescription = resolvedTitle,
tint = contentColor
)
Spacer(modifier = Modifier.width(2.dp))
Text(
text = resolvedTitle,
color = contentColor,
)
} }
} }
} }

View File

@@ -1,20 +1,23 @@
@file:Suppress("HardCodedStringLiteral")
package eu.gaudian.translator.view.composable package eu.gaudian.translator.view.composable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
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.shape.CircleShape
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarColors import androidx.compose.material3.TopAppBarColors
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
@@ -25,8 +28,10 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment 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.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp 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
@@ -36,7 +41,8 @@ import eu.gaudian.translator.view.hints.LocalShowHints
@Composable @Composable
fun AppTopAppBar( fun AppTopAppBar(
title: @Composable () -> Unit, title: String,
additionalContent: @Composable () -> Unit = {},
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onNavigateBack: (() -> Unit)? = null, onNavigateBack: (() -> Unit)? = null,
navigationIcon: @Composable (() -> Unit)? = null, navigationIcon: @Composable (() -> Unit)? = null,
@@ -47,59 +53,62 @@ fun AppTopAppBar(
val sheetState = rememberModalBottomSheetState() val sheetState = rememberModalBottomSheetState()
var showBottomSheet by remember { mutableStateOf(false) } var showBottomSheet by remember { mutableStateOf(false) }
TopAppBar( // Changed to CenterAlignedTopAppBar to perfectly match the design requirements
CenterAlignedTopAppBar(
modifier = modifier.height(56.dp), modifier = modifier.height(56.dp),
windowInsets = WindowInsets(0.dp), windowInsets = WindowInsets(0.dp),
colors = colors, colors = colors,
title = { title = {
Box( val showHints = LocalShowHints.current
modifier = Modifier.fillMaxHeight(), if (showHints && hintContent != null) {
contentAlignment = Alignment.Center // Simplified row: keeps the title and hint icon neatly centered together
) { Row(
val showHints = LocalShowHints.current verticalAlignment = Alignment.CenterVertically,
if (showHints && hintContent != null) { horizontalArrangement = Arrangement.Center
Row( ) {
modifier = Modifier.fillMaxWidth(), Text(
verticalAlignment = Alignment.CenterVertically text = title,
) { style = MaterialTheme.typography.titleLarge,
Box(modifier = Modifier.weight(1f)) { fontWeight = FontWeight.Bold,
title() modifier = Modifier.weight(1f),
} textAlign = androidx.compose.ui.text.style.TextAlign.Center
Box { )
IconButton(onClick = { showBottomSheet = true }) { IconButton(onClick = { showBottomSheet = true }) {
Icon( Icon(
imageVector = AppIcons.Help, imageVector = AppIcons.Help,
contentDescription = stringResource(R.string.show_hint), contentDescription = stringResource(R.string.show_hint),
tint = MaterialTheme.colorScheme.secondary tint = MaterialTheme.colorScheme.secondary
) )
}
}
} }
} else {
title()
} }
} else {
Text(
text = title,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
textAlign = androidx.compose.ui.text.style.TextAlign.Center
)
} }
}, },
navigationIcon = { navigationIcon = {
if (onNavigateBack != null) { if (onNavigateBack != null) {
Box( IconButton(
modifier = Modifier.fillMaxHeight(), onClick = onNavigateBack,
contentAlignment = Alignment.Center modifier = Modifier.padding(start = 8.dp),
// This tells the button to paint its own circular background natively
colors = IconButtonDefaults.iconButtonColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
contentColor = MaterialTheme.colorScheme.primary
)
) { ) {
IconButton(onClick = onNavigateBack) { Icon(
Icon( imageVector = AppIcons.ArrowBack,
AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_navigate_back)
contentDescription = stringResource(R.string.cd_navigate_back), // Notice we removed the 'tint' here, as contentColor handles it perfectly now!
tint = LocalContentColor.current )
)
}
} }
} else if (navigationIcon != null) { } else if (navigationIcon != null) {
Box(modifier = Modifier.fillMaxHeight(), contentAlignment = Alignment.Center) { navigationIcon()
navigationIcon()
}
} else {
// No navigation icon
} }
}, },
actions = actions actions = actions
@@ -119,17 +128,9 @@ fun AppTopAppBar(
} }
} }
/** /**
* A composable that acts as a TopAppBar, containing a back navigation icon * A composable that acts as a TopAppBar, containing a back navigation icon
* and an [AppTabLayout]. * and an [AppTabLayout].
*
* @param T The type of the tab item, must implement [TabItem].
* @param tabs The list of tab items to display.
* @param selectedTab The currently selected tab item.
* @param onTabSelected Callback function when a tab is selected.
* @param onNavigateBack Callback function when the back arrow is clicked.
* @param modifier The modifier to be applied to the layout.
*/ */
@Composable @Composable
fun <T : TabItem> TabbedTopAppBar( fun <T : TabItem> TabbedTopAppBar(
@@ -139,7 +140,6 @@ fun <T : TabItem> TabbedTopAppBar(
onNavigateBack: () -> Unit, onNavigateBack: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
// Use a Surface to provide background color and context for the app bar
Surface( Surface(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.surface color = MaterialTheme.colorScheme.surface
@@ -148,20 +148,21 @@ fun <T : TabItem> TabbedTopAppBar(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
// Back navigation icon, similar to its usage in AppTopAppBar // Updated back icon here as well to keep your entire app consistent!
IconButton( IconButton(
onClick = onNavigateBack, onClick = onNavigateBack,
modifier = Modifier.padding(horizontal = 4.dp) modifier = Modifier
.padding(start = 8.dp, end = 4.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
) { ) {
Icon( Icon(
imageVector = AppIcons.ArrowBack, imageVector = AppIcons.ArrowBack,
contentDescription = stringResource(R.string.cd_navigate_back), contentDescription = stringResource(R.string.cd_navigate_back),
tint = MaterialTheme.colorScheme.onSurface tint = MaterialTheme.colorScheme.primary
) )
} }
// The AppTabLayout, taking up the remaining space.
// Its appearance matches the provided image.
AppTabLayout( AppTabLayout(
tabs = tabs, tabs = tabs,
selectedTab = selectedTab, selectedTab = selectedTab,
@@ -172,11 +173,12 @@ fun <T : TabItem> TabbedTopAppBar(
} }
} }
// ... [Previews remain exactly the same below]
@Suppress("HardCodedStringLiteral") @Suppress("HardCodedStringLiteral")
@ThemePreviews @ThemePreviews
@Composable @Composable
fun TabbedTopAppBarPreview() { fun TabbedTopAppBarPreview() {
// Sample data for preview, similar to ModernTabLayoutPreview
data class SampleTabItem(override val title: String, override val icon: ImageVector) : TabItem data class SampleTabItem(override val title: String, override val icon: ImageVector) : TabItem
val tabs = listOf( val tabs = listOf(
@@ -202,7 +204,7 @@ fun TabbedTopAppBarPreview() {
@Composable @Composable
fun AppTopAppBarPreview() { fun AppTopAppBarPreview() {
AppTopAppBar( AppTopAppBar(
title = { Text("Preview Title") } title = "Previwe Title"
) )
} }
@@ -210,7 +212,7 @@ fun AppTopAppBarPreview() {
@Composable @Composable
fun AppTopAppBarWithNavigationIconPreview() { fun AppTopAppBarWithNavigationIconPreview() {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.title_title_preview_title)) }, title = "Preview Title",
onNavigateBack = {} onNavigateBack = {}
) )
} }
@@ -219,13 +221,13 @@ fun AppTopAppBarWithNavigationIconPreview() {
@Composable @Composable
fun AppTopAppBarWithActionsPreview() { fun AppTopAppBarWithActionsPreview() {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.title_title_preview_title)) }, title = "Preview Title",
actions = { actions = {
IconButton(onClick = {}) { IconButton(onClick = {}) {
Icon(AppIcons.Settings, contentDescription = stringResource(R.string.title_settings)) Icon(AppIcons.Settings, contentDescription = stringResource(R.string.title_settings))
} }
IconButton(onClick = {}) { IconButton(onClick = {}) {
AppIcons.ArrowBack Icon(AppIcons.ArrowBack, contentDescription = null)
} }
} }
) )

View File

@@ -11,23 +11,44 @@ import androidx.compose.animation.core.spring
import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
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.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationBarItemDefaults import androidx.compose.material3.NavigationBarItemDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
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.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment 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.scale import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
@@ -41,6 +62,7 @@ import androidx.navigation.NavDestination.Companion.hierarchy
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.view.LocalShowExperimentalFeatures import eu.gaudian.translator.view.LocalShowExperimentalFeatures
import kotlinx.coroutines.launch
sealed class Screen( sealed class Screen(
val route: String, val route: String,
@@ -48,34 +70,44 @@ sealed class Screen(
val selectedIcon: ImageVector, val selectedIcon: ImageVector,
val unselectedIcon: ImageVector val unselectedIcon: ImageVector
) { ) {
object Home : Screen("home", R.string.label_translation, AppIcons.TranslateFilled, AppIcons.TranslateOutlined) object Home : Screen("home", R.string.label_home, AppIcons.Home, AppIcons.Home)
object Stats : Screen("stats", R.string.label_stats, AppIcons.Statistics, AppIcons.Statistics)
object Translation : Screen("translation", R.string.label_translation, AppIcons.TranslateFilled, AppIcons.TranslateOutlined)
object Library : Screen("library", R.string.label_library, AppIcons.VocabularyFilled, AppIcons.VocabularyOutlined)
object Vocabulary : Screen("vocabulary", R.string.label_legacy_vocabulary, AppIcons.VocabularyFilled, AppIcons.VocabularyOutlined)
object Settings : Screen("settings", R.string.title_settings, AppIcons.SettingsFilled, AppIcons.SettingsOutlined)
object More : Screen("more", R.string.label_more, AppIcons.MoreVert, AppIcons.MoreVert)
object Dictionary : Screen("dictionary", R.string.label_dictionary, AppIcons.DictionaryFilled, AppIcons.DictionaryOutlined) object Dictionary : Screen("dictionary", R.string.label_dictionary, AppIcons.DictionaryFilled, AppIcons.DictionaryOutlined)
object Exercises : Screen("exercises", R.string.label_exercises, AppIcons.DictionaryFilled, AppIcons.DictionaryOutlined) object Exercises : Screen("exercises", R.string.label_exercises, AppIcons.DictionaryFilled, AppIcons.DictionaryOutlined)
object Vocabulary : Screen("vocabulary", R.string.label_vocabulary, AppIcons.VocabularyFilled, AppIcons.VocabularyOutlined)
object Settings : Screen("settings", R.string.title_settings, AppIcons.SettingsFilled, AppIcons.SettingsOutlined)
companion object { companion object {
fun getAllScreens(showExperimental: Boolean = false): List<Screen> { fun getAllScreens(showExperimental: Boolean = false): List<Screen> {
val screens = mutableListOf(Home, Dictionary, Vocabulary, Settings) return listOf(Home, Library, Stats)
}
fun getMoreMenuItems(showExperimental: Boolean = false): List<Screen> {
val items = mutableListOf<Screen>()
items.add(Translation)
items.add(Vocabulary) // Legacy vocabulary moved to More
items.add(Dictionary)
items.add(Settings)
if (showExperimental) { if (showExperimental) {
screens.add(2, Exercises) items.add(Exercises)
} }
return screens return items
} }
@Composable @Composable
fun fromDestination(destination: NavDestination?): Screen { fun fromDestination(destination: NavDestination?): Screen {
val showExperimental = LocalShowExperimentalFeatures.current val showExperimental = LocalShowExperimentalFeatures.current
return getAllScreens(showExperimental).find { screen -> val allScreens = getAllScreens(showExperimental) + getMoreMenuItems(showExperimental) + More
return allScreens.find { screen ->
destination?.hierarchy?.any { it.route == screen.route } == true destination?.hierarchy?.any { it.route == screen.route } == true
} ?: Home } ?: Home
} }
} }
} }
/**
* A modernized Material 3 bottom navigation bar with spring animations and haptic feedback.
*/
@SuppressLint("UnusedBoxWithConstraintsScope") @SuppressLint("UnusedBoxWithConstraintsScope")
@Composable @Composable
fun BottomNavigationBar( fun BottomNavigationBar(
@@ -84,87 +116,253 @@ fun BottomNavigationBar(
showLabels: Boolean, showLabels: Boolean,
onItemSelected: (Screen) -> Unit, onItemSelected: (Screen) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onPlayClicked: () -> Unit = {}
) { ) {
val showExperimental = LocalShowExperimentalFeatures.current val showExperimental = LocalShowExperimentalFeatures.current
val screens = remember(showExperimental) { Screen.getAllScreens(showExperimental) } val screens = remember(showExperimental) { Screen.getAllScreens(showExperimental) }
val moreScreen = remember { Screen.More }
val haptic = LocalHapticFeedback.current val haptic = LocalHapticFeedback.current
var showMoreMenu by remember { mutableStateOf(false) }
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val scope = rememberCoroutineScope()
// Configuration for the play button
val playButtonSize = 56.dp
val glowPadding = 32.dp // Total extra space for the glow (16dp on each side)
// This dictates how far up the button shifts.
// Setting it to around half the button size centers it on the top border.
val upwardOffset = 28.dp
AnimatedVisibility( AnimatedVisibility(
visible = isVisible, visible = isVisible,
enter = slideInVertically( enter = slideInVertically(
animationSpec = androidx.compose.animation.core.tween(durationMillis = 220), animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
initialOffsetY = { it } initialOffsetY = { it }
), ),
exit = slideOutVertically( exit = slideOutVertically(
animationSpec = androidx.compose.animation.core.tween(durationMillis = 220), animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
targetOffsetY = { it } targetOffsetY = { it }
) )
) { ) {
val baseHeight = if (showLabels) 80.dp else 56.dp val baseHeight = if (showLabels) 80.dp else 56.dp
val density = LocalDensity.current val density = LocalDensity.current
val navBarDp = with(density) { WindowInsets.navigationBars.getBottom(this).toDp() } val navBarDp = with(density) { WindowInsets.navigationBars.getBottom(this).toDp() }
val height = baseHeight + navBarDp val height = baseHeight + navBarDp
NavigationBar( // Outer Box height is purely determined by the NavigationBar now
modifier = modifier.height(height), Box(
containerColor = MaterialTheme.colorScheme.surface, // Cleaner look than surfaceVariant modifier = modifier.fillMaxWidth(),
tonalElevation = 8.dp, // Slight elevation for depth contentAlignment = Alignment.TopCenter
) { ) {
screens.forEach { screen ->
val isSelected = screen == selectedItem
val title = stringResource(id = screen.title)
// 1. Spring Animation for the Icon Scale // The actual Navigation Bar
val scale by animateFloatAsState( NavigationBar(
targetValue = if (isSelected) 1.2f else 1.0f, // Subtle pop effect modifier = Modifier.height(height),
animationSpec = spring( containerColor = MaterialTheme.colorScheme.surface,
dampingRatio = Spring.DampingRatioMediumBouncy, tonalElevation = 8.dp,
stiffness = Spring.StiffnessLow ) {
), // Create a list of 5 items (2 left, 1 empty spacer, 2 right)
label = "iconScale" val allNavItems = buildList {
addAll(screens.take(2))
add(null) // Empty spacer for Play Button gap
if (screens.size > 2) {
addAll(screens.drop(2))
}
add(moreScreen)
}
allNavItems.forEach { screen ->
if (screen == null) {
// Dummy item to create the gap
NavigationBarItem(
selected = false,
onClick = {},
enabled = false, // Disables ripples and clicks
icon = { Spacer(modifier = Modifier.size(24.dp)) },
label = if (showLabels) { { Spacer(modifier = Modifier.size(10.dp)) } } else null,
colors = NavigationBarItemDefaults.colors(
disabledIconColor = Color.Transparent,
disabledTextColor = Color.Transparent
)
)
} else {
// Regular or More items
val isSelected = if (screen == Screen.More) {
selectedItem is Screen.More || Screen.getMoreMenuItems(showExperimental).contains(selectedItem)
} else {
screen == selectedItem
}
val title = stringResource(id = screen.title)
val scale by animateFloatAsState(
targetValue = if (isSelected) 1.2f else 1.0f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
),
label = "iconScale"
)
NavigationBarItem(
selected = isSelected,
onClick = {
if (!isSelected) {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
if (screen == Screen.More) showMoreMenu = true else onItemSelected(screen)
}
},
label = if (showLabels) {
{
Text(
text = title,
maxLines = 1,
fontSize = 10.sp,
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
color = if(isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
)
}
} else null,
icon = {
Crossfade(targetState = isSelected, label = "iconFade") { selected ->
Icon(
imageVector = if (selected) screen.selectedIcon else screen.unselectedIcon,
contentDescription = title,
modifier = Modifier.scale(scale)
)
}
},
colors = NavigationBarItemDefaults.colors(
indicatorColor = MaterialTheme.colorScheme.primaryContainer,
selectedIconColor = MaterialTheme.colorScheme.onPrimaryContainer,
selectedTextColor = MaterialTheme.colorScheme.primary,
unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant
)
)
}
}
}
// The Glowing Play Button
Box(
modifier = Modifier
// This negative offset pulls the button UP out of the bounding box
// without increasing the layout height of the parent Box.
.offset(y = -upwardOffset)
.size(playButtonSize + glowPadding),
contentAlignment = Alignment.Center
) {
// Background radial glow
Box(
modifier = Modifier
.matchParentSize()
.background(
brush = Brush.radialGradient(
colors = listOf(
Color(0xFF3B82F6).copy(alpha = 0.5f),
Color.Transparent
)
),
shape = CircleShape
)
) )
NavigationBarItem( // Actual clickable button
selected = isSelected, Box(
onClick = { modifier = Modifier
if (!isSelected) { .size(playButtonSize)
haptic.performHapticFeedback(HapticFeedbackType.LongPress) // 2. Tactile feedback .clip(CircleShape)
onItemSelected(screen) .background(Color(0xFF3B82F6))
} .clickable {
}, haptic.performHapticFeedback(HapticFeedbackType.LongPress)
label = if (showLabels) { onPlayClicked()
{ },
Text( contentAlignment = Alignment.Center
text = title, ) {
maxLines = 1, Icon(
fontSize = 10.sp, imageVector = Icons.Filled.PlayArrow,
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal, contentDescription = "Play",
color = if(isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant tint = Color.White,
) modifier = Modifier.size(32.dp)
}
} else null,
icon = {
// 3. Crossfade between Outlined and Filled icons
Crossfade(targetState = isSelected, label = "iconFade") { selected ->
Icon(
imageVector = if (selected) screen.selectedIcon else screen.unselectedIcon,
contentDescription = title,
modifier = Modifier.scale(scale) // Apply the spring scale
)
}
},
colors = NavigationBarItemDefaults.colors(
indicatorColor = MaterialTheme.colorScheme.primaryContainer,
selectedIconColor = MaterialTheme.colorScheme.onPrimaryContainer,
selectedTextColor = MaterialTheme.colorScheme.primary,
unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant
) )
) }
} }
} }
} }
// Modal Bottom Sheet for More menu (Remains exactly the same)
if (showMoreMenu) {
ModalBottomSheet(
onDismissRequest = { showMoreMenu = false },
sheetState = sheetState
) {
MoreBottomSheetContent(
showExperimental = showExperimental,
onItemSelected = { screen ->
scope.launch {
sheetState.hide()
showMoreMenu = false
onItemSelected(screen)
}
}
)
}
}
}
@Composable
private fun MoreBottomSheetContent(
showExperimental: Boolean,
onItemSelected: (Screen) -> Unit
) {
val moreItems = remember(showExperimental) { Screen.getMoreMenuItems(showExperimental) }
Column(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 32.dp)
) {
Text(
text = stringResource(R.string.label_more),
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
)
HorizontalDivider()
moreItems.forEach { screen ->
MoreMenuItem(
screen = screen,
onClick = { onItemSelected(screen) }
)
}
}
}
@Composable
private fun MoreMenuItem(
screen: Screen,
onClick: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(horizontal = 24.dp, vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = screen.selectedIcon,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.width(16.dp))
Text(
text = stringResource(screen.title),
style = MaterialTheme.typography.bodyLarge
)
}
} }
@ThemePreviews @ThemePreviews

View File

@@ -65,7 +65,7 @@ fun VocabularyReviewScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.found_items)) }, title = stringResource(R.string.found_items),
hintContent = HintDefinition.REVIEW.hint() hintContent = HintDefinition.REVIEW.hint()
) )
}, },

View File

@@ -10,8 +10,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -28,7 +26,6 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavController import androidx.navigation.NavController
@@ -346,28 +343,8 @@ fun DictionarySimpleTopBar(
onNavigateBack: () -> Unit onNavigateBack: () -> Unit
) { ) {
AppTopAppBar( AppTopAppBar(
title = { title = "TODO",
Column { onNavigateBack = onNavigateBack
Text(
text = word ?: stringResource(R.string.text_loading_3d),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
languageName?.let {
Text(
text = it,
style = MaterialTheme.typography.bodyMedium,
fontStyle = FontStyle.Italic
)
}
}
},
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
},
actions = {}
) )
} }

View File

@@ -30,7 +30,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
@@ -94,27 +93,8 @@ fun EtymologyResultScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { title = "TODO",
Column { onNavigateBack = { navController.popBackStack() },
Text(
text = word,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
language?.name?.let {
Text(
text = it,
style = MaterialTheme.typography.bodyMedium,
fontStyle = androidx.compose.ui.text.font.FontStyle.Italic
)
}
}
},
navigationIcon = {
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
},
actions = { actions = {
etymologyData?.let { data -> etymologyData?.let { data ->
if (isTtsAvailable) { if (isTtsAvailable) {

View File

@@ -21,6 +21,7 @@ import eu.gaudian.translator.view.LocalConnectionConfigured
import eu.gaudian.translator.view.NoConnectionScreen import eu.gaudian.translator.view.NoConnectionScreen
import eu.gaudian.translator.view.composable.AppIcons import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppTabLayout 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.composable.TabItem
import eu.gaudian.translator.view.settings.SettingsRoutes import eu.gaudian.translator.view.settings.SettingsRoutes
import eu.gaudian.translator.viewmodel.CorrectionViewModel import eu.gaudian.translator.viewmodel.CorrectionViewModel
@@ -63,7 +64,15 @@ fun MainDictionaryScreen(
AppTabLayout( AppTabLayout(
tabs = dictionaryTabs, tabs = dictionaryTabs,
selectedTab = selectedTab, selectedTab = selectedTab,
onTabSelected = { selectedTab = it } onTabSelected = { selectedTab = it },
onNavigateBack = {
if (!navController.popBackStack()) {
navController.navigate(Screen.Home.route) {
launchSingleTop = true
restoreState = false
}
}
}
) )
when (selectedTab) { when (selectedTab) {

View File

@@ -24,7 +24,7 @@ fun ExerciseVocabularyScreen(
) { ) {
Scaffold( Scaffold(
topBar = { topBar = {
AppTopAppBar(title = { Text(stringResource(R.string.text_new_vocabulary_for_this_exercise)) }) AppTopAppBar(title =stringResource(R.string.text_new_vocabulary_for_this_exercise))
}, },
bottomBar = { bottomBar = {
Surface(shadowElevation = 8.dp) { Surface(shadowElevation = 8.dp) {

View File

@@ -38,6 +38,7 @@ 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.composable.AppTabLayout import eu.gaudian.translator.view.composable.AppTabLayout
import eu.gaudian.translator.view.composable.DialogButton 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.view.composable.TabItem
import eu.gaudian.translator.viewmodel.AiGenerationState import eu.gaudian.translator.viewmodel.AiGenerationState
import eu.gaudian.translator.viewmodel.ExerciseViewModel import eu.gaudian.translator.viewmodel.ExerciseViewModel
@@ -76,7 +77,15 @@ fun MainExerciseScreen(
AppTabLayout( AppTabLayout(
tabs = ExerciseTab.entries, tabs = ExerciseTab.entries,
selectedTab = selectedTab, 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)) { Box(modifier = Modifier.weight(1f)) {

View File

@@ -0,0 +1,432 @@
package eu.gaudian.translator.view.exercises
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
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.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBackIosNew
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.Hearing
import androidx.compose.material.icons.filled.List
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.outlined.Circle
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
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.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavHostController
@Composable
fun StartExerciseScreen(
navController: NavHostController,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.TopCenter
) {
Column(
modifier = Modifier
.widthIn(max = 700.dp) // Keeps it from over-stretching on tablets
.fillMaxSize()
) {
TopBarSection(onBackClick = { navController.popBackStack() })
LazyColumn(
modifier = Modifier
.weight(1f)
.padding(horizontal = 24.dp),
verticalArrangement = Arrangement.spacedBy(32.dp)
) {
item { Spacer(modifier = Modifier.height(8.dp)) }
item { LanguagePairSection() }
item { CategoriesSection() }
item { DifficultySection() }
item { NumberOfCardsSection() }
item { QuestionTypesSection() }
item { Spacer(modifier = Modifier.height(24.dp)) }
}
BottomButtonSection()
}
}
}
@Composable
fun TopBarSection(onBackClick: () -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(
onClick = onBackClick,
modifier = Modifier
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
) {
Icon(
imageVector = Icons.Default.ArrowBackIosNew,
contentDescription = "Back",
modifier = Modifier.size(18.dp),
tint = MaterialTheme.colorScheme.primary
)
}
Text(
text = "Start Exercise",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
modifier = Modifier.weight(1f),
textAlign = androidx.compose.ui.text.style.TextAlign.Center
)
// Spacer to balance the back button for centering
Spacer(modifier = Modifier.size(48.dp))
}
}
@Composable
fun SectionHeader(title: String, actionText: String? = null, onActionClick: () -> Unit = {}) {
Row(
modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = title.uppercase(),
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold,
letterSpacing = 1.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
if (actionText != null) {
Text(
text = actionText,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold,
modifier = Modifier.clickable { onActionClick() }
)
}
}
}
@Composable
fun LanguagePairSection() {
var selectedPair by remember { mutableStateOf(0) }
Column {
SectionHeader(title = "Language Pair", actionText = "Change")
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
LanguageChip(
text = "EN → ES",
isSelected = selectedPair == 0,
modifier = Modifier.weight(1f),
onClick = { selectedPair = 0 }
)
LanguageChip(
text = "EN → FR",
isSelected = selectedPair == 1,
modifier = Modifier.weight(1f),
onClick = { selectedPair = 1 }
)
}
}
}
@Composable
fun LanguageChip(text: String, isSelected: Boolean, modifier: Modifier = Modifier, onClick: () -> Unit) {
Surface(
modifier = modifier.height(56.dp),
shape = RoundedCornerShape(16.dp),
color = if (isSelected) MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) else MaterialTheme.colorScheme.surfaceVariant,
border = if (isSelected) BorderStroke(2.dp, MaterialTheme.colorScheme.primary) else null,
onClick = onClick
) {
Row(
modifier = Modifier.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
// Dummy overlapping flags
Box(modifier = Modifier.width(32.dp)) {
Box(modifier = Modifier.size(20.dp).clip(CircleShape).background(Color.Red).align(Alignment.CenterStart))
Box(modifier = Modifier.size(20.dp).clip(CircleShape).background(Color.Blue).align(Alignment.CenterEnd))
}
Spacer(modifier = Modifier.width(8.dp))
Text(text = text, fontWeight = FontWeight.Bold, style = MaterialTheme.typography.bodyMedium)
}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun CategoriesSection() {
val categories = listOf("Travel", "Business", "Food", "Technology", "Slang", "Academic", "Relationships")
var selectedCategories by remember { mutableStateOf(setOf("Travel", "Food")) }
Column {
SectionHeader(title = "Categories")
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth()
) {
categories.forEach { category ->
val isSelected = selectedCategories.contains(category)
Surface(
shape = RoundedCornerShape(20.dp),
color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant,
onClick = {
selectedCategories = if (isSelected) selectedCategories - category else selectedCategories + category
}
) {
Text(
text = category,
color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
style = MaterialTheme.typography.bodyMedium,
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal
)
}
}
}
}
}
@Composable
fun DifficultySection() {
val difficulties = listOf("Easy", "Medium", "Hard")
var selectedDifficulty by remember { mutableStateOf("Medium") }
Column {
SectionHeader(title = "Difficulty Level")
Surface(
shape = RoundedCornerShape(50),
color = MaterialTheme.colorScheme.surfaceVariant,
modifier = Modifier.fillMaxWidth().height(56.dp)
) {
Row(
modifier = Modifier.fillMaxSize().padding(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
difficulties.forEach { level ->
val isSelected = selectedDifficulty == level
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.clip(RoundedCornerShape(50))
.background(if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent)
.clickable { selectedDifficulty = level },
contentAlignment = Alignment.Center
) {
Text(
text = level,
color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}
}
}
@Composable
fun NumberOfCardsSection() {
var sliderPosition by remember { mutableFloatStateOf(25f) }
Column {
Row(
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "NUMBER OF CARDS",
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold,
letterSpacing = 1.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Surface(
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.primary,
) {
Text(
text = sliderPosition.toInt().toString(),
color = MaterialTheme.colorScheme.onPrimary,
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
)
}
}
Slider(
value = sliderPosition,
onValueChange = { sliderPosition = it },
valueRange = 5f..50f,
steps = 45
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text("5 CARDS", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
Text("50 CARDS", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
}
@Composable
fun QuestionTypesSection() {
var selectedTypes by remember { mutableStateOf(setOf("Multiple Choice", "Spelling")) }
Column {
SectionHeader(title = "Question Types")
QuestionTypeCard(
title = "Multiple Choice",
subtitle = "Choose the correct meaning",
icon = Icons.Default.List,
isSelected = selectedTypes.contains("Multiple Choice"),
onClick = {
selectedTypes = if (selectedTypes.contains("Multiple Choice")) selectedTypes - "Multiple Choice" else selectedTypes + "Multiple Choice"
}
)
Spacer(modifier = Modifier.height(12.dp))
QuestionTypeCard(
title = "Spelling",
subtitle = "Type the translated word",
icon = Icons.Default.Edit,
isSelected = selectedTypes.contains("Spelling"),
onClick = {
selectedTypes = if (selectedTypes.contains("Spelling")) selectedTypes - "Spelling" else selectedTypes + "Spelling"
}
)
Spacer(modifier = Modifier.height(12.dp))
QuestionTypeCard(
title = "Listening",
subtitle = "Recognize spoken words",
icon = Icons.Default.Hearing,
isSelected = selectedTypes.contains("Listening"),
onClick = {
selectedTypes = if (selectedTypes.contains("Listening")) selectedTypes - "Listening" else selectedTypes + "Listening"
}
)
}
}
@Composable
fun QuestionTypeCard(title: String, subtitle: String, icon: ImageVector, isSelected: Boolean, onClick: () -> Unit) {
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
color = if (isSelected) MaterialTheme.colorScheme.primary.copy(alpha = 0.05f) else MaterialTheme.colorScheme.surfaceVariant,
border = if (isSelected) BorderStroke(1.dp, MaterialTheme.colorScheme.primary.copy(alpha = 0.5f)) else null,
onClick = onClick
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surface),
contentAlignment = Alignment.Center
) {
Icon(imageVector = icon, contentDescription = null, tint = MaterialTheme.colorScheme.primary)
}
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
Text(text = title, fontWeight = FontWeight.Bold, style = MaterialTheme.typography.bodyLarge)
Text(text = subtitle, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
Icon(
imageVector = if (isSelected) Icons.Default.CheckCircle else Icons.Outlined.Circle,
contentDescription = null,
tint = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
@Composable
fun BottomButtonSection() {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp)
) {
Button(
onClick = { /* TODO: Start Session */ },
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(28.dp),
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary)
) {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Start Session",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.width(8.dp))
Icon(
imageVector = Icons.Default.PlayArrow,
contentDescription = "Play",
modifier = Modifier.size(20.dp)
)
}
}
}
}

View File

@@ -16,8 +16,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
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
@@ -26,12 +24,10 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import eu.gaudian.translator.R
import eu.gaudian.translator.utils.findActivity import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.AppIcons import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppScaffold import eu.gaudian.translator.view.composable.AppScaffold
@@ -61,12 +57,8 @@ fun YouTubeBrowserScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text("YouTube") }, title = "YouTube" ,
navigationIcon = { onNavigateBack = { navController.popBackStack() }
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, stringResource(R.string.cd_back))
}
}
) )
} }
) { padding -> ) { padding ->

View File

@@ -183,14 +183,8 @@ fun YouTubeExerciseScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(title, maxLines = 1) }, title = title,
navigationIcon = { onNavigateBack = { navController.popBackStack() },
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(
R.string.cd_back
))
}
},
actions = { actions = {
IconButton( IconButton(
onClick = { onFinishVideo() }, onClick = { onFinishVideo() },

View File

@@ -5,15 +5,9 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavController import androidx.navigation.NavController
import eu.gaudian.translator.R
import eu.gaudian.translator.view.composable.AppIcons
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
@@ -30,12 +24,8 @@ fun HintScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(title) }, title = title,
navigationIcon = { onNavigateBack = { navController.popBackStack() }
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
}
) )
} }
) { paddingValues -> ) { paddingValues ->

View File

@@ -77,7 +77,7 @@ fun HintsOverviewScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.hint_title_hints_overview), style = MaterialTheme.typography.titleLarge) } title = stringResource(R.string.hint_title_hints_overview)
) )
} }
) { paddingValues -> ) { paddingValues ->

View File

@@ -0,0 +1,360 @@
package eu.gaudian.translator.view.home
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
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.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AddCircle
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material.icons.filled.LocalFireDepartment
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Psychology
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.TrendingUp
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
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.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import eu.gaudian.translator.view.composable.Screen
@Composable
fun HomeScreen(
navController: NavHostController,
modifier: Modifier = Modifier
) {
// A Box with TopCenter alignment keeps the UI centered on wide screens (tablets/foldables)
Box(
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.TopCenter
) {
LazyColumn(
modifier = Modifier
.widthIn(max = 700.dp) // Prevents extreme stretching on tablets
.fillMaxSize()
.padding(horizontal = 20.dp, vertical = 24.dp),
verticalArrangement = Arrangement.spacedBy(20.dp)
) {
item { TopProfileSection(navController = navController) }
item { StreakAndGoalSection() }
item {
ActionCard(
title = "Daily Review",
subtitle = "42 words need attention",
icon = Icons.Default.Psychology,
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary
)
}
item {
ActionCard(
title = "New Words",
subtitle = "Expand your vocabulary",
icon = Icons.Default.AddCircle,
containerColor = MaterialTheme.colorScheme.surfaceVariant,
contentColor = MaterialTheme.colorScheme.onSurfaceVariant
)
}
item { WeeklyProgressSection() }
item { BottomStatsSection() }
// Bottom padding for edge-to-edge screens
item { Spacer(modifier = Modifier.height(24.dp)) }
}
}
}
@Composable
fun TopProfileSection(navController: NavHostController) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
// Dummy Avatar
Box(
modifier = Modifier
.size(56.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surfaceVariant),
contentAlignment = Alignment.Center
) {
Icon(Icons.Default.Person, contentDescription = "Profile", tint = MaterialTheme.colorScheme.onSurfaceVariant)
}
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = "Welcome back,",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
)
Text(
text = "Alex Rivera 👋",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
}
IconButton(
onClick = { navController.navigate(Screen.Settings.route) },
modifier = Modifier
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surfaceVariant)
) {
Icon(
imageVector = Icons.Default.Settings,
contentDescription = "Settings",
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
@Composable
fun StreakAndGoalSection() {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
// Streak Card
StatCard(
modifier = Modifier.weight(1f),
icon = Icons.Default.LocalFireDepartment,
title = "7 Days",
subtitle = "CURRENT STREAK"
)
// Goal Card
GoalCard(
modifier = Modifier.weight(1f),
progress = 0.7f,
title = "14 / 20",
subtitle = "DAILY GOAL"
)
}
}
@Composable
fun StatCard(
modifier: Modifier = Modifier,
icon: ImageVector,
title: String,
subtitle: String
) {
Card(
modifier = modifier,
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
) {
Column(
modifier = Modifier.padding(20.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(32.dp)
)
Spacer(modifier = Modifier.height(12.dp))
Text(text = title, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(4.dp))
Text(text = subtitle, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f))
}
}
}
@Composable
fun GoalCard(
modifier: Modifier = Modifier,
progress: Float,
title: String,
subtitle: String
) {
Card(
modifier = modifier,
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
) {
Column(
modifier = Modifier.padding(20.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Box(contentAlignment = Alignment.Center) {
CircularProgressIndicator(
progress = { progress },
modifier = Modifier.size(48.dp),
strokeWidth = 4.dp,
color = MaterialTheme.colorScheme.primary,
trackColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
)
Text(text = "${(progress * 100).toInt()}%", style = MaterialTheme.typography.labelSmall, fontWeight = FontWeight.Bold)
}
Spacer(modifier = Modifier.height(12.dp))
Text(text = title, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(4.dp))
Text(text = subtitle, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f))
}
}
}
@Composable
fun ActionCard(
title: String,
subtitle: String,
icon: ImageVector,
containerColor: Color,
contentColor: Color
) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = containerColor, contentColor = contentColor)
) {
Row(
modifier = Modifier.padding(20.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(40.dp)
)
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
Text(text = title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
Text(text = subtitle, style = MaterialTheme.typography.bodyMedium, color = contentColor.copy(alpha = 0.8f))
}
Icon(
imageVector = Icons.Default.ChevronRight,
contentDescription = "Go",
modifier = Modifier.size(24.dp)
)
}
}
}
@Composable
fun WeeklyProgressSection() {
Column {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(text = "Weekly Progress", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
TextButton(onClick = { /* TODO */ }) {
Text("See History")
}
}
Spacer(modifier = Modifier.height(8.dp))
Card(
modifier = Modifier
.fillMaxWidth()
.height(200.dp), // Fixed height for dummy chart area
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Bottom
) {
// Dummy Chart Graph Space
Spacer(modifier = Modifier.weight(1f))
// Days row
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
listOf("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun").forEach { day ->
Text(
text = day,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
)
}
}
}
}
}
}
@Composable
fun BottomStatsSection() {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
// Total Words
Card(
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
) {
Column(modifier = Modifier.padding(20.dp)) {
Text(text = "TOTAL WORDS", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f))
Spacer(modifier = Modifier.height(8.dp))
Text(text = "1,284", style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(8.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Default.TrendingUp, contentDescription = null, tint = Color.Green, modifier = Modifier.size(16.dp))
Spacer(modifier = Modifier.width(4.dp))
Text(text = "+12 today", style = MaterialTheme.typography.labelSmall, color = Color.Green)
}
}
}
// Accuracy
Card(
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
) {
Column(modifier = Modifier.padding(20.dp)) {
Text(text = "ACCURACY", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f))
Spacer(modifier = Modifier.height(8.dp))
Text(text = "92%", style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(8.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Default.CheckCircle, contentDescription = null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(16.dp))
Spacer(modifier = Modifier.width(4.dp))
Text(text = "Master level", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.primary)
}
}
}
}
}

View File

@@ -0,0 +1,730 @@
@file:Suppress("HardCodedStringLiteral")
package eu.gaudian.translator.view.library
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
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.fillMaxHeight
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.LazyListState
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.AddCircleOutline
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.LocalMall
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Tune
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
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.drawBehind
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.Stroke
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.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import eu.gaudian.translator.R
import eu.gaudian.translator.model.Language
import eu.gaudian.translator.model.VocabularyCategory
import eu.gaudian.translator.model.VocabularyItem
import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.insertBreakOpportunities
/**
* Top bar for the library screen with title and add button
*/
@Composable
fun LibraryTopBar(
onAddClick: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Library",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
IconButton(
onClick = onAddClick,
modifier = Modifier
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surfaceVariant)
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "Add",
tint = MaterialTheme.colorScheme.primary
)
}
}
}
/**
* Top bar shown when items are selected for batch operations
*/
@Composable
fun SelectionTopBar(
selectionCount: Int,
onCloseClick: () -> Unit,
onSelectAllClick: () -> Unit,
onDeleteClick: () -> Unit,
onMoveToCategoryClick: () -> Unit,
onMoveToStageClick: () -> Unit,
isRemoveEnabled: Boolean,
onRemoveFromCategoryClick: () -> Unit,
modifier: Modifier = Modifier
) {
var showOverflowMenu by remember { mutableStateOf(false) }
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(verticalAlignment = Alignment.CenterVertically) {
IconButton(onClick = onCloseClick) {
Icon(imageVector = AppIcons.Close, contentDescription = stringResource(R.string.label_close_selection_mode))
}
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(R.string.d_selected, selectionCount),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
}
Row {
IconButton(onClick = onSelectAllClick) {
Icon(
imageVector = AppIcons.SelectAll,
contentDescription = stringResource(R.string.select_all)
)
}
IconButton(onClick = onDeleteClick) {
Icon(AppIcons.Delete, contentDescription = stringResource(R.string.label_delete))
}
Box {
IconButton(onClick = { showOverflowMenu = true }) {
Icon(imageVector = AppIcons.More, contentDescription = stringResource(R.string.more_actions))
}
DropdownMenu(
expanded = showOverflowMenu,
onDismissRequest = { showOverflowMenu = false }
) {
DropdownMenuItem(
text = { Text(stringResource(R.string.move_to_category)) },
onClick = {
onMoveToCategoryClick()
showOverflowMenu = false
},
leadingIcon = { Icon(AppIcons.Category, contentDescription = null) }
)
if (isRemoveEnabled) {
DropdownMenuItem(
text = { Text(stringResource(R.string.remove_from_category)) },
onClick = {
onRemoveFromCategoryClick()
showOverflowMenu = false
},
leadingIcon = { Icon(AppIcons.Remove, contentDescription = null) }
)
}
DropdownMenuItem(
text = { Text(stringResource(R.string.move_to_stage)) },
onClick = {
onMoveToStageClick()
showOverflowMenu = false
},
leadingIcon = { Icon(AppIcons.Stages, contentDescription = null) }
)
}
}
}
}
}
/**
* Search bar with filter button
*/
@Composable
fun SearchBar(
searchQuery: String,
onQueryChanged: (String) -> Unit,
onFilterClick: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier
.fillMaxWidth()
.height(56.dp)
.clip(RoundedCornerShape(16.dp))
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
.padding(start = 16.dp, end = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Search,
contentDescription = "Search",
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.width(12.dp))
androidx.compose.foundation.text.BasicTextField(
value = searchQuery,
onValueChange = onQueryChanged,
modifier = Modifier.weight(1f),
textStyle = MaterialTheme.typography.bodyLarge.copy(
color = MaterialTheme.colorScheme.onSurface
),
singleLine = true,
cursorBrush = androidx.compose.ui.graphics.SolidColor(MaterialTheme.colorScheme.primary),
decorationBox = { innerTextField ->
Box(contentAlignment = Alignment.CenterStart) {
if (searchQuery.isEmpty()) {
Text(
text = "Search cards or topics...",
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
style = MaterialTheme.typography.bodyLarge
)
}
innerTextField()
}
}
)
IconButton(onClick = onFilterClick) {
Icon(
imageVector = Icons.Default.Tune,
contentDescription = "Filter options",
tint = MaterialTheme.colorScheme.primary
)
}
}
}
/**
* Segmented control for switching between All Cards and Categories view
*/
@Composable
fun SegmentedControl(
isCategoriesView: Boolean,
onTabSelected: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier
.fillMaxWidth()
.height(48.dp)
.clip(RoundedCornerShape(12.dp))
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
.padding(4.dp)
) {
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.clip(RoundedCornerShape(8.dp))
.background(if (!isCategoriesView) MaterialTheme.colorScheme.primary else Color.Transparent)
.clickable { onTabSelected(false) },
contentAlignment = Alignment.Center
) {
Text(
text = "All Cards",
color = if (!isCategoriesView) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.labelLarge
)
}
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.clip(RoundedCornerShape(8.dp))
.background(if (isCategoriesView) MaterialTheme.colorScheme.primary else Color.Transparent)
.clickable { onTabSelected(true) },
contentAlignment = Alignment.Center
) {
Text(
text = "Categories",
color = if (isCategoriesView) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.labelLarge
)
}
}
}
/**
* List view of all vocabulary cards
*/
@Composable
fun AllCardsView(
vocabularyItems: List<VocabularyItem>,
allLanguages: List<Language>,
selection: Set<Long>,
onItemClick: (VocabularyItem) -> Unit,
onItemLongClick: (VocabularyItem) -> Unit,
onDeleteClick: (VocabularyItem) -> Unit,
listState: LazyListState,
modifier: Modifier = Modifier
) {
if (vocabularyItems.isEmpty()) {
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Image(
modifier = Modifier.size(200.dp),
painter = painterResource(id = R.drawable.ic_nothing_found),
contentDescription = stringResource(id = R.string.no_vocabulary_items_found_perhaps_try_changing_the_filters)
)
Spacer(modifier = Modifier.size(16.dp))
Text(
text = stringResource(R.string.no_vocabulary_items_found_perhaps_try_changing_the_filters),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center
)
}
} else {
LazyColumn(
state = listState,
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier.fillMaxSize(),
contentPadding = PaddingValues(bottom = 100.dp)
) {
items(
items = vocabularyItems,
key = { it.id }
) { item ->
val isSelected = selection.contains(item.id.toLong())
VocabularyCard(
item = item,
allLanguages = allLanguages,
isSelected = isSelected,
onItemClick = { onItemClick(item) },
onItemLongClick = { onItemLongClick(item) },
onDeleteClick = { onDeleteClick(item) }
)
}
}
}
}
/**
* Individual vocabulary card component
*/
@Composable
fun VocabularyCard(
item: VocabularyItem,
allLanguages: List<Language>,
isSelected: Boolean,
onItemClick: () -> Unit,
onItemLongClick: () -> Unit,
onDeleteClick: () -> Unit,
modifier: Modifier = Modifier
) {
val languageMap = remember(allLanguages) { allLanguages.associateBy { it.nameResId } }
val langFirst = item.languageFirstId?.let { languageMap[it]?.name } ?: ""
val langSecond = item.languageSecondId?.let { languageMap[it]?.name } ?: ""
Card(
modifier = modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.combinedClickable(
onClick = onItemClick,
onLongClick = onItemLongClick
),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = if (isSelected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant
),
border = if (isSelected) BorderStroke(2.dp, MaterialTheme.colorScheme.primary) else null
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
// Top row: First word + Language Pill
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = insertBreakOpportunities(item.wordFirst),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.width(8.dp))
Surface(
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f),
shape = RoundedCornerShape(4.dp)
) {
Text(
text = langFirst,
style = MaterialTheme.typography.labelSmall,
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
Spacer(modifier = Modifier.height(4.dp))
// Bottom row: Second word + Language Pill
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = insertBreakOpportunities(item.wordSecond),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.width(8.dp))
Surface(
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.15f),
shape = RoundedCornerShape(8.dp)
) {
Text(
text = langSecond,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp)
)
}
}
}
if (isSelected) {
Icon(
imageVector = Icons.Default.CheckCircle,
contentDescription = "Selected",
tint = MaterialTheme.colorScheme.primary
)
} else {
IconButton(onClick = { /* Options menu could go here */ }) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = "Options",
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}
/**
* Grid view of categories
*/
@Composable
fun CategoriesView(
categories: List<VocabularyCategory>,
onCategoryClick: (VocabularyCategory) -> Unit,
onExploreMoreClick: () -> Unit,
modifier: Modifier = Modifier
) {
LazyVerticalGrid(
columns = GridCells.Fixed(2),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier.fillMaxSize(),
contentPadding = PaddingValues(bottom = 100.dp)
) {
items(categories) { category ->
CategoryCard(
category = category,
onClick = { onCategoryClick(category) }
)
}
item(span = { GridItemSpan(2) }) {
ExploreMoreCard(onClick = onExploreMoreClick)
}
}
}
/**
* Individual category card in grid view
*/
@Composable
fun CategoryCard(
category: VocabularyCategory,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier
.fillMaxWidth()
.height(140.dp)
.clickable(onClick = onClick),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.SpaceBetween
) {
Box(
modifier = Modifier
.size(40.dp)
.clip(RoundedCornerShape(8.dp))
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.15f)),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.LocalMall,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(24.dp)
)
}
Column {
Text(
text = category.name,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
LinearProgressIndicator(
progress = { 0.5f },
modifier = Modifier
.fillMaxWidth()
.height(4.dp)
.clip(RoundedCornerShape(2.dp)),
color = MaterialTheme.colorScheme.primary,
trackColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
)
}
}
}
}
/**
* Card to explore more categories
*/
@Composable
fun ExploreMoreCard(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val borderColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
Box(
modifier = modifier
.fillMaxWidth()
.padding(top = 8.dp)
.height(80.dp)
.clip(RoundedCornerShape(16.dp))
.clickable(onClick = onClick)
.drawBehind {
val stroke = Stroke(
width = 2.dp.toPx(),
pathEffect = PathEffect.dashPathEffect(floatArrayOf(20f, 20f), 0f)
)
drawRoundRect(
color = borderColor,
style = stroke,
cornerRadius = CornerRadius(16.dp.toPx(), 16.dp.toPx())
)
},
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(
imageVector = Icons.Default.AddCircleOutline,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Explore more categories",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
/**
* Crossfade container for switching between views
*/
@Composable
fun LibraryViewContainer(
isCategoriesView: Boolean,
categoriesContent: @Composable () -> Unit,
allCardsContent: @Composable () -> Unit,
modifier: Modifier = Modifier
) {
Crossfade(
targetState = isCategoriesView,
label = "LibraryViewTransition",
modifier = modifier
) { showCategories ->
if (showCategories) {
categoriesContent()
} else {
allCardsContent()
}
}
}
// ==================== PREVIEWS ====================
@Preview(showBackground = true)
@Composable
fun LibraryTopBarPreview() {
MaterialTheme {
LibraryTopBar(onAddClick = {})
}
}
@Preview(showBackground = true)
@Composable
fun SelectionTopBarPreview() {
MaterialTheme {
SelectionTopBar(
selectionCount = 5,
onCloseClick = {},
onSelectAllClick = {},
onDeleteClick = {},
onMoveToCategoryClick = {},
onMoveToStageClick = {},
isRemoveEnabled = true,
onRemoveFromCategoryClick = {}
)
}
}
@Preview(showBackground = true)
@Composable
fun SearchBarPreview() {
MaterialTheme {
SearchBar(
searchQuery = "",
onQueryChanged = {},
onFilterClick = {}
)
}
}
@Preview(showBackground = true)
@Composable
fun SegmentedControlPreview() {
MaterialTheme {
SegmentedControl(
isCategoriesView = false,
onTabSelected = {}
)
}
}
@Preview(showBackground = true)
@Composable
fun VocabularyCardPreview() {
MaterialTheme {
VocabularyCard(
item = VocabularyItem(
id = 1,
wordFirst = "Hello",
wordSecond = "Hola",
languageFirstId = 1,
languageSecondId = 2,
createdAt = null,
features = null,
zipfFrequencyFirst = null,
zipfFrequencySecond = null
),
allLanguages = emptyList(),
isSelected = false,
onItemClick = {},
onItemLongClick = {},
onDeleteClick = {}
)
}
}
@Preview(showBackground = true)
@Composable
fun CategoryCardPreview() {
MaterialTheme {
CategoryCard(
category = eu.gaudian.translator.model.TagCategory(
1,
"Travel Phrases"
),
onClick = {}
)
}
}
@Preview(showBackground = true)
@Composable
fun ExploreMoreCardPreview() {
MaterialTheme {
ExploreMoreCard(onClick = {})
}
}

View File

@@ -0,0 +1,553 @@
@file:Suppress("HardCodedStringLiteral")
package eu.gaudian.translator.view.library
import android.os.Parcelable
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.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
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.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.BottomSheetDefaults
import androidx.compose.material3.Button
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
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.model.VocabularyStage
import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppSwitch
import eu.gaudian.translator.view.composable.MultipleLanguageDropdown
import eu.gaudian.translator.view.dialogs.CategorySelectionDialog
import eu.gaudian.translator.view.dialogs.StageSelectionDialog
import eu.gaudian.translator.viewmodel.CategoryViewModel
import eu.gaudian.translator.viewmodel.LanguageConfigViewModel
import eu.gaudian.translator.viewmodel.LanguageViewModel
import eu.gaudian.translator.viewmodel.VocabularyViewModel
import eu.gaudian.translator.viewmodel.VocabularyViewModel.SortOrder
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
@Parcelize
data class LibraryFilterState(
val searchQuery: String = "",
val selectedStage: VocabularyStage? = null,
val sortOrder: SortOrder = SortOrder.NEWEST_FIRST,
val categoryIds: List<Int> = emptyList(),
val dueTodayOnly: Boolean = false,
val selectedLanguageIds: List<Int> = emptyList(),
val selectedWordClass: String? = null
) : Parcelable
@Composable
fun LibraryScreen(
navController: NavHostController,
modifier: Modifier = Modifier
) {
val scope = rememberCoroutineScope()
val activity = LocalContext.current.findActivity()
val lazyListState = rememberLazyListState()
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
val context = LocalContext.current
var filterState by rememberSaveable { mutableStateOf(LibraryFilterState()) }
var showFilterSheet by remember { mutableStateOf(false) }
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
var selection by remember { mutableStateOf<Set<Long>>(emptySet()) }
val isInSelectionMode = selection.isNotEmpty()
var showCategoryDialog by remember { mutableStateOf(false) }
var showStageDialog by remember { mutableStateOf(false) }
var isCategoriesView by remember { mutableStateOf(false) }
val allLanguages by languageViewModel.allLanguages.collectAsStateWithLifecycle(initialValue = emptyList())
val languagesPresent by vocabularyViewModel.languagesPresent.collectAsStateWithLifecycle(initialValue = emptySet())
val categories by vocabularyViewModel.categories.collectAsStateWithLifecycle()
val vocabularyItemsFlow = remember(filterState) {
vocabularyViewModel.filterVocabularyItems(
languages = filterState.selectedLanguageIds,
query = filterState.searchQuery.takeIf { it.isNotBlank() },
categoryIds = filterState.categoryIds,
stage = filterState.selectedStage,
wordClass = filterState.selectedWordClass,
dueTodayOnly = filterState.dueTodayOnly,
sortOrder = filterState.sortOrder
)
}
val vocabularyItems by vocabularyItemsFlow.collectAsStateWithLifecycle(initialValue = emptyList())
var isHeaderVisible by remember { mutableStateOf(true) }
var previousIndex by remember { mutableStateOf(0) }
var previousScrollOffset by remember { mutableStateOf(0) }
// Set navigation context when vocabulary items are loaded
LaunchedEffect(vocabularyItems) {
if (vocabularyItems.isNotEmpty()) {
vocabularyViewModel.setNavigationContext(vocabularyItems, vocabularyItems.first().id)
}
}
LaunchedEffect(isCategoriesView, isInSelectionMode) {
if (isCategoriesView || isInSelectionMode) {
isHeaderVisible = true
}
}
LaunchedEffect(lazyListState, isCategoriesView, isInSelectionMode) {
if (isCategoriesView || isInSelectionMode) return@LaunchedEffect
snapshotFlow { lazyListState.firstVisibleItemIndex to lazyListState.firstVisibleItemScrollOffset }
.collect { (index, offset) ->
val isScrollingDown = index > previousIndex || (index == previousIndex && offset > previousScrollOffset)
val isAtTop = index == 0 && offset <= 4
isHeaderVisible = if (isAtTop) true else !isScrollingDown
previousIndex = index
previousScrollOffset = offset
}
}
Box(
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.TopCenter
) {
Column(
modifier = Modifier
.widthIn(max = 700.dp)
.fillMaxSize()
.padding(horizontal = 24.dp),
) {
AnimatedVisibility(
visible = isHeaderVisible,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
Column {
Spacer(modifier = Modifier.height(24.dp))
if (isInSelectionMode) {
SelectionTopBar(
selectionCount = selection.size,
onCloseClick = { selection = emptySet() },
onSelectAllClick = {
selection = if (selection.size == vocabularyItems.size) emptySet()
else vocabularyItems.map { it.id.toLong() }.toSet()
},
onDeleteClick = {
vocabularyViewModel.deleteVocabularyItemsById(selection.map { it.toInt() })
selection = emptySet()
},
onMoveToCategoryClick = { showCategoryDialog = true },
onMoveToStageClick = { showStageDialog = true },
isRemoveEnabled = false,
onRemoveFromCategoryClick = {}
)
} else {
LibraryTopBar(
onAddClick = { /* TODO: Add new card/category */ }
)
}
Spacer(modifier = Modifier.height(24.dp))
SearchBar(
searchQuery = filterState.searchQuery,
onQueryChanged = { filterState = filterState.copy(searchQuery = it) },
onFilterClick = { showFilterSheet = true }
)
Spacer(modifier = Modifier.height(24.dp))
SegmentedControl(
isCategoriesView = isCategoriesView,
onTabSelected = { isCategoriesView = it }
)
Spacer(modifier = Modifier.height(24.dp))
}
}
LibraryViewContainer(
isCategoriesView = isCategoriesView,
categoriesContent = {
CategoriesView(
categories = categories,
onCategoryClick = { category ->
navController.navigate("category_detail/${category.id}")
},
onExploreMoreClick = {
navController.navigate("category_list_screen")
}
)
},
allCardsContent = {
AllCardsView(
vocabularyItems = vocabularyItems,
allLanguages = allLanguages,
selection = selection,
listState = lazyListState,
onItemClick = { item ->
if (isInSelectionMode) {
selection = if (selection.contains(item.id.toLong())) {
selection - item.id.toLong()
} else {
selection + item.id.toLong()
}
} else {
vocabularyViewModel.setNavigationContext(vocabularyItems, item.id)
navController.navigate("vocabulary_detail/${item.id}")
}
},
onItemLongClick = { item ->
if (!isInSelectionMode) {
selection = setOf(item.id.toLong())
}
},
onDeleteClick = { item ->
vocabularyViewModel.deleteData(
VocabularyViewModel.DeleteType.VOCABULARY_ITEM,
item = item
)
}
)
},
modifier = Modifier.weight(1f)
)
}
// Floating Action Button for scrolling to top
val showFab by remember {
derivedStateOf { lazyListState.firstVisibleItemIndex > 5 && !isInSelectionMode }
}
AnimatedVisibility(
visible = showFab,
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 16.dp)
) {
FloatingActionButton(
onClick = { scope.launch { lazyListState.animateScrollToItem(0) } },
shape = CircleShape,
modifier = Modifier.size(50.dp),
containerColor = MaterialTheme.colorScheme.surfaceVariant
) {
Icon(AppIcons.ArrowCircleUp, contentDescription = "Scroll to top")
}
}
}
if (showFilterSheet) {
ModalBottomSheet(
onDismissRequest = { showFilterSheet = false },
sheetState = sheetState,
containerColor = MaterialTheme.colorScheme.surface,
dragHandle = { BottomSheetDefaults.DragHandle() }
) {
FilterBottomSheetContent(
currentFilterState = filterState,
languageViewModel = languageViewModel,
languagesPresent = allLanguages.filter { it.nameResId in languagesPresent },
onApplyFilters = { newState ->
filterState = newState
showFilterSheet = false
scope.launch { lazyListState.scrollToItem(0) }
},
onResetClick = {
filterState = LibraryFilterState()
}
)
}
}
if (showCategoryDialog) {
val selectedItems = vocabularyItems.filter { selection.contains(it.id.toLong()) }
CategorySelectionDialog(
onCategorySelected = {
vocabularyViewModel.addVocabularyItemToCategories(
selectedItems,
it.mapNotNull { category -> category?.id }
)
showCategoryDialog = false
selection = emptySet()
},
onDismissRequest = { showCategoryDialog = false }
)
}
if (showStageDialog) {
val selectedItems = vocabularyItems.filter { selection.contains(it.id.toLong()) }
StageSelectionDialog(
onStageSelected = { selectedStage ->
selectedStage?.let {
vocabularyViewModel.addVocabularyItemToStage(selectedItems, it)
}
showStageDialog = false
selection = emptySet()
},
onDismissRequest = { showStageDialog = false }
)
}
}
@Composable
fun FilterBottomSheetContent(
currentFilterState: LibraryFilterState,
languageViewModel: LanguageViewModel,
languagesPresent: List<eu.gaudian.translator.model.Language>,
onApplyFilters: (LibraryFilterState) -> Unit,
onResetClick: () -> Unit,
modifier: Modifier = Modifier
) {
var selectedStage by rememberSaveable { mutableStateOf(currentFilterState.selectedStage) }
var dueTodayOnly by rememberSaveable { mutableStateOf(currentFilterState.dueTodayOnly) }
var selectedLanguageIds by rememberSaveable { mutableStateOf(currentFilterState.selectedLanguageIds) }
var selectedWordClass by rememberSaveable { mutableStateOf(currentFilterState.selectedWordClass) }
var sortOrder by rememberSaveable { mutableStateOf(currentFilterState.sortOrder) }
val context = LocalContext.current
val activity = LocalContext.current.findActivity()
val languageConfigViewModel: LanguageConfigViewModel = hiltViewModel(activity)
val allWordClasses by languageConfigViewModel.allWordClasses.collectAsStateWithLifecycle()
Column(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 24.dp)
.padding(bottom = 32.dp)
.navigationBarsPadding()
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Filter Cards",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
TextButton(onClick = {
selectedStage = null
dueTodayOnly = false
selectedLanguageIds = emptyList()
selectedWordClass = null
sortOrder = SortOrder.NEWEST_FIRST
onResetClick()
}) {
Text("Reset")
}
}
HorizontalDivider(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f))
Spacer(modifier = Modifier.height(16.dp))
Column(
modifier = Modifier
.weight(1f, fill = false)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
// Sort Order
Column {
Text(
text = "SORT BY",
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold,
letterSpacing = 1.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 12.dp)
)
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth()
) {
SortOrder.entries.forEach { order ->
FilterChip(
selected = sortOrder == order,
onClick = { sortOrder = order },
label = {
Text(order.name.replace('_', ' ').lowercase()
.replaceFirstChar { it.titlecase() })
}
)
}
}
}
// Due Today
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = stringResource(R.string.text_due_today_only).uppercase(),
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold,
letterSpacing = 1.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
AppSwitch(checked = dueTodayOnly, onCheckedChange = { dueTodayOnly = it })
}
// Stages
Column {
Text(
text = "STAGES",
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold,
letterSpacing = 1.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 12.dp)
)
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth()
) {
FilterChip(
selected = selectedStage == null,
onClick = { selectedStage = null },
label = { Text(stringResource(R.string.label_all_stages)) }
)
VocabularyStage.entries.forEach { stage ->
FilterChip(
selected = selectedStage == stage,
onClick = { selectedStage = stage },
label = { Text(stage.toString(context)) }
)
}
}
}
// Languages
Column {
Text(
text = stringResource(R.string.language).uppercase(),
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold,
letterSpacing = 1.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 12.dp)
)
MultipleLanguageDropdown(
languageViewModel = languageViewModel,
onLanguagesSelected = { languages ->
selectedLanguageIds = languages.map { it.nameResId }
},
alternateLanguages = languagesPresent
)
}
// Word Class
if (allWordClasses.isNotEmpty()) {
Column {
Text(
text = stringResource(R.string.filter_by_word_type).uppercase(),
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold,
letterSpacing = 1.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 12.dp)
)
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth()
) {
FilterChip(
selected = selectedWordClass == null,
onClick = { selectedWordClass = null },
label = { Text(stringResource(R.string.label_all_types)) }
)
allWordClasses.forEach { wordClass ->
FilterChip(
selected = selectedWordClass == wordClass,
onClick = { selectedWordClass = wordClass },
label = { Text(wordClass.replaceFirstChar { it.titlecase() }) }
)
}
}
}
}
}
Spacer(modifier = Modifier.height(24.dp))
Button(
onClick = {
onApplyFilters(
currentFilterState.copy(
selectedStage = selectedStage,
dueTodayOnly = dueTodayOnly,
selectedLanguageIds = selectedLanguageIds,
selectedWordClass = selectedWordClass,
sortOrder = sortOrder
)
)
},
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(28.dp)
) {
Text(
text = "Apply Filters",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
}
}
}

View File

@@ -17,7 +17,6 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
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.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -73,12 +72,8 @@ fun AboutScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.label_about)) }, title = stringResource(R.string.label_about),
navigationIcon = { onNavigateBack = { navController.popBackStack() }
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
}
) )
} }
) { paddingValues -> ) { paddingValues ->

View File

@@ -134,12 +134,8 @@ fun AddModelScreen(navController: NavController, providerKey: String) {
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(providerName) }, title = providerName,
navigationIcon = { onNavigateBack = { navController.popBackStack() },
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
},
hintContent = HintDefinition.ADD_MODEL_SCAN.hint() hintContent = HintDefinition.ADD_MODEL_SCAN.hint()
) )
}, },

View File

@@ -78,6 +78,7 @@ import eu.gaudian.translator.view.composable.AppTabLayout
import eu.gaudian.translator.view.composable.AppTopAppBar import eu.gaudian.translator.view.composable.AppTopAppBar
import eu.gaudian.translator.view.composable.ClickableText import eu.gaudian.translator.view.composable.ClickableText
import eu.gaudian.translator.view.composable.PrimaryButton 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.SecondaryButton
import eu.gaudian.translator.view.composable.TabItem import eu.gaudian.translator.view.composable.TabItem
import eu.gaudian.translator.view.hints.HintDefinition import eu.gaudian.translator.view.hints.HintDefinition
@@ -115,12 +116,8 @@ fun ApiKeyScreen(navController: NavController) {
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.label_ai_configuration)) }, title = stringResource(R.string.label_ai_configuration),
navigationIcon = { onNavigateBack = { navController.popBackStack() },
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
},
hintContent = HintDefinition.API_KEY.hint() hintContent = HintDefinition.API_KEY.hint()
) )
} }
@@ -137,7 +134,15 @@ fun ApiKeyScreen(navController: NavController) {
AppTabLayout( AppTabLayout(
tabs = apiTabs, tabs = apiTabs,
selectedTab = selectedTab, selectedTab = selectedTab,
onTabSelected = { selectedTab = it } onTabSelected = { selectedTab = it },
onNavigateBack = {
if (!navController.popBackStack()) {
navController.navigate(Screen.Home.route) {
launchSingleTop = true
restoreState = false
}
}
}
) )
// Tab Content // Tab Content

View File

@@ -5,9 +5,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
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
@@ -22,7 +19,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController import androidx.navigation.NavController
import eu.gaudian.translator.R import eu.gaudian.translator.R
import eu.gaudian.translator.utils.findActivity import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.AppIcons
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.viewmodel.ApiViewModel import eu.gaudian.translator.viewmodel.ApiViewModel
@@ -55,12 +51,8 @@ fun CustomVocabularyPromptScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.text_vocabulary_prompt)) }, title = stringResource(R.string.text_vocabulary_prompt),
navigationIcon = { onNavigateBack = { navController.popBackStack() },
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
},
hintContent = null //TODO: Add hint hintContent = null //TODO: Add hint
) )

View File

@@ -8,9 +8,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
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
@@ -31,7 +28,6 @@ import eu.gaudian.translator.R
import eu.gaudian.translator.utils.findActivity import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.LocalShowExperimentalFeatures import eu.gaudian.translator.view.LocalShowExperimentalFeatures
import eu.gaudian.translator.view.composable.AppCard 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.AppScaffold
import eu.gaudian.translator.view.composable.AppTopAppBar import eu.gaudian.translator.view.composable.AppTopAppBar
import eu.gaudian.translator.view.composable.OptionItemSwitch import eu.gaudian.translator.view.composable.OptionItemSwitch
@@ -66,12 +62,8 @@ fun DictionaryOptionsScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.label_dictionary_options)) }, title = stringResource(R.string.label_dictionary_options),
navigationIcon = { onNavigateBack = { navController.popBackStack() },
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
},
hintContent = HintDefinition.DICTIONARY_OPTIONS.hint() hintContent = HintDefinition.DICTIONARY_OPTIONS.hint()
) )
} }

View File

@@ -8,8 +8,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -31,7 +29,6 @@ import eu.gaudian.translator.R
import eu.gaudian.translator.utils.findActivity import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.ApiModelDropDown import eu.gaudian.translator.view.composable.ApiModelDropDown
import eu.gaudian.translator.view.composable.AppCard import eu.gaudian.translator.view.composable.AppCard
import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppOutlinedTextField import eu.gaudian.translator.view.composable.AppOutlinedTextField
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
@@ -71,12 +68,8 @@ fun ExerciseSettingsScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.exercise_settings)) }, title = stringResource(R.string.exercise_settings),
navigationIcon = { onNavigateBack = { navController.popBackStack() },
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
}
) )
} }
) { paddingValues -> ) { paddingValues ->

View File

@@ -7,8 +7,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -24,7 +22,6 @@ import androidx.navigation.NavController
import eu.gaudian.translator.R import eu.gaudian.translator.R
import eu.gaudian.translator.utils.findActivity import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.AppCard 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.AppScaffold
import eu.gaudian.translator.view.composable.AppSwitch import eu.gaudian.translator.view.composable.AppSwitch
import eu.gaudian.translator.view.composable.AppTopAppBar import eu.gaudian.translator.view.composable.AppTopAppBar
@@ -41,12 +38,8 @@ fun GeneralSettingsScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.label_general)) }, title = stringResource(R.string.label_general),
navigationIcon = { onNavigateBack = { navController.popBackStack() },
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
}
) )
} }
) { paddingValues -> ) { paddingValues ->

View File

@@ -61,12 +61,8 @@ fun LanguageOptionsScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.text_language_options)) }, title = stringResource(R.string.text_language_options),
navigationIcon = { onNavigateBack = { navController.popBackStack() },
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
}
) )
} }
) { paddingValues -> ) { paddingValues ->

View File

@@ -35,7 +35,6 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
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.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
@@ -101,12 +100,8 @@ fun LayoutOptionsScreen(navController: NavController) {
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.label_appearance)) }, title = stringResource(R.string.label_appearance),
navigationIcon = { onNavigateBack = { navController.popBackStack() },
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, contentDescription = cdBack)
}
}
) )
} }
) { paddingValues -> ) { paddingValues ->

View File

@@ -101,15 +101,8 @@ fun LogsScreen(navController: NavController) {
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.label_logs)) }, title = stringResource(R.string.label_logs),
navigationIcon = { onNavigateBack = { navController.popBackStack() },
IconButton(onClick = { navController.popBackStack() }) {
Icon(
imageVector = AppIcons.ArrowBack,
contentDescription = stringResource(R.string.cd_back)
)
}
},
actions = { actions = {
TextButton(onClick = { TextButton(onClick = {
settingsViewModel.clearApiLogs() settingsViewModel.clearApiLogs()

View File

@@ -27,6 +27,7 @@ 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.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.composable.Screen
private data class Setting(val titleRes: Int, val icon: ImageVector, val route: String) private data class Setting(val titleRes: Int, val icon: ImageVector, val route: String)
@@ -84,7 +85,15 @@ fun MainSettingsScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.title_settings), style = MaterialTheme.typography.titleLarge) } title =stringResource(R.string.title_settings),
onNavigateBack = {
if (!navController.popBackStack()) {
navController.navigate(Screen.Home.route) {
launchSingleTop = true
restoreState = false
}
}
}
) )
} }
) { paddingValues -> ) { paddingValues ->

View File

@@ -15,8 +15,6 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -86,12 +84,8 @@ fun TextToSpeechSettingsScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.settings_title_voice)) }, title = stringResource(R.string.settings_title_voice),
navigationIcon = { onNavigateBack = { navController.popBackStack() },
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
}
) )
} }
) { paddingValues -> ) { paddingValues ->

View File

@@ -9,9 +9,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
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
@@ -27,7 +24,6 @@ import androidx.navigation.NavController
import eu.gaudian.translator.R import eu.gaudian.translator.R
import eu.gaudian.translator.utils.findActivity import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.AppCard 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.AppScaffold
import eu.gaudian.translator.view.composable.AppTopAppBar import eu.gaudian.translator.view.composable.AppTopAppBar
import eu.gaudian.translator.view.composable.OptionItemSwitch import eu.gaudian.translator.view.composable.OptionItemSwitch
@@ -64,12 +60,8 @@ fun TranslationSettingsScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.label_translation_settings)) }, title = stringResource(R.string.label_translation_settings),
navigationIcon = { onNavigateBack = { navController.popBackStack() },
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
},
hintContent = null //TODO add hint hintContent = null //TODO add hint
) )
} }

View File

@@ -21,7 +21,6 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
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.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
@@ -78,13 +77,8 @@ fun VocabularyProgressOptionsScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.vocabulary_settings)) }, title = stringResource(R.string.vocabulary_settings),
navigationIcon = { onNavigateBack = { navController.popBackStack() },
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
},
// Here is the new hint content
hintContent = HintDefinition.VOCABULARY_PROGRESS.hint() hintContent = HintDefinition.VOCABULARY_PROGRESS.hint()
) )
} }

View File

@@ -18,8 +18,6 @@ import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
@@ -44,7 +42,6 @@ import eu.gaudian.translator.utils.StatusMessageService
import eu.gaudian.translator.utils.findActivity 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.AppCard import eu.gaudian.translator.view.composable.AppCard
import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppOutlinedButton import eu.gaudian.translator.view.composable.AppOutlinedButton
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
@@ -200,12 +197,8 @@ fun VocabularyRepositoryOptionsScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.vocabulary_repository)) }, title = stringResource(R.string.vocabulary_repository),
navigationIcon = { onNavigateBack = { navController.popBackStack() },
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
}
) )
} }
) { paddingValues -> ) { paddingValues ->

View File

@@ -0,0 +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()
.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 = 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")
}
}
}

View File

@@ -67,9 +67,23 @@ fun ActionBar(
} }
@Composable @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)) { 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) { if (hintContent != null) {
WithHint(hintContent = hintContent) { WithHint(hintContent = hintContent) {
} }

View File

@@ -106,6 +106,14 @@ fun TranslationScreen(
settingsViewModel = settingsViewModel, settingsViewModel = settingsViewModel,
onHistoryClick = onHistoryClick, onHistoryClick = onHistoryClick,
onSettingsClick = onSettingsClick, onSettingsClick = onSettingsClick,
onNavigateBack = {
if (!navController.popBackStack()) {
navController.navigate(eu.gaudian.translator.view.composable.Screen.Home.route) {
launchSingleTop = true
restoreState = false
}
}
},
context = context context = context
) )
} }
@@ -119,6 +127,7 @@ private fun LoadedTranslationContent(
settingsViewModel: SettingsViewModel, settingsViewModel: SettingsViewModel,
onHistoryClick: () -> Unit, onHistoryClick: () -> Unit,
onSettingsClick: () -> Unit, onSettingsClick: () -> Unit,
onNavigateBack: () -> Unit,
context: Context context: Context
) { ) {
val inputText by translationViewModel.inputText.collectAsState() val inputText by translationViewModel.inputText.collectAsState()
@@ -167,6 +176,7 @@ private fun LoadedTranslationContent(
TopBarActions( TopBarActions(
languageViewModel = languageViewModel, languageViewModel = languageViewModel,
onSettingsClick = onSettingsClick, onSettingsClick = onSettingsClick,
onNavigateBack = onNavigateBack,
hintContent = { HintDefinition.TRANSLATION.Render() } hintContent = { HintDefinition.TRANSLATION.Render() }
) )

View File

@@ -5,14 +5,16 @@ package eu.gaudian.translator.view.vocabulary
import android.annotation.SuppressLint import android.annotation.SuppressLint
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.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
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.heightIn import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@@ -30,7 +32,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
@@ -43,10 +46,12 @@ import eu.gaudian.translator.view.composable.AppIcons
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.composable.PrimaryButton import eu.gaudian.translator.view.composable.PrimaryButton
import eu.gaudian.translator.view.composable.SecondaryButton
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.widgets.CategoryProgressCircle
import eu.gaudian.translator.viewmodel.CategoryProgress
import eu.gaudian.translator.viewmodel.CategoryViewModel import eu.gaudian.translator.viewmodel.CategoryViewModel
import eu.gaudian.translator.viewmodel.LanguageViewModel import eu.gaudian.translator.viewmodel.LanguageViewModel
import eu.gaudian.translator.viewmodel.ProgressViewModel import eu.gaudian.translator.viewmodel.ProgressViewModel
@@ -89,11 +94,10 @@ fun CategoryDetailScreen(
if (!hasLangList && !hasPair && !hasStages) { if (!hasLangList && !hasPair && !hasStages) {
append(stringResource(R.string.text_filter_all_items)) append(stringResource(R.string.text_filter_all_items))
} else { } else {
//append(stringResource(R.string.filter))
append(" ") append(" ")
if (hasPair) { if (hasPair) {
val (a,b) = cat.languagePairs val (a, b) = cat.languagePairs
append("[${languages.value.find{ it.nameResId == a }?.name} - ${languages.value.find{ it.nameResId == b }?.name}]") append("[${languages.value.find { it.nameResId == a }?.name} - ${languages.value.find { it.nameResId == b }?.name}]")
} else if (hasLangList) { } else if (hasLangList) {
append(cat.languages.joinToString(", ") { langId -> languages.value.find { it.nameResId == langId }?.name.toString() }) append(cat.languages.joinToString(", ") { langId -> languages.value.find { it.nameResId == langId }?.name.toString() })
} else { } else {
@@ -118,30 +122,8 @@ fun CategoryDetailScreen(
modifier = Modifier.background(MaterialTheme.colorScheme.surface) modifier = Modifier.background(MaterialTheme.colorScheme.surface)
) { ) {
AppTopAppBar( AppTopAppBar(
title = { title = title,
Column { onNavigateBack = { navController.popBackStack() },
Text(
text = title,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = subtitle,
style = MaterialTheme.typography.bodySmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
},
navigationIcon = {
IconButton(onClick = onBackClick) {
Icon(
AppIcons.ArrowBack,
contentDescription = stringResource(R.string.cd_back)
)
}
},
actions = { actions = {
IconButton(onClick = { showMenu = !showMenu }) { IconButton(onClick = { showMenu = !showMenu }) {
Icon( Icon(
@@ -150,85 +132,49 @@ fun CategoryDetailScreen(
) )
} }
DropdownMenu( DropdownMenu(
expanded = showMenu, expanded = showMenu,
onDismissRequest = { showMenu = false }, onDismissRequest = { showMenu = false },
modifier = Modifier.width(220.dp) modifier = Modifier.width(220.dp)
) { ) {
DropdownMenuItem(
text = { Text(stringResource(R.string.text_edit_category)) },
onClick = {
categoryViewModel.setShowEditCategoryDialog(true, categoryId)
showMenu = false
}
)
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringResource(R.string.text_export_category)) }, text = { Text(stringResource(R.string.text_export_category)) },
onClick = { onClick = {
vocabularyViewModel.saveCategory(categoryId) vocabularyViewModel.saveCategory(categoryId)
showMenu = false showMenu = false
} },
leadingIcon = { Icon(AppIcons.Share, contentDescription = null) }
) )
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringResource(R.string.delete_items_category)) }, text = { Text(stringResource(R.string.delete_items_category)) },
onClick = { onClick = {
categoryViewModel.setShowDeleteItemsDialog(true, categoryId) categoryViewModel.setShowDeleteItemsDialog(true, categoryId)
showMenu = false showMenu = false
} },
) leadingIcon = { Icon(AppIcons.Delete, contentDescription = null) }
DropdownMenuItem(
text = { Text(stringResource(R.string.text_delete_category)) },
onClick = {
categoryViewModel.setShowDeleteCategoryDialog(true, categoryId)
showMenu = false
}
) )
} }
}, },
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
// TODO: Review this
containerColor = MaterialTheme.colorScheme.surface containerColor = MaterialTheme.colorScheme.surface
) )
) )
Row( // Category Header Card with Progress and Action Buttons
modifier = Modifier CategoryHeaderCard(
.fillMaxWidth() subtitle = subtitle,
.padding(vertical = 8.dp, horizontal = 16.dp), categoryProgress = categoryProgress,
horizontalArrangement = Arrangement.SpaceEvenly, onStartExerciseClick = {
verticalAlignment = Alignment.CenterVertically val categories = listOf(category)
) { val categoryIds = categories.joinToString(",") { it?.id.toString() }
if (categoryProgress != null) { navController.navigate("vocabulary_exercise/false?categories=$categoryIds")
Box(modifier = Modifier.weight(1f)) { },
CategoryProgressCircle( onEditClick = {
totalItems = categoryProgress.totalItems, categoryViewModel.setShowEditCategoryDialog(true, categoryId)
itemsCompleted = categoryProgress.itemsCompleted, },
itemsInStages = categoryProgress.itemsInStages, onDeleteClick = {
newItems = categoryProgress.newItems, categoryViewModel.setShowDeleteCategoryDialog(true, categoryId)
circleSize = 80.dp,
)
}
} else {
Spacer(modifier = Modifier.weight(1f))
} }
)
Box(
modifier = Modifier
.weight(1f)
.padding(start = 8.dp)
) {
PrimaryButton(
text = stringResource(R.string.label_start),
icon = AppIcons.Play,
onClick = {
val categories = listOf(category)
val categoryIds = categories.joinToString(",") { it?.id.toString() }
navController.navigate("vocabulary_exercise/false?categories=$categoryIds")
},
modifier = Modifier.heightIn(max = 80.dp)
)
}
}
} }
} }
) { paddingValues -> ) { paddingValues ->
@@ -237,7 +183,7 @@ fun CategoryDetailScreen(
categoryId = categoryId, categoryId = categoryId,
showDueTodayOnly = false, showDueTodayOnly = false,
onNavigateToItem = onNavigateToItem, onNavigateToItem = onNavigateToItem,
navController = navController, // Pass the received navController here navController = navController,
isRemoveFromCategoryEnabled = category is TagCategory, isRemoveFromCategoryEnabled = category is TagCategory,
showTopBar = false, showTopBar = false,
enableNavigationButtons = true enableNavigationButtons = true
@@ -266,3 +212,131 @@ fun CategoryDetailScreen(
} }
} }
} }
@Composable
fun CategoryHeaderCard(
subtitle: String,
categoryProgress: CategoryProgress?,
onStartExerciseClick: () -> Unit,
onEditClick: () -> Unit,
onDeleteClick: () -> Unit,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Subtitle
if (subtitle.isNotBlank()) {
Text(
text = subtitle,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier.padding(bottom = 16.dp)
)
}
// Progress Circle
if (categoryProgress != null) {
CategoryProgressCircle(
totalItems = categoryProgress.totalItems,
itemsCompleted = categoryProgress.itemsCompleted,
itemsInStages = categoryProgress.itemsInStages,
newItems = categoryProgress.newItems,
circleSize = 120.dp,
)
Spacer(modifier = Modifier.height(24.dp))
}
// Action Buttons
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// Start Exercise Button (Primary)
PrimaryButton(
text = stringResource(R.string.label_start_exercise),
icon = AppIcons.Play,
onClick = onStartExerciseClick,
modifier = Modifier.weight(1f)
)
}
Spacer(modifier = Modifier.height(12.dp))
// Secondary Action Buttons
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// Edit Button
SecondaryButton(
text = stringResource(R.string.label_edit),
icon = AppIcons.Edit,
onClick = onEditClick,
modifier = Modifier.weight(1f)
)
// Delete Button
SecondaryButton(
text = stringResource(R.string.label_delete),
icon = AppIcons.Delete,
onClick = onDeleteClick,
modifier = Modifier.weight(1f)
)
}
}
}
}
// ==================== PREVIEWS ====================
@Preview(showBackground = true)
@Composable
fun CategoryHeaderCardPreview() {
MaterialTheme {
CategoryHeaderCard(
subtitle = "German - English | All Stages",
categoryProgress = null,
onStartExerciseClick = {},
onEditClick = {},
onDeleteClick = {}
)
}
}
@Preview(showBackground = true)
@Composable
fun CategoryHeaderCardWithProgressPreview() {
MaterialTheme {
CategoryHeaderCard(
subtitle = "Travel Vocabulary",
categoryProgress = eu.gaudian.translator.viewmodel.CategoryProgress(
vocabularyCategory = eu.gaudian.translator.model.TagCategory(
1,
"Travel"
),
totalItems = 50,
newItems = 15,
itemsInStages = 25,
itemsCompleted = 10
),
onStartExerciseClick = {},
onEditClick = {},
onDeleteClick = {}
)
}
}

View File

@@ -100,13 +100,7 @@ fun CategoryListScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { title = "TODO",
if (isSelectionMode && selectedCategories.isNotEmpty()) {
Text(stringResource(R.string.text_2d_categories_selected, selectedCategories.size))
} else {
Text(stringResource(R.string.label_categories))
}
},
navigationIcon = { navigationIcon = {
if (isSelectionMode) { if (isSelectionMode) {
IconButton(onClick = { IconButton(onClick = {

View File

@@ -78,7 +78,7 @@ fun LanguageProgressScreen(navController: NavController) {
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.your_language_journey)) }, title = stringResource(R.string.your_language_journey),
onNavigateBack = { navController.popBackStack() } onNavigateBack = { navController.popBackStack() }
) )
}, },

View File

@@ -56,6 +56,7 @@ import eu.gaudian.translator.view.composable.AppOutlinedCard
import eu.gaudian.translator.view.composable.AppSlider import eu.gaudian.translator.view.composable.AppSlider
import eu.gaudian.translator.view.composable.AppTabLayout import eu.gaudian.translator.view.composable.AppTabLayout
import eu.gaudian.translator.view.composable.OptionItemSwitch 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.composable.TabItem
import eu.gaudian.translator.view.dialogs.StartExerciseDialog import eu.gaudian.translator.view.dialogs.StartExerciseDialog
import eu.gaudian.translator.view.dialogs.VocabularyMenu import eu.gaudian.translator.view.dialogs.VocabularyMenu
@@ -329,6 +330,14 @@ fun MainVocabularyScreen(
launchSingleTop = true launchSingleTop = true
restoreState = true restoreState = true
} }
},
onNavigateBack = {
if (!navController.popBackStack()) {
navController.navigate(Screen.Home.route) {
launchSingleTop = true
restoreState = false
}
}
} }
) )

File diff suppressed because it is too large Load Diff

View File

@@ -14,8 +14,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -39,7 +37,6 @@ import androidx.navigation.NavController
import eu.gaudian.translator.R import eu.gaudian.translator.R
import eu.gaudian.translator.utils.findActivity 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.AppSlider import eu.gaudian.translator.view.composable.AppSlider
import eu.gaudian.translator.view.composable.AppTopAppBar import eu.gaudian.translator.view.composable.AppTopAppBar
import eu.gaudian.translator.viewmodel.VocabularyViewModel import eu.gaudian.translator.viewmodel.VocabularyViewModel
@@ -66,12 +63,8 @@ fun NoGrammarItemsScreen(
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.title_items_without_grammar)) }, title = stringResource(R.string.title_items_without_grammar),
navigationIcon = { onNavigateBack = { navController.popBackStack() },
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
},
) )
Box( Box(
modifier = Modifier modifier = Modifier

View File

@@ -2,9 +2,6 @@ package eu.gaudian.translator.view.vocabulary
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -17,7 +14,6 @@ import androidx.navigation.NavHostController
import eu.gaudian.translator.R import eu.gaudian.translator.R
import eu.gaudian.translator.model.VocabularyStage import eu.gaudian.translator.model.VocabularyStage
import eu.gaudian.translator.utils.findActivity import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.AppIcons
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.vocabulary.widgets.DetailedStageProgressBar
@@ -40,15 +36,8 @@ fun StageDetailScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(text = stringResource(R.string.due_today_, stage.toString())) }, title = stringResource(R.string.due_today_, stage.toString()),
navigationIcon = { onNavigateBack = { navController.popBackStack() },
IconButton(onClick = { navController.popBackStack() }) {
Icon(
AppIcons.ArrowBack,
contentDescription =stringResource(R.string.cd_back)
)
}
}
) )
} }
) { paddingValues -> ) { paddingValues ->

View File

@@ -157,7 +157,7 @@ private fun StartScreenContent(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.prepare_exercise)) }, title = stringResource(R.string.prepare_exercise),
navigationIcon = { navigationIcon = {
IconButton(onClick = onClose) { IconButton(onClick = onClose) {
Icon( Icon(

View File

@@ -75,21 +75,8 @@ fun VocabularyCardHost(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
modifier = Modifier.height(56.dp), modifier = Modifier.height(56.dp),
title = { title = stringResource(R.string.item_details),
if (navigationItems.isNotEmpty()) { onNavigateBack = { navController.popBackStack() },
Text(stringResource(R.string.label_card_with_position, navigationPosition + 1, navigationItems.size))
} else {
Text(stringResource(R.string.item_details))
}
},
navigationIcon = {
IconButton(onClick = { onBackPressed?.invoke() }) {
Icon(
AppIcons.ArrowBack,
contentDescription = stringResource(R.string.cd_back)
)
}
},
actions = { actions = {
// Previous button // Previous button
if (navigationPosition > 0) { if (navigationPosition > 0) {

View File

@@ -95,7 +95,7 @@ fun VocabularyHeatmapScreen(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.label_vocabulary_activity)) }, title = stringResource(R.string.label_vocabulary_activity),
onNavigateBack = { navController.popBackStack() }, onNavigateBack = { navController.popBackStack() },
) )
}, },

View File

@@ -446,25 +446,8 @@ private fun DefaultTopAppBar(
var showSortMenu by remember { mutableStateOf(false) } var showSortMenu by remember { mutableStateOf(false) }
AppTopAppBar( AppTopAppBar(
modifier = Modifier.height(56.dp), modifier = Modifier.height(56.dp),
title = { title = title,
onNavigateBack = onNavigateBack,
Box(
modifier = Modifier.fillMaxHeight(),
contentAlignment = Alignment.Center
) {
Text(title)
}
},
navigationIcon = {
onNavigateBack?.let {
IconButton(onClick = it) {
Icon(
AppIcons.ArrowBack,
contentDescription = "stringResource(R.string.navigate_back)"
)
}
}
},
actions = { actions = {
IconButton(onClick = onSearchClick) { IconButton(onClick = onSearchClick) {
Icon( Icon(
@@ -534,7 +517,8 @@ private fun SearchTopAppBar(
AppTopAppBar( AppTopAppBar(
modifier = Modifier.height(56.dp), modifier = Modifier.height(56.dp),
title = { title = "TODO",
additionalContent = {
Box( Box(
modifier = Modifier.fillMaxHeight(), modifier = Modifier.fillMaxHeight(),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
@@ -605,14 +589,7 @@ private fun ContextualTopAppBar(
AppTopAppBar( AppTopAppBar(
modifier = modifier.height(56.dp), modifier = modifier.height(56.dp),
title = { title = stringResource(R.string.d_selected, selectionCount),
Box(
modifier = Modifier.fillMaxHeight(),
contentAlignment = Alignment.Center
) {
Text(stringResource(R.string.d_selected, selectionCount))
}
},
navigationIcon = { navigationIcon = {
IconButton(onClick = onCloseClick) { IconButton(onClick = onCloseClick) {
Icon(imageVector = AppIcons.Close, contentDescription = stringResource(R.string.label_close_selection_mode)) Icon(imageVector = AppIcons.Close, contentDescription = stringResource(R.string.label_close_selection_mode))

View File

@@ -155,7 +155,7 @@ fun VocabularySortingScreen(
var showFilterMenu by remember { mutableStateOf(false) } var showFilterMenu by remember { mutableStateOf(false) }
AppTopAppBar( AppTopAppBar(
title = { Text(stringResource(R.string.sort_new_vocabulary)) }, title = stringResource(R.string.sort_new_vocabulary),
actions = { actions = {
Box { Box {
IconButton(onClick = { showFilterMenu = true }) { IconButton(onClick = { showFilterMenu = true }) {
@@ -231,11 +231,7 @@ fun VocabularySortingScreen(
} }
} }
}, },
navigationIcon = { onNavigateBack = { navController.popBackStack() },
IconButton(onClick = { navController.popBackStack() }) {
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.cd_back))
}
},
hintContent = HintDefinition.SORTING.hint() hintContent = HintDefinition.SORTING.hint()
) )
}, },

View File

@@ -61,7 +61,7 @@
<string-array name="changelog_entries"> <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.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.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 hints and help content, added more instcructions and help \n• UI changes in the flashcards with a more intuitive design \n• Lots of bugfixes \n• Improved translations for German and Portuguese</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> <item> </item>
</string-array> </string-array>

View File

@@ -288,6 +288,7 @@
<string name="label_hard">Hard</string> <string name="label_hard">Hard</string>
<string name="label_header_row">First Row is a Header</string> <string name="label_header_row">First Row is a Header</string>
<string name="label_hide_examples">Hide examples</string> <string name="label_hide_examples">Hide examples</string>
<string name="label_home">Home</string>
<string name="label_import">Import</string> <string name="label_import">Import</string>
<string name="label_import_table_csv_excel">Import Table (CSV)</string> <string name="label_import_table_csv_excel">Import Table (CSV)</string>
<string name="label_in_stages">In Stages</string> <string name="label_in_stages">In Stages</string>
@@ -1114,4 +1115,8 @@
<string name="message_test_info">This is a generic info message.</string> <string name="message_test_info">This is a generic info message.</string>
<string name="message_test_success">This is a test success message!</string> <string name="message_test_success">This is a test success message!</string>
<string name="message_test_error">Oops, something went wrong :(</string> <string name="message_test_error">Oops, something went wrong :(</string>
<string name="label_stats">Stats</string>
<string name="label_library">Library</string>
<string name="label_legacy_vocabulary">Legacy Vocabulary</string>
<string name="label_edit">Edit</string>
</resources> </resources>