Compare commits

...

3 Commits

8 changed files with 129 additions and 59 deletions

View File

@@ -109,10 +109,19 @@ class ExampleSentenceRequest(
override val requiredFields = listOf("word", "sourceSentence", "targetSentence") override val requiredFields = listOf("word", "sourceSentence", "targetSentence")
init { init {
promptBuilder.basePrompt = "Provide one short, simple and clear example sentence for the word '$word' in $languageFirst and fully translate the sentence to $languageSecond, using $wordTranslation as a translation." promptBuilder.basePrompt = """
addDetail("Structure: { 'word': string, 'sourceSentence': string, 'targetSentence': string }.") Task: Create a short, concise natural example sentence in $languageFirst for the word '$word'.
addDetail("Only include the fields above. Keep sentences concise and clear. Do not include any explanations or additional text.")
withJsonResponse("a JSON object with 'word' (the original word), 'sourceSentence' (the example sentence in the source language), and 'targetSentence' (the translation in the target language). Ensure all values are properly quoted strings.") Rules:
1. The 'sourceSentence' must be entirely in $languageFirst.
2. Do NOT use the word '$wordTranslation' in the 'sourceSentence'.
3. The 'targetSentence' must be the $languageSecond translation of the 'sourceSentence'.
4. In the 'targetSentence', use the word '$wordTranslation'.
""".trimIndent()
addDetail("Constraint: Ensure 'sourceSentence' contains ONLY $languageFirst and 'targetSentence' contains ONLY $languageSecond.")
addDetail("Structure: { 'word': '$word', 'sourceSentence': string, 'targetSentence': string }.")
withJsonResponse("a JSON object. Ensure no mixed-language sentences occur.")
} }
} }

View File

