From f39375e9dfb76af387d3a0e9a2317fa7c26a01ca Mon Sep 17 00:00:00 2001 From: jonasgaudian <43753916+jonasgaudian@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:45:23 +0100 Subject: [PATCH] Refactor navigation and cleanup resources across the application --- app/build.gradle.kts | 1 - .../java/eu/gaudian/translator/TestConfig.kt | 7 +- .../gaudian/translator/di/RepositoryModule.kt | 2 - .../LocalDictionaryMorphologyMapper.kt | 1 + .../model/repository/ApiRepository.kt | 13 - .../model/repository/LanguageRepository.kt | 1 + .../eu/gaudian/translator/utils/JsonHelper.kt | 19 - .../java/eu/gaudian/translator/utils/Log.kt | 1 + .../translator/utils/StatusMessageIds.kt | 6 + .../translator/utils/StatusMessageService.kt | 1 - .../translator/utils/TranslationService.kt | 1 - .../utils/dictionary/InflectionParser.kt | 11 - .../utils/dictionary/LocalDictionaryParser.kt | 5 - .../eu/gaudian/translator/view/Navigation.kt | 2 +- .../translator/view/composable/AppScaffold.kt | 45 -- .../view/composable/AppTabLayout.kt | 17 +- .../view/composable/BottomNavigationBar.kt | 2 +- .../view/dialogs/CategorySelectionDialog.kt | 4 - .../view/dialogs/StartExerciseDialog.kt | 132 ----- .../view/dialogs/VocabularyReviewScreen.kt | 7 +- .../dictionary/DictionaryResultComponents.kt | 8 +- .../dictionary/DictionaryTableComponents.kt | 2 + .../view/exercises/StartExerciseScreen.kt | 35 +- .../gaudian/translator/view/hints/AllHints.kt | 1 - .../eu/gaudian/translator/view/hints/Hint.kt | 2 - .../view/hints/MarkdownHintLoader.kt | 1 + .../view/settings/LanguageOptionsScreen.kt | 1 + .../view/settings/LayoutOptionsScreen.kt | 1 - .../VocabularyProgressOptionsScreen.kt | 4 +- .../view/vocabulary/DashboardContent.kt | 1 - .../view/vocabulary/NewWordReviewScreen.kt | 1 + .../view/vocabulary/NewWordScreen.kt | 78 ++- .../view/vocabulary/NoGrammarItemsScreen.kt | 2 +- .../translator/view/vocabulary/StartScreen.kt | 467 ---------------- .../view/vocabulary/VocabularyCardHost.kt | 3 - .../VocabularyExerciseHostScreen.kt | 2 + .../vocabulary/VocabularyHeatMapScreen.kt | 2 +- .../vocabulary/VocabularySortingScreen.kt | 2 - .../view/vocabulary/card/VocabularyCard.kt | 30 - .../viewmodel/DictionaryViewModel.kt | 2 +- .../translator/viewmodel/ExerciseViewModel.kt | 7 - .../translator/viewmodel/ProgressViewModel.kt | 2 +- .../viewmodel/VocabularyExerciseViewModel.kt | 7 +- app/src/main/res/drawable/ic_empty.png | Bin 39146 -> 0 bytes app/src/main/res/values-de-rDE/strings.xml | 31 -- app/src/main/res/values-pt-rBR/strings.xml | 33 +- app/src/main/res/values/strings.xml | 50 +- .../translator/utils/ApiArchitectureTest.kt | 511 ------------------ .../eu/gaudian/translator/utils/BaseTest.kt | 36 -- .../translator/utils/JsonHelperTest.kt | 323 ----------- .../translator/utils/ServiceFixesTest.kt | 203 ------- gradle/libs.versions.toml | 16 +- settings.gradle.kts | 2 + 53 files changed, 112 insertions(+), 2032 deletions(-) delete mode 100644 app/src/main/java/eu/gaudian/translator/utils/dictionary/InflectionParser.kt delete mode 100644 app/src/main/java/eu/gaudian/translator/view/dialogs/StartExerciseDialog.kt delete mode 100644 app/src/main/java/eu/gaudian/translator/view/vocabulary/StartScreen.kt delete mode 100644 app/src/main/res/drawable/ic_empty.png delete mode 100644 app/src/test/java/eu/gaudian/translator/utils/ApiArchitectureTest.kt delete mode 100644 app/src/test/java/eu/gaudian/translator/utils/BaseTest.kt delete mode 100644 app/src/test/java/eu/gaudian/translator/utils/JsonHelperTest.kt delete mode 100644 app/src/test/java/eu/gaudian/translator/utils/ServiceFixesTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2e73b66..7b5bbf9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -126,7 +126,6 @@ dependencies { implementation(libs.androidx.room.runtime) // ADDED: Explicitly add runtime implementation(libs.androidx.room.ktx) implementation(libs.core.ktx) - implementation(libs.androidx.compose.foundation) ksp(libs.room.compiler) // Networking diff --git a/app/src/androidTest/java/eu/gaudian/translator/TestConfig.kt b/app/src/androidTest/java/eu/gaudian/translator/TestConfig.kt index 6dba602..f59ea19 100644 --- a/app/src/androidTest/java/eu/gaudian/translator/TestConfig.kt +++ b/app/src/androidTest/java/eu/gaudian/translator/TestConfig.kt @@ -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" diff --git a/app/src/main/java/eu/gaudian/translator/di/RepositoryModule.kt b/app/src/main/java/eu/gaudian/translator/di/RepositoryModule.kt index bc22f88..a6ce147 100644 --- a/app/src/main/java/eu/gaudian/translator/di/RepositoryModule.kt +++ b/app/src/main/java/eu/gaudian/translator/di/RepositoryModule.kt @@ -1,5 +1,3 @@ -@file:Suppress("unused", "HardCodedStringLiteral") - package eu.gaudian.translator.di import android.app.Application diff --git a/app/src/main/java/eu/gaudian/translator/model/grammar/LocalDictionaryMorphologyMapper.kt b/app/src/main/java/eu/gaudian/translator/model/grammar/LocalDictionaryMorphologyMapper.kt index 67ef23e..ae733c6 100644 --- a/app/src/main/java/eu/gaudian/translator/model/grammar/LocalDictionaryMorphologyMapper.kt +++ b/app/src/main/java/eu/gaudian/translator/model/grammar/LocalDictionaryMorphologyMapper.kt @@ -56,6 +56,7 @@ object LocalDictionaryMorphologyMapper { /** * Overload that uses parsed [DictionaryEntryData] instead of raw JSON. */ + @Suppress("unused") fun parseMorphology( langCode: String, pos: String?, diff --git a/app/src/main/java/eu/gaudian/translator/model/repository/ApiRepository.kt b/app/src/main/java/eu/gaudian/translator/model/repository/ApiRepository.kt index 52db647..1137871 100644 --- a/app/src/main/java/eu/gaudian/translator/model/repository/ApiRepository.kt +++ b/app/src/main/java/eu/gaudian/translator/model/repository/ApiRepository.kt @@ -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 } diff --git a/app/src/main/java/eu/gaudian/translator/model/repository/LanguageRepository.kt b/app/src/main/java/eu/gaudian/translator/model/repository/LanguageRepository.kt index 233f129..6612d69 100644 --- a/app/src/main/java/eu/gaudian/translator/model/repository/LanguageRepository.kt +++ b/app/src/main/java/eu/gaudian/translator/model/repository/LanguageRepository.kt @@ -76,6 +76,7 @@ class LanguageRepository(private val context: Context) { } } + @Suppress("unused") suspend fun wipeHistoryAndFavorites() { clearLanguages(LanguageListType.HISTORY) clearLanguages(LanguageListType.FAVORITE) diff --git a/app/src/main/java/eu/gaudian/translator/utils/JsonHelper.kt b/app/src/main/java/eu/gaudian/translator/utils/JsonHelper.kt index 5dd8069..6191367 100644 --- a/app/src/main/java/eu/gaudian/translator/utils/JsonHelper.kt +++ b/app/src/main/java/eu/gaudian/translator/utils/JsonHelper.kt @@ -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 } diff --git a/app/src/main/java/eu/gaudian/translator/utils/Log.kt b/app/src/main/java/eu/gaudian/translator/utils/Log.kt index d235231..bd44fcc 100644 --- a/app/src/main/java/eu/gaudian/translator/utils/Log.kt +++ b/app/src/main/java/eu/gaudian/translator/utils/Log.kt @@ -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") diff --git a/app/src/main/java/eu/gaudian/translator/utils/StatusMessageIds.kt b/app/src/main/java/eu/gaudian/translator/utils/StatusMessageIds.kt index b0a01a9..8181515 100644 --- a/app/src/main/java/eu/gaudian/translator/utils/StatusMessageIds.kt +++ b/app/src/main/java/eu/gaudian/translator/utils/StatusMessageIds.kt @@ -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 diff --git a/app/src/main/java/eu/gaudian/translator/utils/StatusMessageService.kt b/app/src/main/java/eu/gaudian/translator/utils/StatusMessageService.kt index 524a410..1642162 100644 --- a/app/src/main/java/eu/gaudian/translator/utils/StatusMessageService.kt +++ b/app/src/main/java/eu/gaudian/translator/utils/StatusMessageService.kt @@ -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)) diff --git a/app/src/main/java/eu/gaudian/translator/utils/TranslationService.kt b/app/src/main/java/eu/gaudian/translator/utils/TranslationService.kt index a2c87a8..5741de6 100644 --- a/app/src/main/java/eu/gaudian/translator/utils/TranslationService.kt +++ b/app/src/main/java/eu/gaudian/translator/utils/TranslationService.kt @@ -117,7 +117,6 @@ class TranslationService(private val context: Context) { } suspend fun translateSentence(sentence: String): Result = withContext(Dispatchers.IO) { - val statusMessageService = StatusMessageService val additionalInstructions = settingsRepository.customPromptTranslation.flow.first() val selectedSource = languageRepository.loadSelectedSourceLanguage().first() val sourceLangName = selectedSource?.englishName ?: "Auto" diff --git a/app/src/main/java/eu/gaudian/translator/utils/dictionary/InflectionParser.kt b/app/src/main/java/eu/gaudian/translator/utils/dictionary/InflectionParser.kt deleted file mode 100644 index 8d5c630..0000000 --- a/app/src/main/java/eu/gaudian/translator/utils/dictionary/InflectionParser.kt +++ /dev/null @@ -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): DisplayInflectionData -} \ No newline at end of file diff --git a/app/src/main/java/eu/gaudian/translator/utils/dictionary/LocalDictionaryParser.kt b/app/src/main/java/eu/gaudian/translator/utils/dictionary/LocalDictionaryParser.kt index db04a29..915006d 100644 --- a/app/src/main/java/eu/gaudian/translator/utils/dictionary/LocalDictionaryParser.kt +++ b/app/src/main/java/eu/gaudian/translator/utils/dictionary/LocalDictionaryParser.kt @@ -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 - ) : DisplayInflectionData() } data class DisplayMood( diff --git a/app/src/main/java/eu/gaudian/translator/view/Navigation.kt b/app/src/main/java/eu/gaudian/translator/view/Navigation.kt index 43e74cf..9ccfefe 100644 --- a/app/src/main/java/eu/gaudian/translator/view/Navigation.kt +++ b/app/src/main/java/eu/gaudian/translator/view/Navigation.kt @@ -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" diff --git a/app/src/main/java/eu/gaudian/translator/view/composable/AppScaffold.kt b/app/src/main/java/eu/gaudian/translator/view/composable/AppScaffold.kt index 5b2dace..e7bd697 100644 --- a/app/src/main/java/eu/gaudian/translator/view/composable/AppScaffold.kt +++ b/app/src/main/java/eu/gaudian/translator/view/composable/AppScaffold.kt @@ -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 - ) - ) -} \ No newline at end of file diff --git a/app/src/main/java/eu/gaudian/translator/view/composable/AppTabLayout.kt b/app/src/main/java/eu/gaudian/translator/view/composable/AppTabLayout.kt index af6ebbb..99207ea 100644 --- a/app/src/main/java/eu/gaudian/translator/view/composable/AppTabLayout.kt +++ b/app/src/main/java/eu/gaudian/translator/view/composable/AppTabLayout.kt @@ -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 AppTabLayout( tabs: List, @@ -175,6 +167,7 @@ fun AppTabLayout( } } +@Suppress("HardCodedStringLiteral") @ThemePreviews @Composable fun ModernTabLayoutPreview() { diff --git a/app/src/main/java/eu/gaudian/translator/view/composable/BottomNavigationBar.kt b/app/src/main/java/eu/gaudian/translator/view/composable/BottomNavigationBar.kt index 1e8a290..38e7934 100644 --- a/app/src/main/java/eu/gaudian/translator/view/composable/BottomNavigationBar.kt +++ b/app/src/main/java/eu/gaudian/translator/view/composable/BottomNavigationBar.kt @@ -1,4 +1,4 @@ -@file:Suppress("HardCodedStringLiteral") +@file:Suppress("HardCodedStringLiteral", "AssignedValueIsNeverRead") package eu.gaudian.translator.view.composable diff --git a/app/src/main/java/eu/gaudian/translator/view/dialogs/CategorySelectionDialog.kt b/app/src/main/java/eu/gaudian/translator/view/dialogs/CategorySelectionDialog.kt index f414baa..93b39f9 100644 --- a/app/src/main/java/eu/gaudian/translator/view/dialogs/CategorySelectionDialog.kt +++ b/app/src/main/java/eu/gaudian/translator/view/dialogs/CategorySelectionDialog.kt @@ -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, diff --git a/app/src/main/java/eu/gaudian/translator/view/dialogs/StartExerciseDialog.kt b/app/src/main/java/eu/gaudian/translator/view/dialogs/StartExerciseDialog.kt deleted file mode 100644 index a12569a..0000000 --- a/app/src/main/java/eu/gaudian/translator/view/dialogs/StartExerciseDialog.kt +++ /dev/null @@ -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, - stages: List, - languageIds: List - ) -> 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>(emptyList()) } - var languages by remember { mutableStateOf>(emptyList()) } - // Map displayed Language to its DB id (lid) using position mapping from load - var languageIdMap by remember { mutableStateOf>(emptyMap()) } - var selectedLanguages by remember { mutableStateOf>(emptyList()) } - var selectedStages by remember { mutableStateOf>(emptyList()) } - var selectedCategories by remember { mutableStateOf>(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() - }, - 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() - }, - 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)) - } - } - } - } -} diff --git a/app/src/main/java/eu/gaudian/translator/view/dialogs/VocabularyReviewScreen.kt b/app/src/main/java/eu/gaudian/translator/view/dialogs/VocabularyReviewScreen.kt index 3b1043a..df0eeca 100644 --- a/app/src/main/java/eu/gaudian/translator/view/dialogs/VocabularyReviewScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/dialogs/VocabularyReviewScreen.kt @@ -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 by vocabularyViewModel.generatedVocabularyItems.collectAsState() - val categories by categoryViewModel.categories.collectAsState(initial = emptyList()) - + val selectedItems = remember { mutableStateListOf() } val duplicates = remember { mutableStateListOf() } var selectedCategories by remember { mutableStateOf>(emptyList()) } diff --git a/app/src/main/java/eu/gaudian/translator/view/dictionary/DictionaryResultComponents.kt b/app/src/main/java/eu/gaudian/translator/view/dictionary/DictionaryResultComponents.kt index 1d573b3..5227d7f 100644 --- a/app/src/main/java/eu/gaudian/translator/view/dictionary/DictionaryResultComponents.kt +++ b/app/src/main/java/eu/gaudian/translator/view/dictionary/DictionaryResultComponents.kt @@ -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 diff --git a/app/src/main/java/eu/gaudian/translator/view/dictionary/DictionaryTableComponents.kt b/app/src/main/java/eu/gaudian/translator/view/dictionary/DictionaryTableComponents.kt index 4a7f537..fb8a5ec 100644 --- a/app/src/main/java/eu/gaudian/translator/view/dictionary/DictionaryTableComponents.kt +++ b/app/src/main/java/eu/gaudian/translator/view/dictionary/DictionaryTableComponents.kt @@ -1,3 +1,5 @@ +@file:Suppress("SameParameterValue") + package eu.gaudian.translator.view.dictionary import androidx.compose.animation.animateContentSize diff --git a/app/src/main/java/eu/gaudian/translator/view/exercises/StartExerciseScreen.kt b/app/src/main/java/eu/gaudian/translator/view/exercises/StartExerciseScreen.kt index d7d56fc..454830e 100644 --- a/app/src/main/java/eu/gaudian/translator/view/exercises/StartExerciseScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/exercises/StartExerciseScreen.kt @@ -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 ) { diff --git a/app/src/main/java/eu/gaudian/translator/view/hints/AllHints.kt b/app/src/main/java/eu/gaudian/translator/view/hints/AllHints.kt index ccc5e52..39417b4 100644 --- a/app/src/main/java/eu/gaudian/translator/view/hints/AllHints.kt +++ b/app/src/main/java/eu/gaudian/translator/view/hints/AllHints.kt @@ -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), diff --git a/app/src/main/java/eu/gaudian/translator/view/hints/Hint.kt b/app/src/main/java/eu/gaudian/translator/view/hints/Hint.kt index 58c0edc..36076af 100644 --- a/app/src/main/java/eu/gaudian/translator/view/hints/Hint.kt +++ b/app/src/main/java/eu/gaudian/translator/view/hints/Hint.kt @@ -1,5 +1,3 @@ -@file:Suppress("HardCodedStringLiteral") - package eu.gaudian.translator.view.hints import androidx.compose.foundation.layout.Arrangement diff --git a/app/src/main/java/eu/gaudian/translator/view/hints/MarkdownHintLoader.kt b/app/src/main/java/eu/gaudian/translator/view/hints/MarkdownHintLoader.kt index 56e7a0e..0d00815 100644 --- a/app/src/main/java/eu/gaudian/translator/view/hints/MarkdownHintLoader.kt +++ b/app/src/main/java/eu/gaudian/translator/view/hints/MarkdownHintLoader.kt @@ -47,6 +47,7 @@ object MarkdownHintLoader { append(language.lowercase()) } if (country.isNotEmpty()) { + @Suppress("HardCodedStringLiteral") append("-r") append(country.uppercase()) } diff --git a/app/src/main/java/eu/gaudian/translator/view/settings/LanguageOptionsScreen.kt b/app/src/main/java/eu/gaudian/translator/view/settings/LanguageOptionsScreen.kt index 51624d1..f0732f5 100644 --- a/app/src/main/java/eu/gaudian/translator/view/settings/LanguageOptionsScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/settings/LanguageOptionsScreen.kt @@ -128,6 +128,7 @@ fun LanguageOptionsScreen( } if (showAddLanguageDialog) { + @Suppress("KotlinConstantConditions") AddCustomLanguageDialog( showDialog = showAddLanguageDialog, onDismiss = { showAddLanguageDialog = false }, diff --git a/app/src/main/java/eu/gaudian/translator/view/settings/LayoutOptionsScreen.kt b/app/src/main/java/eu/gaudian/translator/view/settings/LayoutOptionsScreen.kt index 04cc328..38a0eed 100644 --- a/app/src/main/java/eu/gaudian/translator/view/settings/LayoutOptionsScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/settings/LayoutOptionsScreen.kt @@ -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( diff --git a/app/src/main/java/eu/gaudian/translator/view/settings/VocabularyProgressOptionsScreen.kt b/app/src/main/java/eu/gaudian/translator/view/settings/VocabularyProgressOptionsScreen.kt index 6949483..c338fbe 100644 --- a/app/src/main/java/eu/gaudian/translator/view/settings/VocabularyProgressOptionsScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/settings/VocabularyProgressOptionsScreen.kt @@ -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, diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/DashboardContent.kt b/app/src/main/java/eu/gaudian/translator/view/vocabulary/DashboardContent.kt index 915c8d5..016bdc4 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/DashboardContent.kt +++ b/app/src/main/java/eu/gaudian/translator/view/vocabulary/DashboardContent.kt @@ -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 diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/NewWordReviewScreen.kt b/app/src/main/java/eu/gaudian/translator/view/vocabulary/NewWordReviewScreen.kt index 6450a0b..3878ab1 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/NewWordReviewScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/vocabulary/NewWordReviewScreen.kt @@ -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) diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/NewWordScreen.kt b/app/src/main/java/eu/gaudian/translator/view/vocabulary/NewWordScreen.kt index badea30..e124ae2 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/NewWordScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/vocabulary/NewWordScreen.kt @@ -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(null) } var selectedLangSecond by remember { mutableStateOf(null) } - var parseError by remember { mutableStateOf(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 ) diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/NoGrammarItemsScreen.kt b/app/src/main/java/eu/gaudian/translator/view/vocabulary/NoGrammarItemsScreen.kt index 29307bd..91eac16 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/NoGrammarItemsScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/vocabulary/NoGrammarItemsScreen.kt @@ -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( diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/StartScreen.kt b/app/src/main/java/eu/gaudian/translator/view/vocabulary/StartScreen.kt deleted file mode 100644 index e5d708c..0000000 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/StartScreen.kt +++ /dev/null @@ -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) -> 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, - 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, - onExerciseTypeSelected: (VocabularyExerciseType) -> Unit, - hideTodayOnlySwitch: Boolean = false, - selectedOriginLanguage: Language?, - onOriginLanguageChanged: (Language?) -> Unit, - selectedTargetLanguage: Language?, - onTargetLanguageChanged: (Language?) -> Unit, - allItems: List, -) { - 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, - 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 - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularyCardHost.kt b/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularyCardHost.kt index 4f09e55..1638dbb 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularyCardHost.kt +++ b/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularyCardHost.kt @@ -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 diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularyExerciseHostScreen.kt b/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularyExerciseHostScreen.kt index 95e7864..6ea97d0 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularyExerciseHostScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularyExerciseHostScreen.kt @@ -1,3 +1,5 @@ +@file:Suppress("HardCodedStringLiteral") + package eu.gaudian.translator.view.vocabulary import androidx.activity.compose.BackHandler diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularyHeatMapScreen.kt b/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularyHeatMapScreen.kt index 84a60a0..5f52c75 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularyHeatMapScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularyHeatMapScreen.kt @@ -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) diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularySortingScreen.kt b/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularySortingScreen.kt index 75ae104..e42461d 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularySortingScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularySortingScreen.kt @@ -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>(emptyList()) } @@ -310,7 +309,6 @@ fun VocabularySortingItem( var articlesLangSecond by remember { mutableStateOf(emptySet()) } 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) { diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/card/VocabularyCard.kt b/app/src/main/java/eu/gaudian/translator/view/vocabulary/card/VocabularyCard.kt index befeb33..132d92f 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/card/VocabularyCard.kt +++ b/app/src/main/java/eu/gaudian/translator/view/vocabulary/card/VocabularyCard.kt @@ -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, diff --git a/app/src/main/java/eu/gaudian/translator/viewmodel/DictionaryViewModel.kt b/app/src/main/java/eu/gaudian/translator/viewmodel/DictionaryViewModel.kt index 28d8e53..90f14f2 100644 --- a/app/src/main/java/eu/gaudian/translator/viewmodel/DictionaryViewModel.kt +++ b/app/src/main/java/eu/gaudian/translator/viewmodel/DictionaryViewModel.kt @@ -995,7 +995,7 @@ class DictionaryViewModel @Inject constructor( * Returns true if data is still loading (null). */ fun getStructuredDictionaryDataLoading(entry: DictionaryWordEntry): StateFlow { - 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) diff --git a/app/src/main/java/eu/gaudian/translator/viewmodel/ExerciseViewModel.kt b/app/src/main/java/eu/gaudian/translator/viewmodel/ExerciseViewModel.kt index 992d911..d40c9c3 100644 --- a/app/src/main/java/eu/gaudian/translator/viewmodel/ExerciseViewModel.kt +++ b/app/src/main/java/eu/gaudian/translator/viewmodel/ExerciseViewModel.kt @@ -209,13 +209,6 @@ class ExerciseViewModel @Inject constructor( } } - fun startAdHocExercise(exercise: Exercise, questions: List) { - _exerciseSessionState.value = ExerciseSessionState( - exercise = exercise, - questions = questions - ) - } - fun startExercise(exercise: Exercise) { viewModelScope.launch { val allQuestions = exerciseRepository.getAllQuestionsFlow().first() diff --git a/app/src/main/java/eu/gaudian/translator/viewmodel/ProgressViewModel.kt b/app/src/main/java/eu/gaudian/translator/viewmodel/ProgressViewModel.kt index ce789f5..b37f1b1 100644 --- a/app/src/main/java/eu/gaudian/translator/viewmodel/ProgressViewModel.kt +++ b/app/src/main/java/eu/gaudian/translator/viewmodel/ProgressViewModel.kt @@ -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 diff --git a/app/src/main/java/eu/gaudian/translator/viewmodel/VocabularyExerciseViewModel.kt b/app/src/main/java/eu/gaudian/translator/viewmodel/VocabularyExerciseViewModel.kt index 8c0a5ab..e981d46 100644 --- a/app/src/main/java/eu/gaudian/translator/viewmodel/VocabularyExerciseViewModel.kt +++ b/app/src/main/java/eu/gaudian/translator/viewmodel/VocabularyExerciseViewModel.kt @@ -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, config: ExerciseConfig diff --git a/app/src/main/res/drawable/ic_empty.png b/app/src/main/res/drawable/ic_empty.png deleted file mode 100644 index 32399d863d261b1e40b22983dda5669f8ab94230..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 39146 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 KMNUMnLSTZqR_B!f diff --git a/app/src/main/res/values-de-rDE/strings.xml b/app/src/main/res/values-de-rDE/strings.xml index 533cd78..d101659 100644 --- a/app/src/main/res/values-de-rDE/strings.xml +++ b/app/src/main/res/values-de-rDE/strings.xml @@ -36,7 +36,6 @@ Erfolgsmeldung anzeigen Kategorie hinzufügen Einstellungen - Dashboard Entwickleroptionen Mehrere Übersetzung @@ -61,7 +60,6 @@ Keine Zeilen zum Importieren. Bitte Spalten und Kopfzeile prüfen. %1$d Vokabeln importiert. Importieren - Vokabular mit KI erstellen YouTube-Übung erstellen YouTube-Link Passe die Intervalle und Kriterien für das Verschieben von Vokabeln an. Karten in niedrigeren Stufen werden öfter abgefragt. @@ -94,10 +92,7 @@ Laden… Laden anzeigen Laden abbrechen - Dies ist eine Info-Nachricht. Info-Nachricht anzeigen - Erfolg! - Hoppla! Etwas ist schiefgegangen. Fehlermeldung anzeigen Intro zurücksetzen Versionsinformation nicht verfügbar. @@ -127,7 +122,6 @@ API-Schlüssel eingeben Schlüssel speichern Modell auswählen - Vorschau-Titel Keine Manuelle Vokabelliste Filter: Alle Einträge @@ -193,7 +187,6 @@ Schwierigkeit: %1$s Anzahl: %1$d Fragen Erstellen - Lass die KI Vokabeln für dich finden! Suchbegriff Tipp Sprachen auswählen @@ -225,8 +218,6 @@ Ziel erreicht Heute keine Vokabeln fällig Alle ansehen - Eigene Übung - Tägliche Übung Wörter gesamt Gelernt Übrig @@ -235,7 +226,6 @@ Lernkriterien Min. richtig zum Aufsteigen Max. falsch zum Absteigen - Tägliches Lernziel Sicherung & Wiederherstellung Vokabeldaten exportieren Vokabeldaten importieren @@ -333,7 +323,6 @@ Zuletzt falsch: %1$s Richtige Antworten: %1$d Falsche Antworten: %1$d - Karte (%1$d/%2$d) Eintrags-ID: %1$d Statistiken werden geladen… nach %1$s @@ -364,7 +353,6 @@ Mehr Aktionen Alle auswählen Auswahl aufheben - Vokabular suchen… Keine Vokabeln gefunden. Vielleicht die Filter ändern? Kategorie: %1$s Repository-Status importiert von %1$s @@ -469,8 +457,6 @@ Ordne diese Elemente zu: Übersetze Folgendes (%1$s): Deine Übersetzung - Dies ist ein Hinweis. - Dies ist der Hauptinhalt. Dies ist der Inhalt in der Karte. Primärer Button Primär mit Icon @@ -488,15 +474,12 @@ Basis-URL (z.B. \'http://192.168.0.99:1234/\') Auswahlmodus schließen %1$d ausgewählt - Suchanfrage Suche schließen - Verwandte Vokabeln generieren Verwerfen Merkmale für \'%1$s\' bearbeiten Keine Grammatikkonfiguration für diese Sprache gefunden. Wortart Level - Schnelle Wortpaare Stufenfilter Sprachpaar Sprachfilter @@ -546,10 +529,6 @@ Freundlich Akademisch Kreativ - Text bearbeiten: %1$s - Kein Text empfangen! - Fehler: Kein Text zum Bearbeiten - Nicht mit zu bearbeitendem Text gestartet Eine einfache Liste, um deine Vokabeln manuell zu sortieren Stimme Standard @@ -590,21 +569,11 @@ Wenn du Hilfe brauchst, findest du Hinweise in allen Bereichen der App. Navigationsleisten-Beschriftungen Textbeschriftungen in der Hauptnavigationsleiste anzeigen. - Wortpaar-Einstellungen - Anzahl der Fragen: %1$d - Fragen mischen - Trainingsmodus - Bilde die Paare - Wortpaar-Übung Trainingsmodus ist aktiv: Antworten beeinflussen den Fortschritt nicht. " Tage" Vokabel hinzufügen - Vokabular mit KI erstellen - Keine Vokabeln gefunden. Jetzt hinzufügen? Dadurch werden alle konfigurierten API-Anbieter, Modelle und gespeicherten API-Schlüssel entfernt. Diese Aktion kann nicht rückgängig gemacht werden. Alle Anbieter und Modelle löschen? - Seiten tauschen - Kein Fortschritt Theme-Vorschau Beispielwort Übersetungs-Server verwenden diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 1117d39..ba534cc 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -36,7 +36,6 @@ Mostrar Mensagem de Sucesso Adicionar Categoria Configurações - Painel Opções do Desenvolvedor Múltiplos Configurações de Tradução @@ -61,7 +60,6 @@ Nenhuma linha para importar. Verifique as colunas e o cabeçalho. %1$d itens de vocabulário importados. Importar - Gerar vocabulário com IA Criar Exercício do YouTube Link do YouTube 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. @@ -93,10 +91,7 @@ Carregando… Mostrar Carregamento Cancelar Carregamento - Esta é uma mensagem informativa. Mostrar Mensagem Informativa - Sucesso! - Oops! Algo deu errado. Mostrar Mensagem de Erro Resetar Introdução Informação de versão não disponível. @@ -125,7 +120,6 @@ Inserir Chave de API Salvar Chave Selecionar Modelo - Título de Prévia Nenhum Lista de vocabulário manual Filtro: Todos os itens @@ -191,7 +185,6 @@ Dificuldade: %1$s Quantidade: %1$d Perguntas Gerar - Deixe a IA encontrar vocabulário para você! Termo de Busca Selecionar Idiomas Selecionar Quantidade @@ -222,8 +215,6 @@ Meta Atingida Nenhum Vocabulário para Hoje Ver Todos - Exercício Personalizado - Exercício Diário Total de Palavras Aprendidas Restantes @@ -232,8 +223,7 @@ Critérios de Aprendizagem Mín. de Acertos para Avançar Máx. de Erros para Regredir - Meta de Aprendizagem Diária - Meta de Respostas Corretas por Dia + Meta de Respostas Corretas por Dia Backup e Restauração Exportar Dados do Vocabulário Importar Dados do Vocabulário @@ -332,7 +322,6 @@ Último erro: %1$s Respostas corretas: %1$d Respostas incorretas: %1$d - Cartão (%1$d/%2$d) ID do Item: %1$d Carregando estatísticas… para %1$s @@ -363,7 +352,6 @@ Mais ações Selecionar Tudo Desmarcar Tudo - Pesquisar vocabulário… Nenhum item de vocabulário encontrado. Que tal tentar mudar os filtros? Categoria: %1$s Estado do repositório importado de %1$s @@ -467,8 +455,6 @@ Associe estes itens: Traduza o seguinte (%1$s): Sua tradução - Esta é uma dica. - Este é o conteúdo principal. Este é o conteúdo dentro do cartão. Botão Primário Primário com Ícone @@ -486,15 +472,12 @@ URL Base (ex: \'http://192.168.0.99:1234/\') Fechar modo de seleção %1$d Selecionado(s) - Termo de pesquisa Fechar pesquisa - Gerar itens de vocabulário relacionados Dispensar Editar Recursos para \'%1$s\' Nenhuma configuração de gramática encontrada para este idioma. Tipo de Palavra Níveis - Pares de palavras rápidos Filtro de Estágio Par de Idiomas Filtro de Idioma @@ -544,10 +527,6 @@ Amigável Acadêmico Criativo - Editando Texto: %1$s - Nenhum texto recebido! - Erro: Nenhum texto para editar - Não iniciado com texto para editar Uma lista simples para organizar o seu vocabulário manualmente Voz Padrão @@ -588,21 +567,11 @@ Se precisar de ajuda, você pode encontrar dicas em todas as seções do aplicativo. Rótulos da Barra de Navegação Mostrar rótulos de texto na barra de navegação principal. - Configurações de Pares de Palavras - Quantidade de perguntas: %1$d - Embaralhar perguntas - Modo de treino - Combine os pares - Exercício de Pares de Palavras Modo de treino ativado: respostas não afetarão o progresso. " dias" Adicionar Vocabulário - Criar Vocabulário com IA - Nenhum item de vocabulário encontrado. Adicionar agora? Isso removerá todos os provedores de API, modelos e chaves de API configurados. Esta ação não pode ser desfeita. Excluir todos os provedores e modelos? - Trocar lados - Sem progresso Prévia do Tema Palavra de Exemplo Usar servidor de Tradução diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8830c01..01b906d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -57,8 +57,6 @@ %1$d Selected %1$s: The quick brown fox jumps over the lazy dog. - Daily Learning Goal - Danger Zone %1$d days @@ -96,8 +94,6 @@ Edit Edit Features for \'%1$s\' - Editing Text: %1$s - Email Address Email Log @@ -106,7 +102,6 @@ Endpoint (e.g., /v1/chat/completions/) No rows to import. Please check the selected columns and header row. - Error: No text to edit Error parsing table Error parsing table: %1$s Please select two languages. @@ -160,8 +155,6 @@ General Settings - Generate related vocabulary items - Get Started Got it! @@ -225,13 +218,11 @@ %1$d models Analyze Grammar Appearance - Help Apply Filters Article Backup and Restore By Language Cancel - Card (%1$d/%2$d) Casual Categories Category @@ -249,7 +240,6 @@ Continue Correct Create Exercise - Create Vocabulary with AI Custom Definitions Delete @@ -418,7 +408,6 @@ Max Wrong to Demote Create YouTube Exercise - Generate vocabulary with AI Merge Merge Items @@ -461,11 +450,9 @@ No Models Configured No models found No New Vocabulary to Sort - No text received! No vocabulary items found. Perhaps try changing the filters? Not available - Not launched with text to edit Number of Cards: %1$d / %2$d @@ -563,8 +550,6 @@ %1$d questions - Quick word pairs - Quit Refresh Word of the Day @@ -591,8 +576,6 @@ Search for a word\'s origin Search Models - Search query - Search vocabulary… Secondary Button Secondary Inverse @@ -673,12 +656,10 @@ Tap the words below to form the sentence - Target Correct Answers Per Day + Target Correct Answers Per Day Test - Training mode - 200 OK %1$d categories selected %1$d Languages Selected @@ -702,7 +683,6 @@ Amount: %1$d Amount: %1$d Questions Amount of cards - Amount of questions: %1$d An unexpected condition was encountered on the server. An unknown error occurred. And many more! … @@ -741,9 +721,7 @@ Copy corrected text Correct! Could not fetch a new word. - Custom Exercise 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. - Daily Exercise How many words do you want to answer correctly each day? Dark Day Streak @@ -827,12 +805,10 @@ Clear language pair selection to choose a direction. Language Options Last 7 Days - Let AI find vocabulary for you! Light List Loading… Manual vocabulary list - Match the pairs Mismatch between question IDs in exercise and questions found in repository. Mistral More options @@ -846,7 +822,6 @@ No items available No Key No models found - No progress No valid API configuration could be found in the settings. Before using this app, you have to configure at least one API provider. No vocabulary available. No Vocabulary Due Today @@ -891,8 +866,6 @@ Select Translations to Add Selected Version information not available. - Oops! Something went wrong. - This is an info message. Show Error Message Show Info Message Show Loading @@ -902,12 +875,9 @@ Shuffle Languages Shuffle what language comes first. Does not affect language direction preferences. Disable language direction preference to enable shuffling. - Shuffle questions Some items are in the wrong category. Stage %1$s Start Over - Success! - Swap sides Text That\'s not quite right. The correct answer is: @@ -934,13 +904,10 @@ Very Frequent View All Visit my website - No Vocabulary Items could be found. Add now? Vocabulary Prompt Watch Video Again Weekly Activity Word of the Day - Word Pair Exercise - Word Pair Settings Your Own AI YouTube Link @@ -949,16 +916,13 @@ The server could not understand the request. The server understood the request, but is refusing to authorize it. - This is a hint. This is a sample output text. This is the content inside the card. - This is the main content. This mode will not affect your progress in stages. Timeout Corrector - Dashboard Developer Options HTTP Status Codes Items Without Grammar @@ -966,7 +930,6 @@ Settings Show Success Message Single - Preview Title Due Today Streak @@ -990,7 +953,7 @@ Vocabulary Added Vocabulary Repository - Progress Settings + Progress Settings Website URL @@ -1043,7 +1006,6 @@ Finding the right AI model How translation works None - Select Search 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) @@ -1120,7 +1082,6 @@ Stats Library Edit - Total Words New Words Expand your vocabulary Settings @@ -1131,11 +1092,16 @@ See History Weekly Progress Go - Apply Filters Sort By Reset Filter Cards Organize Your Vocabulary in Groups Extract a New Word to Your List Scroll to top + Settings + Import CSV + AI Generator + New Words + Recently Added + View All diff --git a/app/src/test/java/eu/gaudian/translator/utils/ApiArchitectureTest.kt b/app/src/test/java/eu/gaudian/translator/utils/ApiArchitectureTest.kt deleted file mode 100644 index e3d6400..0000000 --- a/app/src/test/java/eu/gaudian/translator/utils/ApiArchitectureTest.kt +++ /dev/null @@ -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() - 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(relaxed = true) - val mockApiManager = mockk(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) - } - } -} diff --git a/app/src/test/java/eu/gaudian/translator/utils/BaseTest.kt b/app/src/test/java/eu/gaudian/translator/utils/BaseTest.kt deleted file mode 100644 index c59dfc2..0000000 --- a/app/src/test/java/eu/gaudian/translator/utils/BaseTest.kt +++ /dev/null @@ -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 -} diff --git a/app/src/test/java/eu/gaudian/translator/utils/JsonHelperTest.kt b/app/src/test/java/eu/gaudian/translator/utils/JsonHelperTest.kt deleted file mode 100644 index 37bedee..0000000 --- a/app/src/test/java/eu/gaudian/translator/utils/JsonHelperTest.kt +++ /dev/null @@ -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 - ) - - @Serializable - data class TestEntryPart( - val title: String, - val content: String - ) - - @Serializable - data class TestVocabularyResponse( - val flashcards: List - ) - - @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) - } -} diff --git a/app/src/test/java/eu/gaudian/translator/utils/ServiceFixesTest.kt b/app/src/test/java/eu/gaudian/translator/utils/ServiceFixesTest.kt deleted file mode 100644 index adf4679..0000000 --- a/app/src/test/java/eu/gaudian/translator/utils/ServiceFixesTest.kt +++ /dev/null @@ -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 { - 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() - 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() - 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) - } -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1e75aae..47731b5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,12 +1,12 @@ [versions] -agp = "9.0.0" +agp = "9.0.1" annotation = "1.9.1" converterGson = "3.0.0" core = "13.0.0" coreSplashscreen = "1.2.0" coreTesting = "2.2.0" datastorePreferences = "1.2.0" -foundation = "1.10.2" +foundation = "1.10.3" hiltAndroidTesting = "2.59.1" jsoup = "1.22.1" kotlin = "2.3.10" @@ -21,14 +21,14 @@ kotlinxCoroutinesTest = "1.10.2" kotlinxDatetime = "0.7.1" kotlinxSerializationJson = "1.10.0" lifecycleRuntimeKtx = "2.10.0" -activityCompose = "1.12.3" -composeBom = "2026.01.01" +activityCompose = "1.12.4" +composeBom = "2026.02.00" loggingInterceptor = "5.3.2" materialIconsExtended = "1.7.8" mockitoCore = "5.21.0" mockitoKotlin = "6.2.3" navigationCompose = "2.9.7" -pagingRuntimeKtx = "3.4.0" +pagingRuntimeKtx = "3.4.1" reorderable = "0.9.6" retrofit = "3.0.0" material = "1.13.0" @@ -36,14 +36,12 @@ material3 = "1.4.0" runner = "1.7.0" timber = "5.0.1" navigationTesting = "2.9.7" -foundationLayout = "1.10.2" +foundationLayout = "1.10.3" room = "2.8.4" coreKtxVersion = "1.7.0" truth = "1.4.5" zstdJni = "1.5.7-7" composeMarkdown = "0.5.8" -jitpack = "1.0.10" -foundationVersion = "1.10.3" [libraries] @@ -104,10 +102,8 @@ hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", ve hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version = "1.3.0" } mockk = { module = "io.mockk:mockk", version = "1.14.9" } compose-markdown = { module = "com.github.jeziellago:compose-markdown", version.ref = "composeMarkdown" } -androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundationVersion" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } -kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } hilt-android = { id = "com.google.dagger.hilt.android", version = "2.59.1" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 0e79b93..14b720f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -14,7 +14,9 @@ pluginManagement { } } dependencyResolutionManagement { + @Suppress("UnstableApiUsage") repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + @Suppress("UnstableApiUsage") repositories { google() mavenCentral()