Refactor navigation and cleanup resources across the application

This commit is contained in:
jonasgaudian
2026-02-17 16:45:23 +01:00
parent db959dab20
commit f39375e9df
53 changed files with 112 additions and 2032 deletions

View File

@@ -4,11 +4,8 @@ package eu.gaudian.translator
object TestConfig {
// REPLACE with your actual API Key for the test
const val API_KEY = "YOUR_REAL_API_KEY_HERE"
// Set to true if you want to see full log output in Logcat
const val ENABLE_LOGGING = true
const val API_KEY = "YOUR_REAL_API_KEY_HERE"
// Optional: If your ApiManager requires a specific provider (e.g., "Mistral", "OpenAI")
const val PROVIDER_NAME = "Mistral"

View File

@@ -1,5 +1,3 @@
@file:Suppress("unused", "HardCodedStringLiteral")
package eu.gaudian.translator.di
import android.app.Application

View File

@@ -56,6 +56,7 @@ object LocalDictionaryMorphologyMapper {
/**
* Overload that uses parsed [DictionaryEntryData] instead of raw JSON.
*/
@Suppress("unused")
fun parseMorphology(
langCode: String,
pos: String?,

View File

@@ -144,19 +144,6 @@ class ApiRepository(private val context: Context) {
var configurationValid = true
// (Helper function to reduce repetition)
fun checkAndFallback(current: LanguageModel?, setter: suspend (LanguageModel) -> Unit) {
val isValid = current != null && availableModels.any { it.modelId == current.modelId && it.providerKey == current.providerKey }
if (!isValid) {
val fallback = findFallbackModel(availableModels)
if (fallback != null) {
// We must use a blocking call or scope here because we can't easily pass a suspend function to a lambda
// But since we are inside a suspend function, we can just call the setter directly if we unroll the loop.
// For simplicity, I'll keep the unrolled logic below.
}
}
}
// Fallback checks
if (currentTrans == null || !availableModels.any { it.modelId == currentTrans.modelId && it.providerKey == currentTrans.providerKey }) {
findFallbackModel(availableModels)?.let { setTranslationModel(it) } ?: run { configurationValid = false }

View File

@@ -76,6 +76,7 @@ class LanguageRepository(private val context: Context) {
}
}
@Suppress("unused")
suspend fun wipeHistoryAndFavorites() {
clearLanguages(LanguageListType.HISTORY)
clearLanguages(LanguageListType.FAVORITE)

View File

@@ -129,25 +129,6 @@ class JsonHelper {
*/
class JsonParsingException(message: String, cause: Throwable? = null) : Exception(message, cause)
/**
* Legacy JsonHelper class for backward compatibility.
* @deprecated Use the enhanced JsonHelper class instead
*/
@Deprecated("Use the enhanced JsonHelper class instead")
class LegacyJsonHelper {
fun cleanJson(json: String): String {
val startIndex = json.indexOf('{')
val endIndex = json.lastIndexOf('}')
if (startIndex == -1 || endIndex == -1 || startIndex >= endIndex) {
throw IllegalArgumentException("Invalid JSON format")
}
return json.substring(startIndex, endIndex + 1).trim()
}
}
object JsonCleanUtil {
private val jsonParser = Json { isLenient = true; ignoreUnknownKeys = true }

View File

@@ -10,6 +10,7 @@ import timber.log.Timber
* "HardcodedText" lint warning for log messages, which are for
* development purposes only.
*/
@Suppress("unused")
object Log {
@SuppressLint("HardcodedText")

View File

@@ -55,6 +55,12 @@ enum class StatusMessageId(
ERROR_FILE_PICKER_NOT_INITIALIZED(R.string.message_error_file_picker_not_initialized, MessageDisplayType.ERROR, 5),
SUCCESS_CATEGORY_SAVED(R.string.message_success_category_saved, MessageDisplayType.SUCCESS, 3),
ERROR_EXCEL_NOT_SUPPORTED(R.string.message_error_excel_not_supported, MessageDisplayType.ERROR, 5),
ERROR_PARSING_TABLE(R.string.error_parsing_table, MessageDisplayType.ERROR, 5),
ERROR_PARSING_TABLE_WITH_REASON(R.string.error_parsing_table_with_reason, MessageDisplayType.ERROR, 5),
ERROR_SELECT_TWO_COLUMNS(R.string.error_select_two_columns, MessageDisplayType.ERROR, 5),
ERROR_SELECT_LANGUAGES(R.string.error_select_languages, MessageDisplayType.ERROR, 5),
ERROR_NO_ROWS_TO_IMPORT(R.string.error_no_rows_to_import, MessageDisplayType.ERROR, 5),
SUCCESS_ITEMS_IMPORTED(R.string.info_imported_items_from, MessageDisplayType.SUCCESS, 3),
// API Key related

View File

@@ -75,7 +75,6 @@ object StatusMessageService {
* @deprecated Use showMessageById() instead for internationalization support.
*/
@Deprecated("Use showMessageById() for internationalization support", ReplaceWith("showMessageById(messageId)"))
@Suppress("unused")
fun showSimpleMessage(text: String, type: MessageDisplayType = MessageDisplayType.INFO) {
scope.launch {
_actions.emit(StatusAction.ShowMessage(text, type, 5))

View File

@@ -117,7 +117,6 @@ class TranslationService(private val context: Context) {
}
suspend fun translateSentence(sentence: String): Result<TranslationHistoryItem> = withContext(Dispatchers.IO) {
val statusMessageService = StatusMessageService
val additionalInstructions = settingsRepository.customPromptTranslation.flow.first()
val selectedSource = languageRepository.loadSelectedSourceLanguage().first()
val sourceLangName = selectedSource?.englishName ?: "Auto"

View File

@@ -1,11 +0,0 @@
package eu.gaudian.translator.utils.dictionary
import eu.gaudian.translator.model.grammar.Inflection
/**
* Interface for a language-specific inflection parser.
*/
interface InflectionParser {
fun parse(inflections: List<Inflection>): DisplayInflectionData
}

View File

@@ -5,11 +5,6 @@ package eu.gaudian.translator.utils.dictionary
* Either a simple list or a complex, grouped verb conjugation table.
*/
sealed class DisplayInflectionData {
data class VerbConjugation(
val gerund: String? = null,
val participle: String? = null,
val moods: List<DisplayMood>
) : DisplayInflectionData()
}
data class DisplayMood(

View File

@@ -55,10 +55,10 @@ private const val TRANSITION_DURATION = 300
object NavigationRoutes {
const val NEW_WORD = "new_word"
const val NEW_WORD_REVIEW = "new_word_review"
const val VOCABULARY_DETAIL = "vocabulary_detail"
const val START_EXERCISE = "start_exercise"
const val CATEGORY_DETAIL = "category_detail"
const val CATEGORY_LIST = "category_list_screen"
const val VOCABULARY_DETAIL = "vocabulary_detail"
const val STATS_VOCABULARY_HEATMAP = "stats/vocabulary_heatmap"
const val STATS_LANGUAGE_PROGRESS = "stats/language_progress"
const val STATS_CATEGORY_DETAIL = "stats/category_detail"

View File

@@ -2,26 +2,15 @@ package eu.gaudian.translator.view.composable
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.FabPosition
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.ScaffoldDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import eu.gaudian.translator.R
@Composable
fun AppScaffold(
@@ -58,37 +47,3 @@ fun AppScaffold(
}
@Composable
fun ParrotTopBar() {
val navyBlue = Color(0xFF1A237E) // The color from your mockup
CenterAlignedTopAppBar(
title = {
Text(
text = "ParrotPal",
style = MaterialTheme.typography.titleLarge,
color = Color.White
)
},
navigationIcon = {
// Your new parrot logo icon
Icon(
painter = painterResource(id = R.drawable.ic_level_parrot),
contentDescription = "Logo",
modifier = Modifier.size(32.dp),
tint = Color.Unspecified // Keeps the logo's original colors
)
},
actions = {
IconButton(onClick = { /* Search */ }) {
Icon(Icons.Default.Search, contentDescription = "Search", tint = Color.White)
}
IconButton(onClick = { /* Profile */ }) {
Icon(Icons.Default.AccountCircle, contentDescription = "Profile", tint = Color.White)
}
},
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = navyBlue
)
)
}

View File

@@ -1,5 +1,3 @@
@file:Suppress("HardCodedStringLiteral")
package eu.gaudian.translator.view.composable
import android.annotation.SuppressLint
@@ -40,8 +38,8 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.gaudian.translator.ui.theme.ThemePreviews
import eu.gaudian.translator.R
import eu.gaudian.translator.ui.theme.ThemePreviews
/**
* An interface that defines the required properties for any item
@@ -51,15 +49,9 @@ interface TabItem {
val title: String
val icon: ImageVector
}
/**
* A generic, reusable tab layout composable.
* @param T The type of the tab item, which must implement the TabItem interface.
* @param tabs A list of all tab items to display.
* @param selectedTab The currently selected tab item.
* @param onTabSelected A lambda function to be invoked when a tab is clicked.
*/
@SuppressLint("UnusedBoxWithConstraintsScope", "LocalContextResourcesRead", "DiscouragedApi")
@SuppressLint("UnusedBoxWithConstraintsScope", "LocalContextResourcesRead", "DiscouragedApi",
"SuspiciousIndentation"
)
@Composable
fun <T : TabItem> AppTabLayout(
tabs: List<T>,
@@ -175,6 +167,7 @@ fun <T : TabItem> AppTabLayout(
}
}
@Suppress("HardCodedStringLiteral")
@ThemePreviews
@Composable
fun ModernTabLayoutPreview() {

View File

@@ -1,4 +1,4 @@
@file:Suppress("HardCodedStringLiteral")
@file:Suppress("HardCodedStringLiteral", "AssignedValueIsNeverRead")
package eu.gaudian.translator.view.composable

View File

@@ -6,8 +6,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@@ -28,8 +26,6 @@ fun CategorySelectionDialog(
) {
val activity = LocalContext.current.findActivity()
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
AppDialog(
onDismissRequest = onDismissRequest,

View File

@@ -1,132 +0,0 @@
package eu.gaudian.translator.view.dialogs
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
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.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import eu.gaudian.translator.R
import eu.gaudian.translator.model.Language
import eu.gaudian.translator.model.VocabularyCategory
import eu.gaudian.translator.model.VocabularyStage
import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.AppDialog
import eu.gaudian.translator.view.composable.MultipleLanguageDropdown
import eu.gaudian.translator.viewmodel.CategoryViewModel
import eu.gaudian.translator.viewmodel.LanguageViewModel
import eu.gaudian.translator.viewmodel.VocabularyViewModel
import kotlinx.coroutines.launch
@Composable
fun StartExerciseDialog(
onDismiss: () -> Unit,
onConfirm: (
categories: List<VocabularyCategory>,
stages: List<VocabularyStage>,
languageIds: List<Int>
) -> Unit
) {
val activity = LocalContext.current.findActivity()
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
val coroutineScope = rememberCoroutineScope()
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
var lids by remember { mutableStateOf<List<Int>>(emptyList()) }
var languages by remember { mutableStateOf<List<Language>>(emptyList()) }
// Map displayed Language to its DB id (lid) using position mapping from load
var languageIdMap by remember { mutableStateOf<Map<Language, Int>>(emptyMap()) }
var selectedLanguages by remember { mutableStateOf<List<Language>>(emptyList()) }
var selectedStages by remember { mutableStateOf<List<VocabularyStage>>(emptyList()) }
var selectedCategories by remember { mutableStateOf<List<VocabularyCategory>>(emptyList()) }
LaunchedEffect(Unit) {
coroutineScope.launch {
lids = vocabularyViewModel.getAllLanguagesIdsPresent().filterNotNull().toList()
languages = lids.map { lid ->
languageViewModel.getLanguageById(lid)
}
// build reverse map
languageIdMap = languages.mapIndexed { index, lang -> lang to lids.getOrNull(index) }.filter { it.second != null }.associate { it.first to it.second!! }
}
}
AppDialog(onDismissRequest = onDismiss, title = { Text(stringResource(R.string.label_start_exercise)) }) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
MultipleLanguageDropdown(
modifier = Modifier.fillMaxWidth(),
languageViewModel = languageViewModel,
onLanguagesSelected = { langs ->
selectedLanguages = langs
},
languages
)
CategoryDropdown(
onCategorySelected = { cats ->
selectedCategories = cats.filterIsInstance<VocabularyCategory>()
},
multipleSelectable = true,
onlyLists = false, // Show both filters and lists
addCategory = false,
modifier = Modifier.fillMaxWidth(),
)
VocabularyStageDropDown(
modifier = Modifier.fillMaxWidth(),
preselectedStages = selectedStages,
onStageSelected = { stages ->
@Suppress("FilterIsInstanceResultIsAlwaysEmpty")
selectedStages = stages.filterIsInstance<VocabularyStage>()
},
multipleSelectable = true
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp),
horizontalArrangement = Arrangement.End
) {
TextButton(
onClick = onDismiss,
) {
Text(stringResource(R.string.label_cancel))
}
TextButton(
onClick = {
run {
val ids = selectedLanguages.mapNotNull { languageIdMap[it] }
onConfirm(selectedCategories, selectedStages, ids)
}
}
) {
Text(stringResource(R.string.label_start_exercise))
}
}
}
}
}

View File

@@ -35,7 +35,6 @@ import eu.gaudian.translator.view.composable.AppCheckbox
import eu.gaudian.translator.view.composable.AppScaffold
import eu.gaudian.translator.view.composable.AppTopAppBar
import eu.gaudian.translator.view.hints.HintDefinition
import eu.gaudian.translator.viewmodel.CategoryViewModel
import eu.gaudian.translator.viewmodel.VocabularyViewModel
@Composable
@@ -45,11 +44,9 @@ fun VocabularyReviewScreen(
) {
val activity = LocalContext.current.findActivity()
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(activity)
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
val generatedItems: List<VocabularyItem> by vocabularyViewModel.generatedVocabularyItems.collectAsState()
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
val selectedItems = remember { mutableStateListOf<VocabularyItem>() }
val duplicates = remember { mutableStateListOf<Boolean>() }
var selectedCategories by remember { mutableStateOf<List<VocabularyCategory?>>(emptyList()) }

View File

@@ -381,7 +381,7 @@ fun DefinitionPart(part: EntryPart) {
// Fallback for JsonObject or other top-level types
else -> contentElement.toString()
}
} catch (e: Exception) {
} catch (_: Exception) {
// Ultimate fallback if something else goes wrong during parsing
part.content.toString()
}
@@ -466,12 +466,6 @@ fun DefinitionPartPreview() {
DefinitionPart(part = mockPart)
}
// Data classes for the refactored components
data class EntryData(
val entry: DictionaryEntry,
val language: Language?
)
data class BreadcrumbItem(
val word: String,
val entryId: Int

View File

@@ -1,3 +1,5 @@
@file:Suppress("SameParameterValue")
package eu.gaudian.translator.view.dictionary
import androidx.compose.animation.animateContentSize

View File

@@ -37,6 +37,7 @@ import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@@ -58,7 +59,6 @@ import eu.gaudian.translator.model.Language
import eu.gaudian.translator.model.TagCategory
import eu.gaudian.translator.model.VocabularyCategory
import eu.gaudian.translator.model.VocabularyStage
import eu.gaudian.translator.utils.Log
import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.AppButton
import eu.gaudian.translator.view.composable.AppIcons
@@ -125,10 +125,9 @@ fun StartExerciseScreen(
ids
}
var amount by remember { mutableStateOf(0) }
var amount by remember { mutableIntStateOf(0) }
androidx.compose.runtime.LaunchedEffect(totalItemCount) {
amount = totalItemCount
Log.d("StartExercise", "Items to show updated: total=$totalItemCount, amount=$amount")
}
val updateConfig: (eu.gaudian.translator.viewmodel.ExerciseConfig) -> Unit = { config ->
@@ -251,14 +250,12 @@ fun StartExerciseScreen(
enabled = totalItemCount > 0 && amount > 0,
amount = amount,
onStart = {
Log.d("StartExercise", "Start pressed. shuffleCards=${exerciseConfig.shuffleCards}, selectedAmount=$amount, items=${itemsToShow.size}, origin=${selectedOriginLanguage?.nameResId}, target=${selectedTargetLanguage?.nameResId}, pairs=${selectedPairsIds.size}, categories=${selectedCategoryIds.size}, stages=${selectedStages.size}")
val finalItems = if (exerciseConfig.shuffleCards) {
itemsToShow.shuffled().take(amount)
} else {
itemsToShow.take(amount)
}
Log.d("StartExercise", "Final items prepared: count=${finalItems.size}")
exerciseViewModel.startExerciseWithConfig(
finalItems,
@@ -270,7 +267,7 @@ fun StartExerciseScreen(
)
)
Log.d("StartExercise", "Navigating to vocabulary_exercise/false")
@Suppress("HardCodedStringLiteral")
navController.navigate("vocabulary_exercise/false")
}
)
@@ -307,7 +304,7 @@ fun TopBarSection(
) {
Icon(
imageVector = Icons.Default.ArrowBackIosNew,
contentDescription = "Back",
contentDescription = stringResource(R.string.cd_back),
modifier = Modifier.size(18.dp),
tint = MaterialTheme.colorScheme.primary
)
@@ -329,7 +326,7 @@ fun TopBarSection(
) {
Icon(
imageVector = Icons.Default.Settings,
contentDescription = "Settings",
contentDescription = stringResource(R.string.cd_settings),
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.primary
)
@@ -349,6 +346,7 @@ fun TopBarSection(
onDismiss = {
scope.launch { sheetState.hide() }.invokeOnCompletion {
if (!sheetState.isVisible) {
@Suppress("AssignedValueIsNeverRead")
showSettings = false
}
}
@@ -360,7 +358,9 @@ fun TopBarSection(
@Composable
fun SectionHeader(title: String, actionText: String? = null, onActionClick: () -> Unit = {}) {
Row(
modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
@@ -398,7 +398,6 @@ fun LanguagePairSection(
val activity = androidx.compose.ui.platform.LocalContext.current.findActivity()
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
val languagesPresent by vocabularyViewModel.languagesPresent.collectAsState(initial = emptySet())
val allLanguages by languageViewModel.allLanguages.collectAsState(initial = emptyList())
val availableLanguages = remember(availableLanguageIds, allLanguages) {
@@ -549,8 +548,16 @@ fun LanguageChip(text: String, isSelected: Boolean, modifier: Modifier = Modifie
) {
// 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))
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)
@@ -690,7 +697,9 @@ fun NumberOfCardsSection(
}
Row(
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {

View File

@@ -40,7 +40,6 @@ enum class HintDefinition(
@Composable
fun hint(definition: HintDefinition): Hint = definition.hint()
@Composable fun HintContent(definition: HintDefinition) = definition.Render()
@Composable fun HintScreen(navController: NavController, definition: HintDefinition) = HintScreen(
navController = navController,
title = stringResource(definition.titleRes),

View File

@@ -1,5 +1,3 @@
@file:Suppress("HardCodedStringLiteral")
package eu.gaudian.translator.view.hints
import androidx.compose.foundation.layout.Arrangement

View File

@@ -47,6 +47,7 @@ object MarkdownHintLoader {
append(language.lowercase())
}
if (country.isNotEmpty()) {
@Suppress("HardCodedStringLiteral")
append("-r")
append(country.uppercase())
}

View File

@@ -128,6 +128,7 @@ fun LanguageOptionsScreen(
}
if (showAddLanguageDialog) {
@Suppress("KotlinConstantConditions")
AddCustomLanguageDialog(
showDialog = showAddLanguageDialog,
onDismiss = { showAddLanguageDialog = false },

View File

@@ -96,7 +96,6 @@ fun LayoutOptionsScreen(navController: NavController) {
val selectedFontName by settingsViewModel.fontPreference.collectAsStateWithLifecycle()
val showBottomNavLabels by settingsViewModel.showBottomNavLabels.collectAsStateWithLifecycle()
val cdBack = stringResource(R.string.cd_back)
AppScaffold(
topBar = {
AppTopAppBar(

View File

@@ -77,7 +77,7 @@ fun VocabularyProgressOptionsScreen(
AppScaffold(
topBar = {
AppTopAppBar(
title = stringResource(R.string.vocabulary_settings),
title = stringResource(R.string.label_vocabulary_settings),
onNavigateBack = { navController.popBackStack() },
hintContent = HintDefinition.VOCABULARY_PROGRESS.hint()
)
@@ -101,7 +101,7 @@ fun VocabularyProgressOptionsScreen(
@Suppress("USELESS_ELVIS", "HardCodedStringLiteral")
SettingsSlider(
label = stringResource(R.string.target_correct_answers_per_day),
label = stringResource(R.string.label_target_correct_answers_per_day),
value = dailyGoal ?: 10,
onValueChange = { settingsViewModel.setDailyGoal(it) },
valueRange = 10f..100f,

View File

@@ -55,7 +55,6 @@ 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

View File

@@ -136,6 +136,7 @@ fun NewWordReviewScreen(
onConfirm = {
val selectedCategoryIds = selectedCategories.filterNotNull().map { it.id }
vocabularyViewModel.addVocabularyItems(selectedItems.toList(), selectedCategoryIds)
@Suppress("HardCodedStringLiteral")
navController.popBackStack("new_word", inclusive = false)
},
modifier = Modifier.padding(16.dp)

View File

@@ -23,7 +23,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AutoAwesome
import androidx.compose.material.icons.filled.DriveFolderUpload
import androidx.compose.material.icons.filled.EditNote
import androidx.compose.material.icons.filled.LibraryBooks
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
@@ -62,12 +61,15 @@ import eu.gaudian.translator.model.VocabularyItem
import eu.gaudian.translator.utils.StatusMessageId
import eu.gaudian.translator.utils.StatusMessageService
import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.NavigationRoutes
import eu.gaudian.translator.view.composable.AppButton
import eu.gaudian.translator.view.composable.AppCard
import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppOutlinedButton
import eu.gaudian.translator.view.composable.AppSlider
import eu.gaudian.translator.view.composable.AppTopAppBar
import eu.gaudian.translator.view.composable.InspiringSearchField
import eu.gaudian.translator.view.composable.Screen
import eu.gaudian.translator.view.composable.SourceLanguageDropdown
import eu.gaudian.translator.view.composable.TargetLanguageDropdown
import eu.gaudian.translator.view.hints.HintDefinition
@@ -96,7 +98,7 @@ fun NewWordScreen(
LaunchedEffect(isGenerating, generatedItems, navigateToReview) {
if (navigateToReview && !isGenerating) {
if (generatedItems.isNotEmpty()) {
navController.navigate("new_word_review")
navController.navigate(NavigationRoutes.NEW_WORD_REVIEW)
}
navigateToReview = false
}
@@ -112,7 +114,6 @@ fun NewWordScreen(
var skipHeader by remember { mutableStateOf(true) }
var selectedLangFirst by remember { mutableStateOf<Language?>(null) }
var selectedLangSecond by remember { mutableStateOf<Language?>(null) }
var parseError by remember { mutableStateOf<String?>(null) }
val recentlyAdded = remember(recentItems) {
recentItems.sortedByDescending { it.id }.take(4)
@@ -172,8 +173,6 @@ fun NewWordScreen(
}.filter { r -> r.any { it.isNotBlank() } }
}
val errorParsingTable = stringResource(R.string.error_parsing_table)
val errorParsingTableWithReason = stringResource(R.string.error_parsing_table_with_reason)
val importTableLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument(),
onResult = { uri ->
@@ -197,34 +196,32 @@ fun NewWordScreen(
selectedColFirst = 0
selectedColSecond = 1.coerceAtMost(rows.first().size - 1)
showTableImportDialog.value = true
parseError = null
} else {
parseError = errorParsingTable
statusMessageService.showErrorMessage(parseError!!)
statusMessageService.showErrorById(StatusMessageId.ERROR_PARSING_TABLE)
}
}
} catch (e: Exception) {
parseError = e.message
statusMessageService.showErrorMessage(
(errorParsingTableWithReason + " " + e.message)
)
} catch (_: Exception) {
statusMessageService.showErrorById(StatusMessageId.ERROR_PARSING_TABLE_WITH_REASON)
}
}
}
)
Box(
modifier = modifier.fillMaxSize().padding(16.dp),
modifier = modifier
.fillMaxSize()
.padding(16.dp),
contentAlignment = Alignment.TopCenter
) {
Column(
modifier = Modifier
.widthIn(max = 700.dp) // Perfect scaling for tablets/foldables
.fillMaxSize()
.verticalScroll(rememberScrollState()).padding(0.dp)
.verticalScroll(rememberScrollState())
.padding(0.dp)
) {
AppTopAppBar(
title = "New Words",
title = stringResource(R.string.label_new_words),
onNavigateBack = { navController.popBackStack() }
)
@@ -282,12 +279,12 @@ fun NewWordScreen(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Recently Added",
text = stringResource(R.string.label_recently_added),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
TextButton(onClick = { navController.navigate("library") }) {
Text("View All")
TextButton(onClick = { navController.navigate(Screen.Library.route) }) {
Text(stringResource(R.string.label_view_all))
}
}
Spacer(modifier = Modifier.height(12.dp))
@@ -297,7 +294,10 @@ fun NewWordScreen(
item = item,
allLanguages = allLanguages,
isSelected = false,
onItemClick = { navController.navigate("vocabulary_detail/${item.id}") },
onItemClick = {
@Suppress("HardCodedStringLiteral")
navController.navigate("${NavigationRoutes.VOCABULARY_DETAIL}/${item.id}")
},
onItemLongClick = {},
onDeleteClick = {}
)
@@ -388,19 +388,15 @@ fun NewWordScreen(
}
},
confirmButton = {
val errorSelectTwoColumns = stringResource(R.string.error_select_two_columns)
val errorSelectLanguages = stringResource(R.string.error_select_languages)
val errorNoRowsToImport = stringResource(R.string.error_no_rows_to_import)
val infoImportedItemsFrom = stringResource(R.string.info_imported_items_from)
TextButton(onClick = {
if (selectedColFirst == selectedColSecond) {
statusMessageService.showErrorMessage(errorSelectTwoColumns)
statusMessageService.showErrorById(StatusMessageId.ERROR_SELECT_TWO_COLUMNS)
return@TextButton
}
val langA = selectedLangFirst
val langB = selectedLangSecond
if (langA == null || langB == null) {
statusMessageService.showErrorMessage(errorSelectLanguages)
statusMessageService.showErrorById(StatusMessageId.ERROR_SELECT_LANGUAGES)
return@TextButton
}
val startIdx = if (skipHeader) 1 else 0
@@ -416,11 +412,11 @@ fun NewWordScreen(
)
}
if (items.isEmpty()) {
statusMessageService.showErrorMessage(errorNoRowsToImport)
statusMessageService.showErrorById(StatusMessageId.ERROR_NO_ROWS_TO_IMPORT)
return@TextButton
}
vocabularyViewModel.addVocabularyItems(items)
statusMessageService.showSuccessMessage(infoImportedItemsFrom + " " + items.size)
statusMessageService.showSuccessById(StatusMessageId.SUCCESS_ITEMS_IMPORTED)
showTableImportDialog.value = false
}) { Text(stringResource(R.string.label_import)) }
},
@@ -451,7 +447,7 @@ fun AIGeneratorCard(
val hints = stringArrayResource(R.array.vocabulary_hints)
AppCard(
modifier = modifier.fillMaxWidth(),
title = "AI Generator",
title = stringResource(R.string.label_ai_generator),
icon = icon,
hintContent = HintDefinition.VOCABULARY_GENERATE_AI.hint(),
) {
@@ -561,19 +557,13 @@ fun AddManuallyCard(
val selectedSourceLanguage by languageViewModel.selectedSourceLanguage.collectAsState()
val selectedTargetLanguage by languageViewModel.selectedTargetLanguage.collectAsState()
val languageLabel = when {
selectedSourceLanguage != null && selectedTargetLanguage != null ->
"${selectedSourceLanguage?.name}${selectedTargetLanguage?.name}"
else -> stringResource(R.string.text_select_languages)
}
val canAdd = wordText.isNotBlank() && translationText.isNotBlank() &&
selectedSourceLanguage != null && selectedTargetLanguage != null
AppCard(
modifier = modifier.fillMaxWidth(),
) {
Column(modifier = Modifier.padding(24.dp)) {
Column(modifier = Modifier.padding(24.dp)) {
// Header Row
Row(
modifier = Modifier.fillMaxWidth(),
@@ -709,9 +699,11 @@ fun BottomActionCardsRow(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
// Explore Packs Card
//TODO Explore Packs Card
AppCard(
modifier = Modifier.weight(1f).height(120.dp),
modifier = Modifier
.weight(1f)
.height(120.dp),
) {
Column(
modifier = Modifier
@@ -728,12 +720,13 @@ fun BottomActionCardsRow(
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.LibraryBooks,
imageVector = AppIcons.Vocabulary,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
}
Spacer(modifier = Modifier.height(12.dp))
@Suppress("HardCodedStringLiteral")
Text(
text = "Explore Packs",
style = MaterialTheme.typography.labelLarge,
@@ -741,6 +734,7 @@ fun BottomActionCardsRow(
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(6.dp))
@Suppress("HardCodedStringLiteral")
Text(
text = "Coming soon",
style = MaterialTheme.typography.labelSmall,
@@ -751,7 +745,9 @@ fun BottomActionCardsRow(
// Import CSV Card
AppCard(
modifier = Modifier.weight(1f).height(120.dp),
modifier = Modifier
.weight(1f)
.height(120.dp),
onClick = onImportCsvClick
) {
Column(
@@ -774,7 +770,7 @@ fun BottomActionCardsRow(
}
Spacer(modifier = Modifier.height(12.dp))
Text(
text = "Import CSV",
text = stringResource(R.string.label_import_csv),
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.Bold
)

View File

@@ -54,7 +54,7 @@ fun NoGrammarItemsScreen(
var showFetchGrammarDialog by remember { mutableStateOf(false) }
@Suppress("UnusedVariable", "unused", "HardCodedStringLiteral") val onClose = { navController.popBackStack() }
@Suppress("UnusedVariable") val onClose = { navController.popBackStack() }
if (itemsWithoutGrammar.isEmpty() && !isGenerating) {
Column(

View File

@@ -1,467 +0,0 @@
package eu.gaudian.translator.view.vocabulary
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.BorderStroke
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.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DividerDefaults
import androidx.compose.material3.HorizontalDivider
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.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import eu.gaudian.translator.R
import eu.gaudian.translator.model.CardSet
import eu.gaudian.translator.model.Language
import eu.gaudian.translator.model.VocabularyItem
import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.AppButton
import eu.gaudian.translator.view.composable.AppIcons
import eu.gaudian.translator.view.composable.AppOutlinedButton
import eu.gaudian.translator.view.composable.AppScaffold
import eu.gaudian.translator.view.composable.AppSlider
import eu.gaudian.translator.view.composable.AppTopAppBar
import eu.gaudian.translator.view.composable.OptionItemSwitch
import eu.gaudian.translator.view.composable.SingleLanguageDropDown
import eu.gaudian.translator.viewmodel.LanguageViewModel
import eu.gaudian.translator.viewmodel.VocabularyViewModel
@Composable
fun StartScreen(
cardSet: CardSet?,
onStartClicked: (List<VocabularyItem>) -> Unit,
onClose: () -> Unit,
shuffleCards: Boolean,
onShuffleCardsChanged: (Boolean) -> Unit,
shuffleLanguages: Boolean,
onShuffleLanguagesChanged: (Boolean) -> Unit,
trainingMode: Boolean,
onTrainingModeChanged: (Boolean) -> Unit,
dueTodayOnly: Boolean,
onDueTodayOnlyChanged: (Boolean) -> Unit,
selectedExerciseTypes: Set<VocabularyExerciseType>,
onExerciseTypeSelected: (VocabularyExerciseType) -> Unit,
hideTodayOnlySwitch: Boolean = false,
selectedOriginLanguage: Language?,
onOriginLanguageChanged: (Language?) -> Unit,
selectedTargetLanguage: Language?,
onTargetLanguageChanged: (Language?) -> Unit,
) {
val activity = LocalContext.current.findActivity()
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
val dueTodayItems by vocabularyViewModel.dueTodayItems.collectAsState(initial = emptyList())
val allItems = cardSet?.cards ?: emptyList()
var amount by remember(allItems) { mutableIntStateOf(allItems.size) }
val itemsToShow = if (dueTodayOnly) {
allItems.filter { card -> dueTodayItems.any { it.id == card.id } }
} else {
allItems
}
if (amount > itemsToShow.size) {
amount = itemsToShow.size
}
StartScreenContent(
vocabularyItemsCount = itemsToShow.size,
shuffleCards = shuffleCards,
onShuffleCardsChanged = onShuffleCardsChanged,
shuffleLanguages = shuffleLanguages,
onShuffleLanguagesChanged = onShuffleLanguagesChanged,
trainingMode = trainingMode,
onTrainingModeChanged = onTrainingModeChanged,
dueTodayOnly = dueTodayOnly,
onDueTodayOnlyChanged = onDueTodayOnlyChanged,
amount = amount,
onAmountChanged = {
@Suppress("AssignedValueIsNeverRead")
amount = it
},
onStartClicked = {
val finalItems = if (shuffleCards) {
itemsToShow.shuffled().take(amount)
} else {
itemsToShow.take(amount)
}
onStartClicked(finalItems)
},
onClose = onClose,
selectedExerciseTypes = selectedExerciseTypes,
onExerciseTypeSelected = onExerciseTypeSelected,
hideTodayOnlySwitch = hideTodayOnlySwitch,
selectedOriginLanguage = selectedOriginLanguage,
onOriginLanguageChanged = onOriginLanguageChanged,
selectedTargetLanguage = selectedTargetLanguage,
onTargetLanguageChanged = onTargetLanguageChanged,
allItems = allItems
)
}
@Composable
private fun StartScreenContent(
vocabularyItemsCount: Int,
shuffleCards: Boolean,
onShuffleCardsChanged: (Boolean) -> Unit,
shuffleLanguages: Boolean,
onShuffleLanguagesChanged: (Boolean) -> Unit,
trainingMode: Boolean,
onTrainingModeChanged: (Boolean) -> Unit,
dueTodayOnly: Boolean,
onDueTodayOnlyChanged: (Boolean) -> Unit,
amount: Int,
onAmountChanged: (Int) -> Unit,
onStartClicked: () -> Unit,
onClose: () -> Unit,
selectedExerciseTypes: Set<VocabularyExerciseType>,
onExerciseTypeSelected: (VocabularyExerciseType) -> Unit,
hideTodayOnlySwitch: Boolean = false,
selectedOriginLanguage: Language?,
onOriginLanguageChanged: (Language?) -> Unit,
selectedTargetLanguage: Language?,
onTargetLanguageChanged: (Language?) -> Unit,
allItems: List<VocabularyItem>,
) {
AppScaffold(
topBar = {
AppTopAppBar(
title = stringResource(R.string.prepare_exercise),
navigationIcon = {
IconButton(onClick = onClose) {
Icon(
AppIcons.Close,
contentDescription = stringResource(R.string.label_close)
)
}
}
)
},
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
Box(
modifier = Modifier.weight(1f)
) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(Modifier.height(16.dp))
if (vocabularyItemsCount > 0) {
Text(stringResource(R.string.number_of_cards, amount, vocabularyItemsCount))
AppSlider(
value = amount.toFloat(),
onValueChange = { onAmountChanged(it.toInt()) },
valueRange = 1f..vocabularyItemsCount.toFloat(),
steps = if (vocabularyItemsCount > 1) vocabularyItemsCount - 2 else 0
)
// Quick selection buttons
val quickSelectValues = listOf(10, 25, 50, 100)
val availableValues =
quickSelectValues.filter { it <= vocabularyItemsCount }
if (availableValues.isNotEmpty()) {
Spacer(Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(
8.dp,
Alignment.CenterHorizontally
)
) {
availableValues.forEach { value ->
AppOutlinedButton(
onClick = { onAmountChanged(value) },
modifier = Modifier.weight(1f),
enabled = value <= vocabularyItemsCount
) {
Text(value.toString())
}
}
}
}
} else {
Text(
stringResource(R.string.no_cards_found_for_the_selected_filters),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.error
)
}
HorizontalDivider(
modifier = Modifier.padding(vertical = 24.dp),
thickness = DividerDefaults.Thickness,
color = DividerDefaults.color
)
// Language Selection Section
Text(
stringResource(R.string.label_language_direction),
style = MaterialTheme.typography.titleLarge
)
Text(
stringResource(R.string.text_language_direction_explanation),
style = MaterialTheme.typography.bodyMedium
)
Spacer(Modifier.height(16.dp))
val activity = LocalContext.current.findActivity()
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
// Get available languages from the card set
val availableLanguages = remember(allItems) {
allItems.flatMap { listOfNotNull(it.languageFirstId, it.languageSecondId) }
.distinct()
.mapNotNull { languageId ->
languageViewModel.allLanguages.value.find { it.nameResId == languageId }
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
// Origin Language Dropdown
Column(modifier = Modifier.weight(1f)) {
Text(
stringResource(R.string.label_origin_language),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(bottom = 8.dp)
)
SingleLanguageDropDown(
modifier = Modifier.fillMaxWidth(),
languageViewModel = languageViewModel,
selectedLanguage = selectedOriginLanguage,
onLanguageSelected = { language ->
onOriginLanguageChanged(language)
// Clear target language if it's the same as origin
if (selectedTargetLanguage?.nameResId == language.nameResId) {
onTargetLanguageChanged(null)
}
},
showNoneOption = true,
alternateLanguages = availableLanguages
)
}
// Target Language Dropdown
Column(modifier = Modifier.weight(1f)) {
Text(
stringResource(R.string.label_target_language),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(bottom = 8.dp)
)
SingleLanguageDropDown(
modifier = Modifier.fillMaxWidth(),
languageViewModel = languageViewModel,
selectedLanguage = selectedTargetLanguage,
onLanguageSelected = { language ->
onTargetLanguageChanged(language)
// Clear origin language if it's the same as target
if (selectedOriginLanguage?.nameResId == language.nameResId) {
onOriginLanguageChanged(null)
}
},
alternateLanguages = availableLanguages,
showNoneOption = true,
)
}
}
Spacer(Modifier.height(16.dp))
HorizontalDivider(
modifier = Modifier.padding(vertical = 24.dp),
thickness = DividerDefaults.Thickness,
color = DividerDefaults.color
)
Text(
stringResource(R.string.label_choose_exercise_types),
style = MaterialTheme.typography.titleLarge
)
Spacer(Modifier.height(16.dp))
ExerciseTypeSelector(
selectedTypes = selectedExerciseTypes,
onTypeSelected = onExerciseTypeSelected
)
HorizontalDivider(
modifier = Modifier.padding(vertical = 24.dp),
thickness = DividerDefaults.Thickness,
color = DividerDefaults.color
)
Text(
stringResource(R.string.options),
style = MaterialTheme.typography.titleLarge
)
Spacer(Modifier.height(16.dp))
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
OptionItemSwitch(
title = stringResource(R.string.shuffle_cards),
description = stringResource(R.string.text_shuffle_card_order_description),
checked = shuffleCards,
onCheckedChange = onShuffleCardsChanged
)
OptionItemSwitch(
title = stringResource(R.string.text_shuffle_languages),
description = stringResource(R.string.text_shuffle_languages_description),
checked = shuffleLanguages,
onCheckedChange = onShuffleLanguagesChanged
)
OptionItemSwitch(
title = stringResource(R.string.label_training_mode),
description = stringResource(R.string.text_training_mode_description),
checked = trainingMode,
onCheckedChange = onTrainingModeChanged
)
if (!hideTodayOnlySwitch) {
OptionItemSwitch(
title = stringResource(R.string.text_due_today_only),
description = stringResource(R.string.text_due_today_only_description),
checked = dueTodayOnly,
onCheckedChange = onDueTodayOnlyChanged
)
}
}
Spacer(Modifier.height(16.dp))
}
}
AppButton(
onClick = onStartClicked,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.height(50.dp),
enabled = vocabularyItemsCount > 0 && amount > 0
) {
Text(stringResource(R.string.label_start_exercise_2d, amount))
}
}
}
}
@Composable
private fun ExerciseTypeSelector(
selectedTypes: Set<VocabularyExerciseType>,
onTypeSelected: (VocabularyExerciseType) -> Unit,
) {
// Using FlowRow for a more flexible layout that wraps to the next line if needed
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
ExerciseTypeCard(
icon = AppIcons.Guessing,
isSelected = VocabularyExerciseType.GUESSING in selectedTypes,
onClick = { onTypeSelected(VocabularyExerciseType.GUESSING) },
text = stringResource(R.string.label_guessing_exercise),
)
ExerciseTypeCard(
icon = AppIcons.SpellCheck,
isSelected = VocabularyExerciseType.SPELLING in selectedTypes,
onClick = { onTypeSelected(VocabularyExerciseType.SPELLING) },
text = stringResource(R.string.label_spelling_exercise),
)
ExerciseTypeCard(
icon = AppIcons.CheckList,
isSelected = VocabularyExerciseType.MULTIPLE_CHOICE in selectedTypes,
onClick = { onTypeSelected(VocabularyExerciseType.MULTIPLE_CHOICE) },
text = stringResource(R.string.label_multiple_choice_exercise),
)
ExerciseTypeCard(
icon = AppIcons.Extension,
isSelected = VocabularyExerciseType.WORD_JUMBLE in selectedTypes,
onClick = { onTypeSelected(VocabularyExerciseType.WORD_JUMBLE) },
text = stringResource(R.string.label_word_jumble_exercise),
)
}
}
@Composable
private fun ExerciseTypeCard(
text: String,
icon: ImageVector,
isSelected: Boolean,
onClick: () -> Unit,
) {
val borderColor by animateColorAsState(
targetValue = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline.copy(
alpha = 0.5f
),
label = "borderColorAnimation",
animationSpec = tween(300)
)
val containerColor by animateColorAsState(
targetValue = if (isSelected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant,
animationSpec = tween(300)
)
Card(
onClick = onClick,
modifier = Modifier.size(width = 120.dp, height = 100.dp), // Made the cards smaller
shape = RoundedCornerShape(12.dp),
border = BorderStroke(2.dp, borderColor),
colors = CardDefaults.cardColors(containerColor = containerColor),
elevation = CardDefaults.cardElevation(if (isSelected) 4.dp else 1.dp)
) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(icon, contentDescription = null, modifier = Modifier.size(32.dp)) // Smaller icon
Spacer(Modifier.height(8.dp))
Text(
text = text,
style = MaterialTheme.typography.bodyLarge, // Smaller text
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center
)
}
}
}

View File

@@ -51,7 +51,6 @@ import eu.gaudian.translator.view.dialogs.CategorySelectionDialog
import eu.gaudian.translator.view.dialogs.StageSelectionDialog
import eu.gaudian.translator.view.vocabulary.card.VocabularyDisplayCard
import eu.gaudian.translator.view.vocabulary.card.VocabularyExerciseCard
import eu.gaudian.translator.viewmodel.LanguageViewModel
import eu.gaudian.translator.viewmodel.VocabularyViewModel
import kotlinx.coroutines.launch
import kotlin.time.ExperimentalTime
@@ -68,7 +67,6 @@ fun VocabularyCardHost(
) {
val activity = LocalContext.current.findActivity()
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
val scope = rememberCoroutineScope()
val navigationItems by vocabularyViewModel.currentNavigationItems.collectAsState()
@@ -148,7 +146,6 @@ fun VocabularyCardHost(
var showStatisticsDialog by remember { mutableStateOf(false) }
var showCategoryDialog by remember { mutableStateOf(false) }
var showStageDialog by remember { mutableStateOf(false) }
var showImportDialog by remember { mutableStateOf(false) }
var showDeleteDialog by remember { mutableStateOf(false) }
LaunchedEffect(currentVocabularyItem.id) {
isEditing = false

View File

@@ -1,3 +1,5 @@
@file:Suppress("HardCodedStringLiteral")
package eu.gaudian.translator.view.vocabulary
import androidx.activity.compose.BackHandler

View File

@@ -284,7 +284,7 @@ private fun MonthGrid(
) {
Column(modifier = modifier) {
Row(modifier = Modifier.fillMaxWidth()) {
val locale = java.util.Locale.getDefault()
val locale = getDefault()
// Generate localized short weekday labels for Monday to Sunday.
val dayFormatter = remember(locale) {
DateTimeFormatter.ofPattern("EEEEE", locale)

View File

@@ -295,7 +295,6 @@ fun VocabularySortingItem(
val activity = LocalContext.current.findActivity()
val languageConfigViewModel: LanguageConfigViewModel = hiltViewModel(viewModelStoreOwner = activity)
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
var wordFirst by remember { mutableStateOf(item.wordFirst) }
var wordSecond by remember { mutableStateOf(item.wordSecond) }
var selectedCategories by remember { mutableStateOf<List<Int>>(emptyList()) }
@@ -310,7 +309,6 @@ fun VocabularySortingItem(
var articlesLangSecond by remember { mutableStateOf(emptySet<String>()) }
var showDuplicateDialog by remember { mutableStateOf(false) }
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
// NEW: Calculate if the item is valid for the "Done" button in faulty mode
val isItemNowValid by remember(wordFirst, wordSecond, langFirst, langSecond) {

View File

@@ -134,36 +134,6 @@ fun VocabularyExerciseCard(
)
}
@Deprecated("We need to seperate this into two: one for display and one for exercises")
@Composable
fun VocabularyCard(
vocabularyItem: VocabularyItem,
navController: NavController,
exerciseMode: Boolean,
isFlipped: Boolean,
switchOrder: Boolean,
onStatisticsClick: () -> Unit = {},
onMoveToCategoryClick: () -> Unit = {},
onMoveToStageClick: () -> Unit = {},
onDeleteClick: () -> Unit = {},
userSpellingAnswer: String? = null,
isUserSpellingCorrect: Boolean? = null,
) {
VocabularyCardContent(
vocabularyItem = vocabularyItem,
navController = navController,
isExerciseMode = exerciseMode,
isFlipped = isFlipped,
switchOrder = switchOrder,
onStatisticsClick = onStatisticsClick,
onMoveToCategoryClick = onMoveToCategoryClick,
onMoveToStageClick = onMoveToStageClick,
onDeleteClick = onDeleteClick,
userSpellingAnswer = userSpellingAnswer,
isUserSpellingCorrect = isUserSpellingCorrect,
)
}
@Composable
private fun VocabularyCardContent(
vocabularyItem: VocabularyItem,

View File

@@ -995,7 +995,7 @@ class DictionaryViewModel @Inject constructor(
* Returns true if data is still loading (null).
*/
fun getStructuredDictionaryDataLoading(entry: DictionaryWordEntry): StateFlow<Boolean> {
val key = entry.word + "_" + entry.langCode
entry.word + "_" + entry.langCode
// Create a derived flow that emits true when data is null
val dataFlow = getStructuredDictionaryDataState(entry)
val loadingFlow = MutableStateFlow(true)

View File

@@ -209,13 +209,6 @@ class ExerciseViewModel @Inject constructor(
}
}
fun startAdHocExercise(exercise: Exercise, questions: List<Question>) {
_exerciseSessionState.value = ExerciseSessionState(
exercise = exercise,
questions = questions
)
}
fun startExercise(exercise: Exercise) {
viewModelScope.launch {
val allQuestions = exerciseRepository.getAllQuestionsFlow().first()

View File

@@ -268,7 +268,7 @@ class ProgressViewModel @Inject constructor(
_dailyGoal.value = dailyGoalValue
// Get today's completed count
val today = kotlin.time.Clock.System.now().toLocalDateTime(kotlinx.datetime.TimeZone.currentSystemDefault()).date
val today = kotlin.time.Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date
val todayCompleted = vocabularyRepository.getCorrectAnswerCountForDate(today)
_todayCompletedCount.value = todayCompleted

View File

@@ -1,3 +1,5 @@
@file:Suppress("HardCodedStringLiteral")
package eu.gaudian.translator.viewmodel
import android.app.Application
@@ -397,11 +399,6 @@ class VocabularyExerciseViewModel @Inject constructor(
loadExercise()
}
fun onTrainingModeChanged(value: Boolean) {
Log.d("ExerciseVM", "onTrainingModeChanged: $value")
_trainingMode.value = value
}
fun startExerciseWithConfig(
items: List<VocabularyItem>,
config: ExerciseConfig

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -36,7 +36,6 @@
<string name="title_show_success_message">Erfolgsmeldung anzeigen</string>
<string name="label_add_category">Kategorie hinzufügen</string>
<string name="title_settings">Einstellungen</string>
<string name="title_dashboard">Dashboard</string>
<string name="title_developer_options">Entwickleroptionen</string>
<string name="title_multiple">Mehrere</string>
<string name="label_translation_settings">Übersetzung</string>
@@ -61,7 +60,6 @@
<string name="error_no_rows_to_import">Keine Zeilen zum Importieren. Bitte Spalten und Kopfzeile prüfen.</string>
<string name="info_imported_items_from">%1$d Vokabeln importiert.</string>
<string name="label_import">Importieren</string>
<string name="menu_import_vocabulary">Vokabular mit KI erstellen</string>
<string name="menu_create_youtube_exercise">YouTube-Übung erstellen</string>
<string name="text_youtube_link">YouTube-Link</string>
<string name="text_customize_the_intervals">Passe die Intervalle und Kriterien für das Verschieben von Vokabeln an. Karten in niedrigeren Stufen werden öfter abgefragt.</string>
@@ -94,10 +92,7 @@
<string name="text_loading_3d">Laden…</string>
<string name="text_show_loading">Laden anzeigen</string>
<string name="text_cancel_loading">Laden abbrechen</string>
<string name="text_sentence_this_is_an_info_message">Dies ist eine Info-Nachricht.</string>
<string name="text_show_info_message">Info-Nachricht anzeigen</string>
<string name="text_success_em">Erfolg!</string>
<string name="text_sentence_oops_something_went_wrong">Hoppla! Etwas ist schiefgegangen.</string>
<string name="text_show_error_message">Fehlermeldung anzeigen</string>
<string name="text_reset_intro">Intro zurücksetzen</string>
<string name="text_sentenc_version_information_not_available">Versionsinformation nicht verfügbar.</string>
@@ -127,7 +122,6 @@
<string name="text_enter_api_key">API-Schlüssel eingeben</string>
<string name="text_save_key">Schlüssel speichern</string>
<string name="text_select_model">Modell auswählen</string>
<string name="title_title_preview_title">Vorschau-Titel</string>
<string name="text_none">Keine</string>
<string name="text_manual_vocabulary_list">Manuelle Vokabelliste</string>
<string name="text_filter_all_items">Filter: Alle Einträge</string>
@@ -193,7 +187,6 @@
<string name="text_difficulty_2d">Schwierigkeit: %1$s</string>
<string name="text_amount_2d_questions">Anzahl: %1$d Fragen</string>
<string name="text_generate">Erstellen</string>
<string name="text_let_ai_find_vocabulary_for_you">Lass die KI Vokabeln für dich finden!</string>
<string name="text_search_term">Suchbegriff</string>
<string name="text_hint">Tipp</string>
<string name="text_select_languages">Sprachen auswählen</string>
@@ -225,8 +218,6 @@
<string name="cd_target_met">Ziel erreicht</string>
<string name="text_no_vocabulary_due_today">Heute keine Vokabeln fällig</string>
<string name="text_view_all">Alle ansehen</string>
<string name="text_custom_exercise">Eigene Übung</string>
<string name="text_daily_exercise">Tägliche Übung</string>
<string name="label_total_words">Wörter gesamt</string>
<string name="label_learned">Gelernt</string>
<string name="remaining">Übrig</string>
@@ -235,7 +226,6 @@
<string name="label_learning_criteria">Lernkriterien</string>
<string name="min_correct_to_advance">Min. richtig zum Aufsteigen</string>
<string name="max_wrong_to_demote">Max. falsch zum Absteigen</string>
<string name="daily_learning_goal">Tägliches Lernziel</string>
<string name="label_backup_and_restore">Sicherung &amp; Wiederherstellung</string>
<string name="export_vocabulary_data">Vokabeldaten exportieren</string>
<string name="import_vocabulary_data">Vokabeldaten importieren</string>
@@ -333,7 +323,6 @@
<string name="last_incorrect">Zuletzt falsch: %1$s</string>
<string name="correct_answers_">Richtige Antworten: %1$d</string>
<string name="incorrect_answers">Falsche Antworten: %1$d</string>
<string name="label_card_with_position">Karte (%1$d/%2$d)</string>
<string name="item_id">Eintrags-ID: %1$d</string>
<string name="statistics_are_loading">Statistiken werden geladen…</string>
<string name="to_d">nach %1$s</string>
@@ -364,7 +353,6 @@
<string name="more_actions">Mehr Aktionen</string>
<string name="select_all">Alle auswählen</string>
<string name="deselect_all">Auswahl aufheben</string>
<string name="search_vocabulary">Vokabular suchen…</string>
<string name="no_vocabulary_items_found_perhaps_try_changing_the_filters">Keine Vokabeln gefunden. Vielleicht die Filter ändern?</string>
<string name="label_category_2d">Kategorie: %1$s</string>
<string name="repository_state_imported_from">Repository-Status importiert von %1$s</string>
@@ -469,8 +457,6 @@
<string name="text_assign_these_items_2d">Ordne diese Elemente zu:</string>
<string name="translate_the_following_d">Übersetze Folgendes (%1$s):</string>
<string name="label_your_translation">Deine Übersetzung</string>
<string name="this_is_a_hint">Dies ist ein Hinweis.</string>
<string name="this_is_the_main_content">Dies ist der Hauptinhalt.</string>
<string name="this_is_the_content_inside_the_card">Dies ist der Inhalt in der Karte.</string>
<string name="primary_button">Primärer Button</string>
<string name="primary_with_icon">Primär mit Icon</string>
@@ -488,15 +474,12 @@
<string name="text_base_url_and_example">Basis-URL (z.B. \'http://192.168.0.99:1234/\')</string>
<string name="label_close_selection_mode">Auswahlmodus schließen</string>
<string name="d_selected">%1$d ausgewählt</string>
<string name="search_query">Suchanfrage</string>
<string name="label_close_search">Suche schließen</string>
<string name="generate_related_vocabulary_items">Verwandte Vokabeln generieren</string>
<string name="dismiss">Verwerfen</string>
<string name="edit_features_for">Merkmale für \'%1$s\' bearbeiten</string>
<string name="no_grammar_configuration_found_for_this_language">Keine Grammatikkonfiguration für diese Sprache gefunden.</string>
<string name="word_type">Wortart</string>
<string name="levels">Level</string>
<string name="quick_word_pairs">Schnelle Wortpaare</string>
<string name="stage_filter">Stufenfilter</string>
<string name="language_pair">Sprachpaar</string>
<string name="language_filter">Sprachfilter</string>
@@ -546,10 +529,6 @@
<string name="friendly">Freundlich</string>
<string name="label_academic">Akademisch</string>
<string name="creative">Kreativ</string>
<string name="editing_text">Text bearbeiten: %1$s</string>
<string name="no_text_received">Kein Text empfangen!</string>
<string name="error_no_text_to_edit">Fehler: Kein Text zum Bearbeiten</string>
<string name="not_launched_with_text_to_edit">Nicht mit zu bearbeitendem Text gestartet</string>
<string name="text_a_simple_list_to">Eine einfache Liste, um deine Vokabeln manuell zu sortieren</string>
<string name="settings_title_voice">Stimme</string>
<string name="default_value">Standard</string>
@@ -590,21 +569,11 @@
<string name="intro_if_you_need_help_you">Wenn du Hilfe brauchst, findest du Hinweise in allen Bereichen der App.</string>
<string name="text_navigation_bar_labels">Navigationsleisten-Beschriftungen</string>
<string name="text_show_text_labels_on_the_main_navigation_bar">Textbeschriftungen in der Hauptnavigationsleiste anzeigen.</string>
<string name="text_word_pair_settings">Wortpaar-Einstellungen</string>
<string name="text_amount_of_questions_2d">Anzahl der Fragen: %1$d</string>
<string name="text_shuffle_questions">Fragen mischen</string>
<string name="tetx_training_mode">Trainingsmodus</string>
<string name="text_match_the_pairs">Bilde die Paare</string>
<string name="text_word_pair_exercise">Wortpaar-Übung</string>
<string name="text_training_mode_description">Trainingsmodus ist aktiv: Antworten beeinflussen den Fortschritt nicht.</string>
<string name="text_days">" Tage"</string>
<string name="label_add_vocabulary">Vokabel hinzufügen</string>
<string name="label_create_vocabulary_with_ai">Vokabular mit KI erstellen</string>
<string name="text_vocab_empty">Keine Vokabeln gefunden. Jetzt hinzufügen?</string>
<string name="text_this_will_remove_all">Dadurch werden alle konfigurierten API-Anbieter, Modelle und gespeicherten API-Schlüssel entfernt. Diese Aktion kann nicht rückgängig gemacht werden.</string>
<string name="text_delete_all_providers_and_models_qm">Alle Anbieter und Modelle löschen?</string>
<string name="text_swap_sides">Seiten tauschen</string>
<string name="text_no_progress">Kein Fortschritt</string>
<string name="text_theme_preview">Theme-Vorschau</string>
<string name="text_sample_word">Beispielwort</string>
<string name="toggle_use_libretranslate">Übersetungs-Server verwenden</string>

View File

@@ -36,7 +36,6 @@
<string name="title_show_success_message">Mostrar Mensagem de Sucesso</string>
<string name="label_add_category">Adicionar Categoria</string>
<string name="title_settings">Configurações</string>
<string name="title_dashboard">Painel</string>
<string name="title_developer_options">Opções do Desenvolvedor</string>
<string name="title_multiple">Múltiplos</string>
<string name="label_translation_settings">Configurações de Tradução</string>
@@ -61,7 +60,6 @@
<string name="error_no_rows_to_import">Nenhuma linha para importar. Verifique as colunas e o cabeçalho.</string>
<string name="info_imported_items_from">%1$d itens de vocabulário importados.</string>
<string name="label_import">Importar</string>
<string name="menu_import_vocabulary">Gerar vocabulário com IA</string>
<string name="menu_create_youtube_exercise">Criar Exercício do YouTube</string>
<string name="text_youtube_link">Link do YouTube</string>
<string name="text_customize_the_intervals">Personalize os intervalos e critérios para mover os cartões de vocabulário. Cartões em estágios iniciais são perguntados com mais frequência.</string>
@@ -93,10 +91,7 @@
<string name="text_loading_3d">Carregando…</string>
<string name="text_show_loading">Mostrar Carregamento</string>
<string name="text_cancel_loading">Cancelar Carregamento</string>
<string name="text_sentence_this_is_an_info_message">Esta é uma mensagem informativa.</string>
<string name="text_show_info_message">Mostrar Mensagem Informativa</string>
<string name="text_success_em">Sucesso!</string>
<string name="text_sentence_oops_something_went_wrong">Oops! Algo deu errado.</string>
<string name="text_show_error_message">Mostrar Mensagem de Erro</string>
<string name="text_reset_intro">Resetar Introdução</string>
<string name="text_sentenc_version_information_not_available">Informação de versão não disponível.</string>
@@ -125,7 +120,6 @@
<string name="text_enter_api_key">Inserir Chave de API</string>
<string name="text_save_key">Salvar Chave</string>
<string name="text_select_model">Selecionar Modelo</string>
<string name="title_title_preview_title">Título de Prévia</string>
<string name="text_none">Nenhum</string>
<string name="text_manual_vocabulary_list">Lista de vocabulário manual</string>
<string name="text_filter_all_items">Filtro: Todos os itens</string>
@@ -191,7 +185,6 @@
<string name="text_difficulty_2d">Dificuldade: %1$s</string>
<string name="text_amount_2d_questions">Quantidade: %1$d Perguntas</string>
<string name="text_generate">Gerar</string>
<string name="text_let_ai_find_vocabulary_for_you">Deixe a IA encontrar vocabulário para você!</string>
<string name="text_search_term">Termo de Busca</string>
<string name="text_select_languages">Selecionar Idiomas</string>
<string name="text_select_amount">Selecionar Quantidade</string>
@@ -222,8 +215,6 @@
<string name="cd_target_met">Meta Atingida</string>
<string name="text_no_vocabulary_due_today">Nenhum Vocabulário para Hoje</string>
<string name="text_view_all">Ver Todos</string>
<string name="text_custom_exercise">Exercício Personalizado</string>
<string name="text_daily_exercise">Exercício Diário</string>
<string name="label_total_words">Total de Palavras</string>
<string name="label_learned">Aprendidas</string>
<string name="remaining">Restantes</string>
@@ -232,8 +223,7 @@
<string name="label_learning_criteria">Critérios de Aprendizagem</string>
<string name="min_correct_to_advance">Mín. de Acertos para Avançar</string>
<string name="max_wrong_to_demote">Máx. de Erros para Regredir</string>
<string name="daily_learning_goal">Meta de Aprendizagem Diária</string>
<string name="target_correct_answers_per_day">Meta de Respostas Corretas por Dia</string>
<string name="label_target_correct_answers_per_day">Meta de Respostas Corretas por Dia</string>
<string name="label_backup_and_restore">Backup e Restauração</string>
<string name="export_vocabulary_data">Exportar Dados do Vocabulário</string>
<string name="import_vocabulary_data">Importar Dados do Vocabulário</string>
@@ -332,7 +322,6 @@
<string name="last_incorrect">Último erro: %1$s</string>
<string name="correct_answers_">Respostas corretas: %1$d</string>
<string name="incorrect_answers">Respostas incorretas: %1$d</string>
<string name="label_card_with_position">Cartão (%1$d/%2$d)</string>
<string name="item_id">ID do Item: %1$d</string>
<string name="statistics_are_loading">Carregando estatísticas…</string>
<string name="to_d">para %1$s</string>
@@ -363,7 +352,6 @@
<string name="more_actions">Mais ações</string>
<string name="select_all">Selecionar Tudo</string>
<string name="deselect_all">Desmarcar Tudo</string>
<string name="search_vocabulary">Pesquisar vocabulário…</string>
<string name="no_vocabulary_items_found_perhaps_try_changing_the_filters">Nenhum item de vocabulário encontrado. Que tal tentar mudar os filtros?</string>
<string name="label_category_2d">Categoria: %1$s</string>
<string name="repository_state_imported_from">Estado do repositório importado de %1$s</string>
@@ -467,8 +455,6 @@
<string name="text_assign_these_items_2d">Associe estes itens:</string>
<string name="translate_the_following_d">Traduza o seguinte (%1$s):</string>
<string name="label_your_translation">Sua tradução</string>
<string name="this_is_a_hint">Esta é uma dica.</string>
<string name="this_is_the_main_content">Este é o conteúdo principal.</string>
<string name="this_is_the_content_inside_the_card">Este é o conteúdo dentro do cartão.</string>
<string name="primary_button">Botão Primário</string>
<string name="primary_with_icon">Primário com Ícone</string>
@@ -486,15 +472,12 @@
<string name="text_base_url_and_example">URL Base (ex: \'http://192.168.0.99:1234/\')</string>
<string name="label_close_selection_mode">Fechar modo de seleção</string>
<string name="d_selected">%1$d Selecionado(s)</string>
<string name="search_query">Termo de pesquisa</string>
<string name="label_close_search">Fechar pesquisa</string>
<string name="generate_related_vocabulary_items">Gerar itens de vocabulário relacionados</string>
<string name="dismiss">Dispensar</string>
<string name="edit_features_for">Editar Recursos para \'%1$s\'</string>
<string name="no_grammar_configuration_found_for_this_language">Nenhuma configuração de gramática encontrada para este idioma.</string>
<string name="word_type">Tipo de Palavra</string>
<string name="levels">Níveis</string>
<string name="quick_word_pairs">Pares de palavras rápidos</string>
<string name="stage_filter">Filtro de Estágio</string>
<string name="language_pair">Par de Idiomas</string>
<string name="language_filter">Filtro de Idioma</string>
@@ -544,10 +527,6 @@
<string name="friendly">Amigável</string>
<string name="label_academic">Acadêmico</string>
<string name="creative">Criativo</string>
<string name="editing_text">Editando Texto: %1$s</string>
<string name="no_text_received">Nenhum texto recebido!</string>
<string name="error_no_text_to_edit">Erro: Nenhum texto para editar</string>
<string name="not_launched_with_text_to_edit">Não iniciado com texto para editar</string>
<string name="text_a_simple_list_to">Uma lista simples para organizar o seu vocabulário manualmente</string>
<string name="settings_title_voice">Voz</string>
<string name="default_value">Padrão</string>
@@ -588,21 +567,11 @@
<string name="intro_if_you_need_help_you">Se precisar de ajuda, você pode encontrar dicas em todas as seções do aplicativo.</string>
<string name="text_navigation_bar_labels">Rótulos da Barra de Navegação</string>
<string name="text_show_text_labels_on_the_main_navigation_bar">Mostrar rótulos de texto na barra de navegação principal.</string>
<string name="text_word_pair_settings">Configurações de Pares de Palavras</string>
<string name="text_amount_of_questions_2d">Quantidade de perguntas: %1$d</string>
<string name="text_shuffle_questions">Embaralhar perguntas</string>
<string name="tetx_training_mode">Modo de treino</string>
<string name="text_match_the_pairs">Combine os pares</string>
<string name="text_word_pair_exercise">Exercício de Pares de Palavras</string>
<string name="text_training_mode_description">Modo de treino ativado: respostas não afetarão o progresso.</string>
<string name="text_days">" dias"</string>
<string name="label_add_vocabulary">Adicionar Vocabulário</string>
<string name="label_create_vocabulary_with_ai">Criar Vocabulário com IA</string>
<string name="text_vocab_empty">Nenhum item de vocabulário encontrado. Adicionar agora?</string>
<string name="text_this_will_remove_all">Isso removerá todos os provedores de API, modelos e chaves de API configurados. Esta ação não pode ser desfeita.</string>
<string name="text_delete_all_providers_and_models_qm">Excluir todos os provedores e modelos?</string>
<string name="text_swap_sides">Trocar lados</string>
<string name="text_no_progress">Sem progresso</string>
<string name="text_theme_preview">Prévia do Tema</string>
<string name="text_sample_word">Palavra de Exemplo</string>
<string name="toggle_use_libretranslate">Usar servidor de Tradução</string>

View File

@@ -57,8 +57,6 @@
<string name="d_selected">%1$d Selected</string>
<string name="d_the_quick_brown_fox_jumps_over_the_lazy_dog">%1$s: The quick brown fox jumps over the lazy dog.</string>
<string name="daily_learning_goal">Daily Learning Goal</string>
<string name="danger_zone">Danger Zone</string>
<string name="days_2d">%1$d days</string>
@@ -96,8 +94,6 @@
<string name="edit">Edit</string>
<string name="edit_features_for">Edit Features for \'%1$s\'</string>
<string name="editing_text">Editing Text: %1$s</string>
<string name="email_address">Email Address</string>
<string name="email_log">Email Log</string>
@@ -106,7 +102,6 @@
<string name="endpoint_e_g_api_chat">Endpoint (e.g., /v1/chat/completions/)</string>
<string name="error_no_rows_to_import">No rows to import. Please check the selected columns and header row.</string>
<string name="error_no_text_to_edit">Error: No text to edit</string>
<string name="error_parsing_table">Error parsing table</string>
<string name="error_parsing_table_with_reason">Error parsing table: %1$s</string>
<string name="error_select_languages">Please select two languages.</string>
@@ -160,8 +155,6 @@
<string name="general_settings">General Settings</string>
<string name="generate_related_vocabulary_items">Generate related vocabulary items</string>
<string name="get_started">Get Started</string>
<string name="got_it">Got it!</string>
@@ -225,13 +218,11 @@
<string name="label_amount_models">%1$d models</string>
<string name="label_analyze_grammar">Analyze Grammar</string>
<string name="label_appearance">Appearance</string>
<string name="hint_settings_title_help">Help</string>
<string name="label_apply_filters">Apply Filters</string>
<string name="label_article">Article</string>
<string name="label_backup_and_restore">Backup and Restore</string>
<string name="label_by_language">By Language</string>
<string name="label_cancel">Cancel</string>
<string name="label_card_with_position">Card (%1$d/%2$d)</string>
<string name="label_casual">Casual</string>
<string name="label_categories">Categories</string>
<string name="label_category">Category</string>
@@ -249,7 +240,6 @@
<string name="label_continue">Continue</string>
<string name="label_correct">Correct</string>
<string name="label_create_exercise">Create Exercise</string>
<string name="label_create_vocabulary_with_ai">Create Vocabulary with AI</string>
<string name="label_custom">Custom</string>
<string name="label_definitions">Definitions</string>
<string name="label_delete">Delete</string>
@@ -418,7 +408,6 @@
<string name="max_wrong_to_demote">Max Wrong to Demote</string>
<string name="menu_create_youtube_exercise">Create YouTube Exercise</string>
<string name="menu_import_vocabulary">Generate vocabulary with AI</string>
<string name="merge">Merge</string>
<string name="merge_items">Merge Items</string>
@@ -461,11 +450,9 @@
<string name="no_models_configured">No Models Configured</string>
<string name="no_models_found">No models found</string>
<string name="no_new_vocabulary_to_sort">No New Vocabulary to Sort</string>
<string name="no_text_received">No text received!</string>
<string name="no_vocabulary_items_found_perhaps_try_changing_the_filters">No vocabulary items found. Perhaps try changing the filters?</string>
<string name="not_available">Not available</string>
<string name="not_launched_with_text_to_edit">Not launched with text to edit</string>
<string name="number_of_cards">Number of Cards: %1$d / %2$d</string>
@@ -563,8 +550,6 @@
<string name="questions">%1$d questions</string>
<string name="quick_word_pairs">Quick word pairs</string>
<string name="quit">Quit</string>
<string name="refresh_word_of_the_day">Refresh Word of the Day</string>
@@ -591,8 +576,6 @@
<string name="search_for_a_word_s_origin">Search for a word\'s origin</string>
<string name="search_models">Search Models</string>
<string name="search_query">Search query</string>
<string name="search_vocabulary">Search vocabulary…</string>
<string name="secondary_button">Secondary Button</string>
<string name="secondary_inverse">Secondary Inverse</string>
@@ -673,12 +656,10 @@
<string name="tap_the_words_below_to_form_the_sentence">Tap the words below to form the sentence</string>
<string name="target_correct_answers_per_day">Target Correct Answers Per Day</string>
<string name="label_target_correct_answers_per_day">Target Correct Answers Per Day</string>
<string name="test">Test</string>
<string name="tetx_training_mode">Training mode</string>
<string name="text_200_ok">200 OK</string>
<string name="text_2d_categories_selected">%1$d categories selected</string>
<string name="text_2d_languages_selected">%1$d Languages Selected</string>
@@ -702,7 +683,6 @@
<string name="text_amount_2d">Amount: %1$d</string>
<string name="text_amount_2d_questions">Amount: %1$d Questions</string>
<string name="text_amount_of_cards">Amount of cards</string>
<string name="text_amount_of_questions_2d">Amount of questions: %1$d</string>
<string name="text_an_unexpected_condition_was_encountered_on_the_server">An unexpected condition was encountered on the server.</string>
<string name="text_an_unknown_error_occurred">An unknown error occurred.</string>
<string name="text_and_many_more">And many more! …</string>
@@ -741,9 +721,7 @@
<string name="text_copy_corrected_text">Copy corrected text</string>
<string name="text_correct_em">Correct!</string>
<string name="text_could_not_fetch_a_new_word">Could not fetch a new word.</string>
<string name="text_custom_exercise">Custom Exercise</string>
<string name="text_customize_the_intervals">Customize the intervals and criteria for moving vocabulary cards between stages. Cards in lower stages should be asked more often than those in higher stages.</string>
<string name="text_daily_exercise">Daily Exercise</string>
<string name="text_daily_goal_description">How many words do you want to answer correctly each day?</string>
<string name="text_dark">Dark</string>
<string name="text_day_streak">Day Streak</string>
@@ -827,12 +805,10 @@
<string name="text_language_direction_disabled_with_pairs">Clear language pair selection to choose a direction.</string>
<string name="text_language_options">Language Options</string>
<string name="text_last_7_days">Last 7 Days</string>
<string name="text_let_ai_find_vocabulary_for_you">Let AI find vocabulary for you!</string>
<string name="text_light">Light</string>
<string name="text_list">List</string>
<string name="text_loading_3d">Loading…</string>
<string name="text_manual_vocabulary_list">Manual vocabulary list</string>
<string name="text_match_the_pairs">Match the pairs</string>
<string name="text_mismatch_between_question_ids_in_exercise_and_questions_found_in_repository">Mismatch between question IDs in exercise and questions found in repository.</string>
<string name="text_mistral">Mistral</string>
<string name="text_more_options">More options</string>
@@ -846,7 +822,6 @@
<string name="text_no_items_available">No items available</string>
<string name="text_no_key">No Key</string>
<string name="text_no_models_found">No models found</string>
<string name="text_no_progress">No progress</string>
<string name="text_no_valid_api_configuration_could_be_found">No valid API configuration could be found in the settings. Before using this app, you have to configure at least one API provider.</string>
<string name="text_no_vocabulary_available">No vocabulary available.</string>
<string name="text_no_vocabulary_due_today">No Vocabulary Due Today</string>
@@ -891,8 +866,6 @@
<string name="text_select_translations_to_add">Select Translations to Add</string>
<string name="text_selected">Selected</string>
<string name="text_sentenc_version_information_not_available">Version information not available.</string>
<string name="text_sentence_oops_something_went_wrong">Oops! Something went wrong.</string>
<string name="text_sentence_this_is_an_info_message">This is an info message.</string>
<string name="text_show_error_message">Show Error Message</string>
<string name="text_show_info_message">Show Info Message</string>
<string name="text_show_loading">Show Loading</string>
@@ -902,12 +875,9 @@
<string name="text_shuffle_languages">Shuffle Languages</string>
<string name="text_shuffle_languages_description">Shuffle what language comes first. Does not affect language direction preferences.</string>
<string name="text_shuffle_languages_disabled_by_direction">Disable language direction preference to enable shuffling.</string>
<string name="text_shuffle_questions">Shuffle questions</string>
<string name="text_some_items_are_in_the_wrong_category">Some items are in the wrong category.</string>
<string name="text_stage_2d">Stage %1$s</string>
<string name="text_start_over">Start Over</string>
<string name="text_success_em">Success!</string>
<string name="text_swap_sides">Swap sides</string>
<string name="text_text">Text</string>
<string name="text_that_s_not_quite_right">That\'s not quite right.</string>
<string name="text_the_correct_answer_is_2d">The correct answer is:</string>
@@ -934,13 +904,10 @@
<string name="text_very_frequent">Very Frequent</string>
<string name="text_view_all">View All</string>
<string name="text_visit_my_website">Visit my website</string>
<string name="text_vocab_empty">No Vocabulary Items could be found. Add now?</string>
<string name="text_vocabulary_prompt">Vocabulary Prompt</string>
<string name="text_watch_video_again">Watch Video Again</string>
<string name="text_widget_title_weekly_activity">Weekly Activity</string>
<string name="text_word_of_the_day">Word of the Day</string>
<string name="text_word_pair_exercise">Word Pair Exercise</string>
<string name="text_word_pair_settings">Word Pair Settings</string>
<string name="text_your_own_ai">Your Own AI</string>
<string name="text_youtube_link">YouTube Link</string>
@@ -949,16 +916,13 @@
<string name="the_server_could_not_understand_the_request">The server could not understand the request.</string>
<string name="the_server_understood_the_request_but_is_refusing_to_authorize_it">The server understood the request, but is refusing to authorize it.</string>
<string name="this_is_a_hint">This is a hint.</string>
<string name="this_is_a_sample_output_text">This is a sample output text.</string>
<string name="this_is_the_content_inside_the_card">This is the content inside the card.</string>
<string name="this_is_the_main_content">This is the main content.</string>
<string name="this_mode_will_not_affect_your_progress_in_stages">This mode will not affect your progress in stages.</string>
<string name="timeout">Timeout</string>
<string name="title_corrector">Corrector</string>
<string name="title_dashboard">Dashboard</string>
<string name="title_developer_options">Developer Options</string>
<string name="title_http_status_codes">HTTP Status Codes</string>
<string name="title_items_without_grammar">Items Without Grammar</string>
@@ -966,7 +930,6 @@
<string name="title_settings">Settings</string>
<string name="title_show_success_message">Show Success Message</string>
<string name="title_single">Single</string>
<string name="title_title_preview_title">Preview Title</string>
<string name="title_widget_due_today">Due Today</string>
<string name="title_widget_streak">Streak</string>
@@ -990,7 +953,7 @@
<string name="vocabulary_added_successfully">Vocabulary Added</string>
<string name="vocabulary_repository">Vocabulary Repository</string>
<string name="vocabulary_settings">Progress Settings</string>
<string name="label_vocabulary_settings">Progress Settings</string>
<string name="website_url">Website URL</string>
@@ -1043,7 +1006,6 @@
<string name="hint_scan_hint_title">Finding the right AI model</string>
<string name="hint_translate_how_it_works">How translation works</string>
<string name="label_no_category">None</string>
<string name="text_select">Select</string>
<string name="text_search">Search</string>
<string name="text_language_settings_description">Set what languages you want to use in the app. Languages that are not activated will not appear in this app. You can also add your own language to the list, or change an existing language (region/locale)</string>
@@ -1120,7 +1082,6 @@
<string name="label_stats">Stats</string>
<string name="label_library">Library</string>
<string name="label_edit">Edit</string>
<string name="label_total_wordss">Total Words</string>
<string name="label_new_words">New Words</string>
<string name="desc_expand_your_vocabulary">Expand your vocabulary</string>
<string name="label_settings">Settings</string>
@@ -1131,11 +1092,16 @@
<string name="label_see_history">See History</string>
<string name="label_weekly_progress">Weekly Progress</string>
<string name="cd_go">Go</string>
<string name="label_aapply_filters">Apply Filters</string>
<string name="label_sort_by">Sort By</string>
<string name="label_reset">Reset</string>
<string name="label_filter_cards">Filter Cards</string>
<string name="text_desc_organize_vocabulary_groups">Organize Your Vocabulary in Groups</string>
<string name="text_add_new_word_to_list">Extract a New Word to Your List</string>
<string name="cd_scroll_to_top">Scroll to top</string>
<string name="cd_settings">Settings</string>
<string name="label_import_csv">Import CSV</string>
<string name="label_ai_generator">AI Generator</string>
<string name="label_new_wordss">New Words</string>
<string name="label_recently_added">Recently Added</string>
<string name="label_view_all">View All</string>
</resources>

View File

@@ -1,511 +0,0 @@
@file:Suppress("HardCodedStringLiteral")
package eu.gaudian.translator.utils
import android.content.Context
import eu.gaudian.translator.model.Language
import eu.gaudian.translator.model.communication.ApiManager
import eu.gaudian.translator.model.communication.ModelType
import eu.gaudian.translator.utils.dictionary.DictionaryService
import io.mockk.every
import io.mockk.mockk
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertFalse
import junit.framework.TestCase.assertNotNull
import junit.framework.TestCase.assertTrue
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
class JsonHelperTest {
private lateinit var jsonHelper: JsonHelper
@Before
fun setup() {
jsonHelper = JsonHelper()
}
@Test
fun `parseJson should successfully parse valid JSON`() = runTest {
// Given
val validJson = """{"word": "hello", "parts": [{"title": "Definition", "content": "A greeting"}]}"""
// When & Then
// This test would need proper serializer setup
// For now, just test the validation
assertTrue(jsonHelper.validateRequiredFields(validJson, listOf("word", "parts")))
}
@Test
fun `validateRequiredFields should return false for missing fields`() {
// Given
val incompleteJson = """{"word": "hello"}"""
val requiredFields = listOf("word", "parts")
// When
val result = jsonHelper.validateRequiredFields(incompleteJson, requiredFields)
// Then
assertFalse(result)
}
@Test
fun `validateRequiredFields should return true for complete fields`() {
// Given
val completeJson = """{"word": "hello", "parts": []}"""
val requiredFields = listOf("word", "parts")
// When
val result = jsonHelper.validateRequiredFields(completeJson, requiredFields)
// Then
assertTrue(result)
}
@Test
fun `extractField should return correct value`() {
// Given
val json = """{"word": "hello", "parts": []}"""
// When
val result = jsonHelper.extractField(json, "word")
// Then
assertEquals("hello", result)
}
@Test
fun `extractField should return null for missing field`() {
// Given
val json = """{"word": "hello"}"""
// When
val result = jsonHelper.extractField(json, "missing")
// Then
assertEquals(null, result)
}
}
class ApiRequestHandlerTest {
private lateinit var apiManager: ApiManager
private lateinit var context: Context
private lateinit var apiRequestHandler: ApiRequestHandler
@Before
fun setup() {
apiManager = mockk(relaxed = true)
context = mockk(relaxed = true)
apiRequestHandler = ApiRequestHandler(apiManager, context)
}
@Test
fun `executeRequest with template should handle successful response`() = runTest {
// Given
val template = DictionaryDefinitionRequest(
word = "test",
language = "English",
requestedParts = "Definition"
)
// Mock the API manager response
every {
apiManager.getCompletion(
prompt = any(),
callback = any(),
modelType = ModelType.DICTIONARY
)
} answers {
val callback = thirdArg<(String?) -> Unit>()
callback("""{"word": "test", "parts": [{"title": "Definition", "content": "A test"}]}""")
}
// When
val result = apiRequestHandler.executeRequest(template)
// Then
assertTrue(result.isSuccess)
result.getOrNull()?.let { response ->
assertEquals("test", response.word)
assertEquals(1, response.parts.size)
}
}
@Test
fun `executeRequest with template should handle API failure`() = runTest {
// Given
val template = DictionaryDefinitionRequest(
word = "test",
language = "English",
requestedParts = "Definition"
)
every {
apiManager.getCompletion(
prompt = any(),
callback = any(),
modelType = ModelType.DICTIONARY
)
} answers {
val callback = secondArg<(String) -> Unit>()
callback("API Error")
}
// When
val result = apiRequestHandler.executeRequest(template)
// Then
assertTrue(result.isFailure)
}
}
class ApiRequestTemplatesTest {
@Test
fun `DictionaryDefinitionRequest should build correct prompt`() {
// Given
val template = DictionaryDefinitionRequest(
word = "hello",
language = "English",
requestedParts = "Definition, Origin"
)
// When
val prompt = template.buildPrompt()
// Then
assertTrue(prompt.contains("hello"))
assertTrue(prompt.contains("English"))
assertTrue(prompt.contains("Definition, Origin"))
assertTrue(prompt.contains("JSON object"))
}
@Test
fun `VocabularyTranslationRequest should build correct prompt`() {
// Given
val words = listOf("hello", "world")
val template = VocabularyTranslationRequest(
words = words,
languageFirst = "English",
languageSecond = "Spanish"
)
// When
val prompt = template.buildPrompt()
// Then
assertTrue(prompt.contains("English"))
assertTrue(prompt.contains("Spanish"))
assertTrue(prompt.contains("hello"))
assertTrue(prompt.contains("world"))
assertTrue(prompt.contains("flashcards"))
}
@Test
fun `TextCorrectionRequest should build correct prompt`() {
// Given
val template = TextCorrectionRequest(
textToCorrect = "Helo world",
language = "English",
grammarOnly = true,
tone = null
)
// When
val prompt = template.buildPrompt()
// Then
assertTrue(prompt.contains("Helo world"))
assertTrue(prompt.contains("English"))
assertTrue(prompt.contains("grammar, spelling, and punctuation"))
assertTrue(prompt.contains("correctedText"))
assertTrue(prompt.contains("explanation"))
}
@Test
fun `SynonymGenerationRequest should build correct prompt`() {
// Given
val template = SynonymGenerationRequest(
amount = 5,
language = "English",
term = "happy",
translation = "feliz",
translationLanguage = "Spanish",
languageCode = "en"
)
// When
val prompt = template.buildPrompt()
// Then
assertTrue(prompt.contains("happy"))
assertTrue(prompt.contains("feliz"))
assertTrue(prompt.contains("English"))
assertTrue(prompt.contains("Spanish"))
assertTrue(prompt.contains("synonyms"))
assertTrue(prompt.contains("proximity"))
}
}
class DictionaryServiceTest {
private lateinit var context: Context
private lateinit var dictionaryService: DictionaryService
@Before
fun setup() {
context = mockk(relaxed = true)
dictionaryService = DictionaryService(context)
}
@Test
fun `searchDefinition should handle successful response`() = runTest {
// This test would require mocking the ApiRequestHandler
// For now, just verify the method exists and basic structure
val language = Language(
name = "English",
nameResId = 1,
code = "en",
englishName = "English",
region = ""
)
// When & Then
// This would need proper mocking setup
assertNotNull(language)
assertEquals("English", language.name)
}
@Test
fun `getExampleSentence should handle successful response`() = runTest {
val languageFirst = Language(
name = "English",
nameResId = 1,
code = "en",
englishName = "English",
region = ""
)
val languageSecond = Language(
name = "Spanish",
nameResId = 2,
code = "es",
englishName = "Spanish",
region = ""
)
// When & Then
assertNotNull(languageFirst)
assertNotNull(languageSecond)
}
}
class VocabularyServiceTest {
private lateinit var context: Context
private lateinit var vocabularyService: VocabularyService
@Before
fun setup() {
context = mockk(relaxed = true)
vocabularyService = VocabularyService(context)
}
@Test
fun `translateWordsBatch should handle empty list`() = runTest {
// Given
val emptyWords = emptyList<String>()
val languageFirst = Language(
name = "English",
nameResId = 1,
code = "en",
englishName = "English",
region = ""
)
val languageSecond = Language(
name = "Spanish",
nameResId = 2,
code = "es",
englishName = "Spanish",
region = ""
)
// When
val result = vocabularyService.translateWordsBatch(emptyWords, languageFirst, languageSecond)
// Then
assertTrue(result.isSuccess)
assertEquals(0, result.getOrNull()?.size)
}
@Test
fun `translateWordsBatch should filter blank words`() = runTest {
// Given
val wordsWithBlanks = listOf("hello", "", "world", " ")
// When & Then
// This would need proper mocking setup for actual API calls
assertEquals(2, wordsWithBlanks.filter { it.isNotBlank() }.size)
}
@Test
fun `generateSynonyms should use correct parameters`() = runTest {
// Given
val language = Language(
name = "English",
nameResId = 1,
code = "en",
englishName = "English",
region = ""
)
val translationLanguage = Language(
name = "Spanish",
nameResId = 2,
code = "es",
englishName = "Spanish",
region = ""
)
// When & Then
// This would need proper mocking setup for actual API calls
assertNotNull(language)
assertNotNull(translationLanguage)
assertEquals("English", language.englishName)
assertEquals("Spanish", translationLanguage.englishName)
}
}
class TranslationServiceTest {
private lateinit var context: Context
private lateinit var translationService: TranslationService
@Before
fun setup() {
context = mockk(relaxed = true)
translationService = TranslationService(context)
}
@Test
fun `simpleTranslateTo should handle basic translation`() = runTest {
// Given
val targetLanguage = Language(
name = "Spanish",
nameResId = 1,
code = "es",
englishName = "Spanish",
region = ""
)
// When & Then
// This would need proper mocking setup for actual API calls
assertNotNull(targetLanguage)
assertEquals("Spanish", targetLanguage.name)
}
@Test
fun `translateSentence should handle sentence translation`() = runTest {
// Given
val sentence = "Hello world"
// When & Then
// This would need proper mocking setup for actual API calls
assertNotNull(sentence)
assertEquals("Hello world", sentence)
}
}
class CorrectionServiceTest {
private lateinit var context: Context
private lateinit var correctionService: CorrectionService
@Before
fun setup() {
context = mockk(relaxed = true)
correctionService = CorrectionService(context)
}
@Test
fun `correctText should handle basic correction`() = runTest {
// Given
val language = Language(
name = "English",
nameResId = 1,
code = "en",
englishName = "English",
region = ""
)
// When & Then
// This would need proper mocking setup for actual API calls
assertNotNull(language)
assertEquals("English", language.name)
}
@Test
fun `correctText should handle grammar only mode`() = runTest {
// Given
val textToCorrect = "Helo world"
val language = Language(
name = "English",
nameResId = 1,
code = "en",
englishName = "English",
region = ""
)
// When & Then
// This would need proper mocking setup for actual API calls
assertNotNull(textToCorrect)
assertNotNull(language)
assertEquals("Helo world", textToCorrect)
}
}
// Integration test example
class ApiArchitectureIntegrationTest {
@Test
fun `end to end dictionary lookup should work`() = runTest {
// This would be an integration test that tests the full flow
// from service -> template -> API handler -> JSON parsing
// Given
val mockContext = mockk<Context>(relaxed = true)
val mockApiManager = mockk<ApiManager>(relaxed = true)
// Setup mock API response
every {
mockApiManager.getCompletion(
prompt = any(),
callback = any(),
modelType = ModelType.DICTIONARY
)
} answers {
val callback = thirdArg<(String?) -> Unit>()
callback("""{"word": "test", "parts": [{"title": "Definition", "content": "A test word"}]}""")
}
// When
val apiHandler = ApiRequestHandler(mockApiManager, mockContext)
val template = DictionaryDefinitionRequest(
word = "test",
language = "English",
requestedParts = "Definition"
)
val result = apiHandler.executeRequest(template)
// Then
assertTrue(result.isSuccess)
result.getOrNull()?.let { response ->
assertEquals("test", response.word)
assertEquals(1, response.parts.size)
assertEquals("Definition", response.parts[0].title)
}
}
}

View File

@@ -1,36 +0,0 @@
package eu.gaudian.translator.utils
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import org.junit.rules.TestWatcher
import org.junit.runner.Description
/**
* Test rule for setting up coroutine testing environment
*/
class CoroutineTestRule @OptIn(ExperimentalCoroutinesApi::class) constructor(
val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()
) : TestWatcher() {
lateinit var testScope: TestScope
private set
override fun starting(description: Description) {
super.starting(description)
testScope = TestScope(testDispatcher)
}
}
/**
* Base test class for API architecture tests
*/
abstract class BaseApiTest {
@get:org.junit.Rule
val coroutineRule = CoroutineTestRule()
protected val testDispatcher = coroutineRule.testDispatcher
protected val testScope = coroutineRule.testScope
}

View File

@@ -1,323 +0,0 @@
@file:Suppress("HardCodedStringLiteral")
package eu.gaudian.translator.utils
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertFalse
import junit.framework.TestCase.assertNotNull
import junit.framework.TestCase.assertTrue
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.junit.Before
import org.junit.Test
class JsonHelperComprehensiveTest {
private lateinit var jsonHelper: JsonHelper
private val jsonParser = Json { ignoreUnknownKeys = true; isLenient = true }
@Before
fun setup() {
jsonHelper = JsonHelper()
}
@Test
fun `parseJson should handle valid DictionaryApiResponse`() {
// Given
val validJson = """{"word": "hello", "parts": [{"title": "Definition", "content": "A greeting"}]}"""
// When
val result = jsonHelper.validateRequiredFields(validJson, listOf("word", "parts"))
// Then
assertTrue(result)
}
@Test
fun `parseJson should handle valid VocabularyApiResponse`() {
// Given
val validJson = """{"flashcards": [{"front": {"language": "English", "word": "hello"}, "back": {"language": "Spanish", "word": "hola"}}]}"""
// When
val result = jsonHelper.validateRequiredFields(validJson, listOf("flashcards"))
// Then
assertTrue(result)
}
@Test
fun `parseJson should handle valid TranslationApiResponse`() {
// Given
val validJson = """{"translatedText": "hola"}"""
// When
val result = jsonHelper.validateRequiredFields(validJson, listOf("translatedText"))
// Then
assertTrue(result)
}
@Test
fun `parseJson should handle valid CorrectionResponse`() {
// Given
val validJson = """{"correctedText": "Hello world", "explanation": "Capitalized first letter"}"""
// When
val result = jsonHelper.validateRequiredFields(validJson, listOf("correctedText", "explanation"))
// Then
assertTrue(result)
}
@Test
fun `parseJson should handle valid EtymologyApiResponse`() {
// Given
val validJson = """{"word": "hello", "timeline": [{"year": "1890", "language": "Old English", "description": "From greeting"}], "relatedWords": []}"""
// When
val result = jsonHelper.validateRequiredFields(validJson, listOf("word", "timeline", "relatedWords"))
// Then
assertTrue(result)
}
@Test
fun `parseJson should handle valid SynonymApiResponse`() {
// Given
val validJson = """{"synonyms": [{"word": "hi", "proximity": 95}, {"word": "greetings", "proximity": 85}]}"""
// When
val result = jsonHelper.validateRequiredFields(validJson, listOf("synonyms"))
// Then
assertTrue(result)
}
@Test
fun `validateRequiredFields should return false for empty JSON`() {
// Given
val emptyJson = """{}"""
val requiredFields = listOf("word")
// When
val result = jsonHelper.validateRequiredFields(emptyJson, requiredFields)
// Then
assertFalse(result)
}
@Test
fun `validateRequiredFields should return false for malformed JSON`() {
// Given
val malformedJson = """{"word": "hello", "parts": ["""
val requiredFields = listOf("word", "parts")
// When
val result = jsonHelper.validateRequiredFields(malformedJson, requiredFields)
// Then
assertFalse(result)
}
@Test
fun `validateRequiredFields should handle nested objects`() {
// Given
val nestedJson = """{"flashcards": [{"front": {"language": "English", "word": "hello"}]}"""
val requiredFields = listOf("flashcards")
// When
val result = jsonHelper.validateRequiredFields(nestedJson, requiredFields)
// Then
assertTrue(result)
}
@Test
fun `extractField should extract simple field`() {
// Given
val json = """{"word": "hello", "parts": []}"""
// When
val result = jsonHelper.extractField(json, "word")
// Then
assertEquals("hello", result)
}
@Test
fun `extractField should extract nested field`() {
// Given
val json = """{"flashcards": [{"front": {"language": "English", "word": "hello"}}]}"""
// When
val result = jsonHelper.extractField(json, "flashcards")
// Then
assertNotNull(result)
assertTrue(result!!.contains("English"))
assertTrue(result.contains("hello"))
}
@Test
fun `extractField should return null for non-existent field`() {
// Given
val json = """{"word": "hello"}"""
// When
val result = jsonHelper.extractField(json, "nonexistent")
// Then
assertEquals(null, result)
}
@Test
fun `extractField should handle array fields`() {
// Given
val json = """{"synonyms": [{"word": "hi", "proximity": 95}]}"""
// When
val result = jsonHelper.extractField(json, "synonyms")
// Then
assertNotNull(result)
assertTrue(result!!.contains("hi"))
assertTrue(result.contains("95"))
}
@Test
fun `formatForDisplay should format simple JSON`() {
// Given
val json = """{"word": "hello", "parts": []}"""
// When
val result = jsonHelper.formatForDisplay(json)
// Then
assertTrue(result.contains("{\n"))
assertTrue(result.contains("}\n"))
assertTrue(result.contains(",\n"))
}
@Test
fun `formatForDisplay should handle malformed JSON gracefully`() {
// Given
val malformedJson = """{"word": "hello", "parts": ["""
// When
val result = jsonHelper.formatForDisplay(malformedJson)
// Then
assertEquals(malformedJson, result) // Should return original if formatting fails
}
@Test
fun `cleanAndValidateJson should handle markdown wrapped JSON`() {
// Given
val markdownJson = """
```json
{"word": "hello", "parts": []}
```
""".trim()
// When
val result = jsonHelper.validateRequiredFields(markdownJson, listOf("word", "parts"))
// Then
assertTrue(result)
}
@Test
fun `cleanAndValidateJson should handle JSON with comments`() {
// Given
val jsonWithComments = """
{
"word": "hello", // This is the word
"parts": [] /* This is the parts array */
}
""".trim()
// When
val result = jsonHelper.validateRequiredFields(jsonWithComments, listOf("word", "parts"))
// Then
assertTrue(result)
}
@Test
fun `cleanAndValidateJson should handle JSON with trailing commas`() {
// Given
val jsonWithTrailingComma = """
{
"word": "hello",
"parts": [],
}
""".trim()
// When
val result = jsonHelper.validateRequiredFields(jsonWithTrailingComma, listOf("word", "parts"))
// Then
assertTrue(result)
}
// Test data classes
@Serializable
data class TestDictionaryResponse(
val word: String,
val parts: List<TestEntryPart>
)
@Serializable
data class TestEntryPart(
val title: String,
val content: String
)
@Serializable
data class TestVocabularyResponse(
val flashcards: List<TestFlashcard>
)
@Serializable
data class TestFlashcard(
val front: TestCardSide,
val back: TestCardSide
)
@Serializable
data class TestCardSide(
val language: String,
val word: String
)
@Test
fun `real parsing test with DictionaryApiResponse`() {
// Given
val json = """{"word": "hello", "parts": [{"title": "Definition", "content": "A greeting"}]}"""
// When
val result = jsonParser.decodeFromString(TestDictionaryResponse.serializer(), json)
// Then
assertEquals("hello", result.word)
assertEquals(1, result.parts.size)
assertEquals("Definition", result.parts[0].title)
assertEquals("A greeting", result.parts[0].content)
}
@Test
fun `real parsing test with VocabularyApiResponse`() {
// Given
val json = """{"flashcards": [{"front": {"language": "English", "word": "hello"}, "back": {"language": "Spanish", "word": "hola"}}]}"""
// When
val result = jsonParser.decodeFromString(TestVocabularyResponse.serializer(), json)
// Then
assertEquals(1, result.flashcards.size)
assertEquals("English", result.flashcards[0].front.language)
assertEquals("hello", result.flashcards[0].front.word)
assertEquals("Spanish", result.flashcards[0].back.language)
assertEquals("hola", result.flashcards[0].back.word)
}
}

View File

@@ -1,203 +0,0 @@
@file:Suppress("HardCodedStringLiteral")
package eu.gaudian.translator.utils
import android.content.Context
import eu.gaudian.translator.model.Language
import eu.gaudian.translator.model.VocabularyItem
import io.mockk.mockk
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNotNull
import junit.framework.TestCase.assertTrue
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import org.junit.Before
import org.junit.Test
class ServiceFixesTest {
private lateinit var context: Context
private lateinit var vocabularyService: VocabularyService
private lateinit var exerciseService: ExerciseService
@Before
fun setup() {
context = mockk(relaxed = true)
vocabularyService = VocabularyService(context)
exerciseService = ExerciseService(context)
}
@Test
fun `VocabularyService generateSynonyms should have correct return type`() = runTest {
// Given
val language = Language(
name = "English",
nameResId = 1,
code = "en",
englishName = "English",
region = ""
)
val translationLanguage = Language(
name = "Spanish",
nameResId = 2,
code = "es",
englishName = "Spanish",
region = ""
)
// When & Then
// This test verifies the method signature is correct
// The actual API call would need mocking for full testing
assertNotNull(language)
assertNotNull(translationLanguage)
assertEquals("English", language.englishName)
assertEquals("Spanish", translationLanguage.englishName)
}
@Test
fun `ExerciseService should use JsonHelper instead of VocabularyParser`() = runTest {
// Given
val exerciseTitle = "Test Exercise"
// When & Then
// This test verifies the ExerciseService can be instantiated without errors
assertNotNull(exerciseService)
assertNotNull(exerciseTitle)
assertEquals("Test Exercise", exerciseTitle)
}
@Test
fun `parseVocabularyFromJson should handle simple JSON`() {
// Given
val jsonResponse = """{"hello": "hola", "world": "mundo"}"""
// When
val result = parseVocabularyFromJson(jsonResponse)
// Then
assertEquals(2, result.size)
assertEquals("hello", result[0].wordFirst)
assertEquals("hola", result[0].wordSecond)
assertEquals("world", result[1].wordFirst)
assertEquals("mundo", result[1].wordSecond)
}
@Test
fun `parseVocabularyFromJson should handle empty JSON`() {
// Given
val jsonResponse = """{}"""
// When
val result = parseVocabularyFromJson(jsonResponse)
// Then
assertEquals(0, result.size)
}
@Test
fun `parseVocabularyFromJson should handle malformed JSON`() {
// Given
val malformedJson = """{"hello": "hola", "world": """
// When
val result = parseVocabularyFromJson(malformedJson)
// Then
assertEquals(0, result.size)
}
/**
* Helper method to test the private parseVocabularyFromJson method
*/
private fun parseVocabularyFromJson(jsonResponse: String): List<VocabularyItem> {
return try {
// Clean and validate the JSON first
val jsonHelper = JsonHelper()
val cleanedJson = jsonHelper.cleanAndValidateJson(jsonResponse)
// Parse the JSON object for vocabulary items
val jsonObject = kotlinx.serialization.json.Json.parseToJsonElement(cleanedJson).jsonObject
val vocabularyItems = mutableListOf<VocabularyItem>()
var id = 1
for ((wordFirst, wordSecondElement) in jsonObject) {
val wordSecond = wordSecondElement.jsonPrimitive.content.trim()
val vocabularyItem = VocabularyItem(
id = id++,
languageFirstId = -1, // Will be set by caller
languageSecondId = -1, // Will be set by caller
wordFirst = wordFirst.trim(),
wordSecond = wordSecond
)
vocabularyItems.add(vocabularyItem)
}
vocabularyItems
} catch (e: Exception) {
emptyList()
}
}
@Test
fun `VocabularyService translateWordsBatch should handle empty list`() = runTest {
// Given
val emptyWords = emptyList<String>()
val languageFirst = Language(
name = "English",
nameResId = 1,
code = "en",
englishName = "English",
region = ""
)
val languageSecond = Language(
name = "Spanish",
nameResId = 2,
code = "es",
englishName = "Spanish",
region = ""
)
// When
val result = vocabularyService.translateWordsBatch(emptyWords, languageFirst, languageSecond)
// Then
assertTrue(result.isSuccess)
assertEquals(0, result.getOrNull()?.size)
}
@Test
fun `VocabularyService generateVocabularyItems should handle basic parameters`() = runTest {
// Given
val category = "Basic"
val languageFirst = Language(
name = "English",
nameResId = 1,
code = "en",
englishName = "English",
region = ""
)
val languageSecond = Language(
name = "Spanish",
nameResId = 2,
code = "es",
englishName = "Spanish",
region = ""
)
val amount = 5
// When & Then
// This test verifies the method exists and accepts parameters correctly
assertNotNull(category)
assertNotNull(languageFirst)
assertNotNull(languageSecond)
assertEquals("Basic", category)
assertEquals("English", languageFirst.name)
assertEquals("Spanish", languageSecond.name)
assertEquals(5, amount)
}
}