@@ -90,14 +90,14 @@ class DictionaryService(context: Context) {
@OptIn(ExperimentalTime::class) @OptIn(ExperimentalTime::class)
suspend fun searchDefinition(query: String, language: Language): Result<DictionaryEntry> = withContext(Dispatchers.IO) { suspend fun searchDefinition(query: String, language: Language): Result<DictionaryEntry> = withContext(Dispatchers.IO) {
try { try {
Log.i("DictionaryService", "Searching definition for word: $query in language: ${language.name}") Log.i("DictionaryService", "Searching definition for word: $query in language: ${language.englishName}")
val requestedParts = getActivatedDictionaryOptions() val requestedParts = getActivatedDictionaryOptions()
Log.d("DictionaryService", "Requested dictionary parts: $requestedParts") Log.d("DictionaryService", "Requested dictionary parts: $requestedParts")
val template = DictionaryDefinitionRequest( val template = DictionaryDefinitionRequest(
word = query, word = query,
language = language.name, language = language.englishName,
requestedParts = requestedParts requestedParts = requestedParts
) )
@@ -110,7 +110,7 @@ class DictionaryService(context: Context) {
word = apiResponse.word, word = apiResponse.word,
definition = apiResponse.parts, definition = apiResponse.parts,
languageCode = language.nameResId, languageCode = language.nameResId,
languageName = language.name, languageName = language.englishName,
createdAt = Clock.System.now() createdAt = Clock.System.now()
) )
}.onFailure { exception -> }.onFailure { exception ->
@@ -127,13 +127,13 @@ class DictionaryService(context: Context) {
*/ */
suspend fun getExampleSentence(word: String, wordTranslation: String, languageFirst: Language, languageSecond: Language): Result<Pair<String, String>> = withContext(Dispatchers.IO) { suspend fun getExampleSentence(word: String, wordTranslation: String, languageFirst: Language, languageSecond: Language): Result<Pair<String, String>> = withContext(Dispatchers.IO) {
try { try {
Log.i("DictionaryService", "Getting example sentence for word: $word (${languageFirst.name} -> ${languageSecond.name})") Log.i("DictionaryService", "Getting example sentence for word: $word (${languageFirst.englishName} -> ${languageSecond.englishName})")
val template = ExampleSentenceRequest( val template = ExampleSentenceRequest(
word = word, word = word,
wordTranslation = wordTranslation, wordTranslation = wordTranslation,
languageFirst = languageFirst.name, languageFirst = languageFirst.englishName,
languageSecond = languageSecond.name languageSecond = languageSecond.englishName
) )
val result = apiRequestHandler.executeRequest(template) val result = apiRequestHandler.executeRequest(template)
@@ -179,7 +179,7 @@ class DictionaryService(context: Context) {
} }
} }
Log.i("DictionaryService", "Generating new word of the day for: $todayString in language: ${language.name}") Log.i("DictionaryService", "Generating new word of the day for: $todayString in language: ${language.englishName}")
val topics = listOf( val topics = listOf(
"science", "literature", "history", "technology", "nature", "science", "literature", "history", "technology", "nature",
@@ -192,7 +192,7 @@ class DictionaryService(context: Context) {
Log.d("DictionaryService", "Selected topic for word of the day: $randomTopic") Log.d("DictionaryService", "Selected topic for word of the day: $randomTopic")
val template = WordOfTheDayRequest( val template = WordOfTheDayRequest(
language = language.name, language = language.englishName,
category = randomTopic category = randomTopic
) )
@@ -205,7 +205,7 @@ class DictionaryService(context: Context) {
word = apiResponse.word, word = apiResponse.word,
definition = apiResponse.parts, definition = apiResponse.parts,
languageCode = language.nameResId, languageCode = language.nameResId,
languageName = language.name, languageName = language.englishName,
createdAt = today createdAt = today
) )
dictionaryRepository.saveWordOfTheDay(newEntry) dictionaryRepository.saveWordOfTheDay(newEntry)
@@ -225,7 +225,7 @@ class DictionaryService(context: Context) {
suspend fun getEtymology(query: String, language: Language): Result<EtymologyData> = withContext(Dispatchers.IO) { suspend fun getEtymology(query: String, language: Language): Result<EtymologyData> = withContext(Dispatchers.IO) {
try { try {
Log.i("DictionaryService", "Getting etymology for word: $query in language: ${language.name}") Log.i("DictionaryService", "Getting etymology for word: $query in language: ${language.englishName}")
val template = EtymologyRequest( val template = EtymologyRequest(
word = query, word = query,

View File

@@ -1,6 +1,7 @@
package eu.gaudian.translator.view.composable package eu.gaudian.translator.view.composable
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.Spring import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring import androidx.compose.animation.core.spring
@@ -55,7 +56,8 @@ data class FabMenuItem(
fun AppFabMenu( fun AppFabMenu(
items: List<FabMenuItem>, items: List<FabMenuItem>,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
title: String? = null title: String? = null,
showFabText: Boolean = true
) { ) {
var isMenuExpanded by remember { mutableStateOf(false) } var isMenuExpanded by remember { mutableStateOf(false) }
@@ -111,14 +113,16 @@ fun AppFabMenu(
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(horizontal = 16.dp) modifier = Modifier
.padding(horizontal = 16.dp)
.animateContentSize()
) { ) {
Icon( Icon(
imageVector = AppIcons.Add, imageVector = AppIcons.Add,
contentDescription = stringResource(R.string.cd_toggle_menu), contentDescription = stringResource(R.string.cd_toggle_menu),
modifier = Modifier.rotate(iconRotationAngle) modifier = Modifier.rotate(iconRotationAngle)
) )
if (title != null) { if (title != null && showFabText) {
Text( Text(
text = title, text = title,
style = MaterialTheme.typography.labelLarge style = MaterialTheme.typography.labelLarge

View File

@@ -22,6 +22,7 @@ import eu.gaudian.translator.viewmodel.VocabularyViewModel
@Composable @Composable
fun VocabularyMenu( fun VocabularyMenu(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
showFabText : Boolean = true
) { ) {
val activity = LocalContext.current.findActivity() val activity = LocalContext.current.findActivity()
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity) val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
@@ -48,7 +49,7 @@ fun VocabularyMenu(
) )
) )
AppFabMenu(items = menuItems, modifier = modifier, title = stringResource(R.string.label_add_vocabulary)) AppFabMenu(items = menuItems, modifier = modifier, title = stringResource(R.string.label_add_vocabulary), showFabText = showFabText)
if (showAddVocabularyDialog) { if (showAddVocabularyDialog) {
AddVocabularyDialog( AddVocabularyDialog(

View File

@@ -88,6 +88,7 @@ fun DashboardContent(
onNavigateToCategoryDetail: (Int) -> Unit, onNavigateToCategoryDetail: (Int) -> Unit,
onNavigateToCategoryList: () -> Unit, onNavigateToCategoryList: () -> Unit,
onShowWordPairExerciseDialog: () -> Unit, onShowWordPairExerciseDialog: () -> Unit,
onScroll: (Boolean) -> Unit = {},
) { ) {
val activity = LocalContext.current.findActivity() val activity = LocalContext.current.findActivity()
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity) val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
@@ -167,6 +168,11 @@ fun DashboardContent(
lazyListState.firstVisibleItemScrollOffset lazyListState.firstVisibleItemScrollOffset
) )
} }
// Detect scroll and notify parent
LaunchedEffect(lazyListState.isScrollInProgress) {
onScroll(lazyListState.isScrollInProgress)
}
DisposableEffect(Unit) { DisposableEffect(Unit) {
onDispose { onDispose {
settingsViewModel.saveDashboardScrollState( settingsViewModel.saveDashboardScrollState(

View File

@@ -4,6 +4,7 @@ package eu.gaudian.translator.view.vocabulary
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.LocalActivity import androidx.activity.compose.LocalActivity
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -112,6 +113,9 @@ fun MainVocabularyScreen(
var wpTrainingMode by remember { mutableStateOf(false) } var wpTrainingMode by remember { mutableStateOf(false) }
var wpDueTodayOnly by remember { mutableStateOf(false) } var wpDueTodayOnly by remember { mutableStateOf(false) }
var isScrolling by remember { mutableStateOf(false) }
if (showCustomExerciseDialog) { if (showCustomExerciseDialog) {
StartExerciseDialog( StartExerciseDialog(
onDismiss = { showCustomExerciseDialog = false }, onDismiss = { showCustomExerciseDialog = false },
@@ -293,6 +297,8 @@ fun MainVocabularyScreen(
VocabularyTab.entries.find { it.route == currentRoute } ?: VocabularyTab.Dashboard VocabularyTab.entries.find { it.route == currentRoute } ?: VocabularyTab.Dashboard
} }
val showFabText = selectedTab == VocabularyTab.Dashboard && !isScrolling
val repoEmpty = val repoEmpty =
vocabularyViewModel.vocabularyItems.collectAsState(initial = emptyList()).value.isEmpty() vocabularyViewModel.vocabularyItems.collectAsState(initial = emptyList()).value.isEmpty()
@@ -333,7 +339,8 @@ fun MainVocabularyScreen(
onNavigateToCategoryList = { onNavigateToCategoryList = {
navController.navigate("category_list_screen") navController.navigate("category_list_screen")
}, },
onShowWordPairExerciseDialog = { showWordPairExerciseDialog = true } onShowWordPairExerciseDialog = { showWordPairExerciseDialog = true },
onScroll = { isScrolling = it }
) )
} }
composable(VocabularyTab.Statistics.route) { composable(VocabularyTab.Statistics.route) {
@@ -390,7 +397,7 @@ fun MainVocabularyScreen(
.padding(16.dp), .padding(16.dp),
horizontalAlignment = Alignment.End horizontalAlignment = Alignment.End
) { ) {
VocabularyMenu(modifier = Modifier.onSizeChanged { menuHeightPx = it.height }) VocabularyMenu(modifier = Modifier.onSizeChanged { menuHeightPx = it.height }, showFabText = showFabText)
} }
// Place the FAB separately and animate its bottom padding based on the menu height // Place the FAB separately and animate its bottom padding based on the menu height
@@ -405,16 +412,19 @@ fun MainVocabularyScreen(
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(horizontal = 16.dp) modifier = Modifier
.padding(horizontal = 16.dp)
.animateContentSize()
) { ) {
Icon( Icon(
imageVector = AppIcons.Quiz, imageVector = AppIcons.Quiz,
contentDescription = null contentDescription = null
) )
if(showFabText) {
Text( Text(
text = stringResource(R.string.label_start_exercise), text = stringResource(R.string.label_start_exercise),
style = MaterialTheme.typography.labelLarge style = MaterialTheme.typography.labelLarge
) )}
} }
} }
} }

View File

@@ -10,11 +10,14 @@ import androidx.compose.foundation.layout.IntrinsicSize
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.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.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
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.Card import androidx.compose.material3.Card
@@ -23,6 +26,7 @@ import androidx.compose.material3.CircularProgressIndicator
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.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedCard import androidx.compose.material3.OutlinedCard
@@ -41,7 +45,9 @@ 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.blur import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -52,6 +58,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle import androidx.compose.ui.text.withStyle
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.compose.ui.zIndex
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.R
@@ -294,9 +301,17 @@ fun VocabularyCard(
} }
) )
Box(
modifier = Modifier
.fillMaxWidth()
.height(0.dp) // Take 0 structural space
.wrapContentHeight(unbounded = true) // Allow children to render at their full size
.zIndex(1f), // Ensure it draws over the bottom CardFace
contentAlignment = Alignment.Center
) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 4.dp) modifier = Modifier.fillMaxWidth()
) { ) {
HorizontalDivider( HorizontalDivider(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
@@ -312,6 +327,7 @@ fun VocabularyCard(
} }
} }
} }
}
CardFace( CardFace(
modifier = backFaceModifier, modifier = backFaceModifier,
@@ -442,28 +458,52 @@ private fun FrequencyPill(zipfFrequency: Float?) {
if (zipfFrequency == null) return if (zipfFrequency == null) return
val semanticColors = MaterialTheme.semanticColors val semanticColors = MaterialTheme.semanticColors
// Determine the label and color based on the Zipf value
val (label, color) = when { val (label, color) = when {
zipfFrequency <= 3f -> Pair(stringResource(R.string.text_rare), semanticColors.wrongContainer) zipfFrequency <= 2.0f -> Pair(stringResource(R.string.text_rare), semanticColors.wrongContainer)
zipfFrequency <= 4f -> Pair(stringResource(R.string.text_infrequent), semanticColors.stageGradient2) zipfFrequency <= 3.0f -> Pair(stringResource(R.string.text_infrequent), semanticColors.stageGradient2)
zipfFrequency <= 4.5f -> Pair(stringResource(R.string.text_uncommon), semanticColors.stageGradient3) zipfFrequency <= 4.0f -> Pair(stringResource(R.string.text_uncommon), semanticColors.stageGradient3)
zipfFrequency <= 5.5f -> Pair(stringResource(R.string.text_common), semanticColors.stageGradient4) zipfFrequency <= 5.5f -> Pair(stringResource(R.string.text_common), semanticColors.stageGradient4)
zipfFrequency <= 6.5f -> Pair(stringResource(R.string.text_frequent), semanticColors.stageGradient5) zipfFrequency <= 6.0f -> Pair(stringResource(R.string.text_frequent), semanticColors.stageGradient5)
zipfFrequency <= 7.5f -> Pair(stringResource(R.string.text_very_frequent), semanticColors.stageGradient6)
else -> Pair(stringResource(R.string.text_very_frequent), semanticColors.successContainer) else -> Pair(stringResource(R.string.text_very_frequent), semanticColors.successContainer)
} }
// Normalize progress: Zipf 1.0 to 8.0 mapped to 0f to 1f
val progress = ((zipfFrequency - 1f) / (8f - 1f)).coerceIn(0f, 1f)
Column(
modifier = Modifier
.padding(horizontal = 4.dp)
.width(80.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Surface( Surface(
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
color = color.copy(alpha = 0.5f), color = color.copy(alpha = 0.2f),
modifier = Modifier.padding(horizontal = 4.dp)
) { ) {
Text( Text(
text = label, text = label,
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp) modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp)
) )
} }
Spacer(modifier = Modifier.height(4.dp))
// The "Slider" representation
LinearProgressIndicator(
progress = { progress },
modifier = Modifier
.fillMaxWidth()
.height(4.dp)
.clip(CircleShape),
color = color,
trackColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f),
strokeCap = StrokeCap.Round
)
}
} }
@Composable @Composable

View File

@@ -986,15 +986,15 @@ class VocabularyViewModel @Inject constructor(
} }
suspend fun getExampleForItem(itemId: Int, isFirstWord: Boolean, languageFirst: Language?, languageSecond: Language?): Pair<String, String>? { suspend fun getExampleForItem(itemId: Int, isFirstWord: Boolean, languageFirst: Language?, languageSecond: Language?): Pair<String, String>? {
Log.d(TAG, "Fetching example for item ID $itemId")
val item = getVocabularyItemById(itemId) ?: return null val item = getVocabularyItemById(itemId) ?: return null
val word = if (isFirstWord) item.wordFirst else item.wordSecond
val wordTranslation = if (!isFirstWord) item.wordFirst else item.wordSecond
if (languageFirst == null || languageSecond == null) return null if (languageFirst == null || languageSecond == null) return null
return dictionaryService.getExampleSentence(word, wordTranslation, languageFirst, languageSecond).getOrNull() return if (isFirstWord) {
dictionaryService.getExampleSentence(item.wordFirst , item.wordSecond, languageFirst, languageSecond).getOrNull()
} else {
dictionaryService.getExampleSentence(item.wordSecond, item.wordFirst , languageSecond, languageFirst).getOrNull()
}
} }
suspend fun fetchAndUpdateZipfFrequency(item: VocabularyItem): VocabularyItem { suspend fun fetchAndUpdateZipfFrequency(item: VocabularyItem): VocabularyItem {